<!DOCTYPE html>
<html lang="en" class="h-100">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" type="image/png" href="icon.png" sizes="32x32">
<link rel="icon" type="image/png" href="icon.png" sizes="64x64">
<link rel="icon" type="image/png" href="icon.png" sizes="96x96">
<title>Codenames</title>
<link rel="stylesheet" type="text/css" href="./css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="./css/style.css" />
<script src="./js/jquery-3.5.0.min.js"></script>
<script src="./js/bootstrap.min.js"></script>
<script src="./js/vue.js"></script>
</head>
<body class="h-100 d-flex flex-column">
<nav class="navbar navbar-light bg-light">
<a class="navbar-brand" href="#">
<img src="icon.png" width="30" height="30" class="d-inline-block align-top" alt="">
Codenames
</a>
</nav>
<div class="container-fluid" id="app">
<div v-if="nameChosen && connected" class="row">
<div class="col-7 offset-1">
<div class="d-flex flex-row justify-content-between">
<div>
<span :class="getPlayerColor('red')">
{{ openRedCards }}
</span>
-
<span :class="getPlayerColor('blue')">
{{ openBlueCards }}
</span>
</div>
<div :class="getPlayerColor()">
<h2 v-if="gameOver">
{{ winner }} won!
</h2>
<span v-else>
{{ teamName }}'s turn
</span>
</div>
</div>
<div class="card-deck m-3" v-for="r, i in size.rows">
<div class="card py-3 border" v-for="c, j in size.cols" :data-id="`${i}, ${j}`" :class="getClasses(i, j)" @click="touchCard(i, j)">
<div class="card-body text-center">
{{ words[i*size.cols + j].value }}
<div class="rounded-circle marker-icon" :class="getClasses(i, j, true)" v-show="isLeader && !wordRevealed(i, j)"></div>
</div>
</div>
</div>
<div class="d-flex flex-row justify-content-between">
<button type="button" class="btn btn-primary" @click="isLeader = !isLeader">
Toggle Leadership
</button>
<button type="button" class="btn btn-primary" @click="startNewGame()" v-if="gameOver">
Start new Game
</button>
<button type="button" class="btn btn-primary" @click="nextPlayer()" v-else>
End {{ teamName }}'s turn
</button>
</div>
</div>
<div class="col-3">
<span>Connected as {{ username }}</span>
<hr class="my-2">
<div class="custom-control custom-switch mb-2">
<input type="checkbox" class="custom-control-input" id="system-message-switch" v-model="showSystemMessages">
<label class="custom-control-label" for="system-message-switch">Show System Messages</label>
</div>
<div>
<div v-for="msg in filteredChats" class="alert small px-2 py-1 mb-2" :class="getChatMessageClasses(msg)">
<span class="small">
<span v-if="msg.isSystem">
System Message - {{ formatDate(msg.time) }}
</span>
<span v-else-if="msg.sender !== username">
{{ msg.sender }} - {{ formatDate(msg.time) }}
</span>
<span v-else>
{{ formatDate(msg.time) }} - You
</span>
</span>
<div>
{{ msg.msg }}
</div>
</div>
</div>
<hr class="my-2">
<form @submit.prevent="sendChatMessage()">
<div class="input-group mb-3">
<input type="text" class="form-control" v-model="chatmsg" placeholder="Chat message..." aria-label="Compose chat message" aria-describedby="submit-chat-msg">
<div class="input-group-append">
<button class="btn btn-outline-success" type="submit" id="submit-chat-msg">Send</button>
</div>
</div>
</form>
</div>
</div>
<div v-else>
<form @submit.prevent="registerUser(username)">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" v-model="username" />
</div>
<button type="submit" class="btn btn-success" :disabled="!username">
Connect
</button>
</form>
</div>
</div>
<script type="application/javascript">
var app = new Vue({
el: '#app',
mounted() {
},
methods: {
reset() {
this.winner = '';
this.redsTurn = true;
},
registerUser(username) {
this.username = username;
this.nameChosen = true;
this.initSocket();
},
initSocket() {
try {
this.socket = new WebSocket(`ws://localhost:8000/codenames/api/event.php`);
this.socket.onopen = _ => {
this.connected = true;
console.log("successfully connected!");
};
this.socket.onmessage = msg => {
let res = JSON.parse(msg.data);
switch(res.type) {
case 'init':
this.size.rows = res.data.rows;
this.size.cols = res.data.cols;
this.words.length = 0;
this.words = res.data.words;
this.socket.send(`{
"type": "join",
"name": "${this.username}"
}`);
break;
case 'new_game':
this.reset();
this.size.rows = res.data.rows;
this.size.cols = res.data.cols;
this.words.length = 0;
this.words = res.data.words;
break;
case 'next_player':
this.redsTurn = res.data.team === 'red';
break;
case 'reveal':
let word = this.getWord(res.data.row, res.data.col);
word.revealed = true;
break;
case 'player_joined':
this.onReceiveChatMessage({
sender: '',
isSystem: true,
msg: `Player ${res.data.name} joined the game`,
time: res.data.time
});
break;
case 'chat_received':
this.onReceiveChatMessage(res.data);
break;
case 'game_over':
this.onGameOver(res.data.winner);
break;
}
};
this.socket.onclose = msg => {
this.connected = false;
console.log("Disconnected :(");
};
} catch(e) {
this.connected = false;
console.log(e);
}
},
sendChatMessage() {
this.socket.send(`{
"type": "chat_send",
"msg": "${this.chatmsg}"
}`);
this.chatmsg = '';
},
onReceiveChatMessage(msgObj) {
this.chats.push(msgObj);
},
onGameOver(winner) {
this.winner = winner;
},
startNewGame() {
this.socket.send(`{
"type": "new_game"
}`);
},
nextPlayer() {
if(this.gameOver) return;
this.socket.send(`{
"type": "turnend",
"team": "${this.teamName}"
}`);
},
touchCard(row, col) {
if(this.gameOver) return;
if(this.isLeader) return;
if(this.getWord(row, col).revealed) return;
this.socket.send(`{
"type": "cardplayed",
"row_index": ${row},
"col_index": ${col},
"team": "${this.teamName}"
}`);
},
getPlayerColor(team) {
let red = team ? team === 'red' : this.redsTurn;
return {
'text-danger': red,
'text-primary': !red
};
},
getClasses(row, col, bgOnly) {
const word = this.getWord(row, col);
if(!!bgOnly) {
return {
'bg-danger': word.red,
'bg-primary': word.blue,
'bg-dark': word.mr_x,
'bg-warning': !word.red && !word.blue && !word.mr_x,
}
}
if(!word.revealed && !this.isLeader) {
let cls = {
'border-secondary': true,
};
if(!this.gameOver) {
cls['clickable'] = true;
}
return cls;
}
if(word.revealed) {
return {
'border-danger': word.red,
'border-primary': word.blue,
'border-dark': word.mr_x,
'border-warning': !word.red && !word.blue && !word.mr_x,
'bg-danger': word.red,
'bg-primary': word.blue,
'bg-dark': word.mr_x,
'bg-warning': !word.red && !word.blue && !word.mr_x,
'text-white': word.red || word.blue || word.mr_x,
'font-weight-bold': true
}
}
return {
'border-danger': word.red,
'border-primary': word.blue,
'border-dark': word.mr_x,
'border-warning': !word.red && !word.blue && !word.mr_x
};
},
getChatMessageClasses(msg) {
let classes = {};
if(msg.sender === this.username) {
classes['alert-primary'] = true;
classes['text-right'] = true;
classes['ml-auto'] = true;
classes['w-75'] = true;
} else if(msg.isSystem) {
classes['alert-warning'] = true;
classes['w-100'] = true;
} else {
classes['alert-info'] = true;
classes['w-75'] = true;
}
return classes;
},
formatDate(ms) {
const d = new Date(ms);
const h = this.padNumber(d.getHours());
const m = this.padNumber(d.getMinutes());
const s = this.padNumber(d.getSeconds());
return `${h}:${m}:${s}`;
},
padNumber(n) {
return String(n).padStart(2, '0');
},
getWord(row, col) {
return this.words[row * this.size.cols + col];
},
wordRevealed(row, col) {
return this.getWord(row, col).revealed;
}
},
data: {
showSystemMessages: false,
socket: null,
connected: false,
username: '',
nameChosen: false,
chats: [],
isLeader: false,
redsTurn: true,
words: [],
size: {
rows: 0,
cols: 0,
},
chatmsg: '',
winner: '',
},
computed: {
filteredChats() {
if(this.showSystemMessages) {
return this.chats;
} else {
return this.chats.filter(c => !c.isSystem);
}
},
teamName() {
return this.redsTurn ? 'red' : 'blue';
},
openRedCards() {
return this.words.filter(w => w.red && !w.revealed).length;
},
openBlueCards() {
return this.words.filter(w => w.blue && !w.revealed).length;
},
gameOver() {
return this.winner !== '';
}
}
})
</script>
</body>
</html>