<template>
<div class="row">
<div v-if="nameChosen && connected" class="col-12">
<div 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 px-2 py-1 mb-2" :class="getChatMessageClasses(msg)">
<span class="small">
<span v-if="msg.team" class="mr-1">
<fa-icon icon="lock" data-toggle="tooltip" data-placement="bottom" :title="`Send to ${msg.team}`" />
</span>
<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 class="chat-msg-content" v-html="formatMarkdown(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" id="chatmsg-composer" v-model="chatmsg" autocomplete="off" placeholder="Chat message..." aria-label="Compose chat message" aria-describedby="submit-chat-msg">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="emoji-picker">😀</button>
<button class="btn btn-outline-success" type="submit" id="submit-chat-msg">Send</button>
<button type="button" class="btn btn-outline-success dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Sender List</span>
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="#" @click.prevent="sendChatMessage()">Send to all</a>
<a class="dropdown-item" href="#" @click.prevent="sendChatMessage({team: 'leaders'})">Send to leaders</a>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<div v-else class="col-4 offset-4">
<div class="text-center mb-3">
<h1>Codenames</h1>
<img src="icon.png" width="100px;" />
</div>
<div class="card">
<div class="card-body">
<h5 class="card-title">
Start a new Codenames game
</h5>
<div class="card-text mx-2">
<form @submit.prevent="registerUser(username)">
<div class="form-group">
<label for="username" class="text-muted">Pick 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>
</div>
</div>
</div>
</template>
<script>
import EmojiButton from '@joeattardi/emoji-button';
import marked from 'marked';
export default {
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.$nextTick(_ => {
this.initEmojiPicker();
});
};
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);
}
},
initEmojiPicker() {
const emojiButton = document.getElementById('emoji-picker');
const picker = new EmojiButton();
picker.on('emoji', emoji => {
this.chatmsg += emoji;
});
emojiButton.addEventListener('click', _ => {
picker.togglePicker(emojiButton);
});
},
sendChatMessage(options = null) {
this.socket.send(`{
"type": "chat_send",
"msg": "${this.addslashes(this.chatmsg)}",
"options": ${JSON.stringify(options)}
}`);
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}"
}`);
},
addslashes(str) {
return str
.replace(/[\\"']/g, '\\$&')
.replace(/\u0000/g, '\\0');
},
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;
},
formatMarkdown(text) {
return marked(text);
}
},
data() {
return {
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.filter(c => {
return !c.team || (c.team === 'leaders' && this.isLeader);
});
} else {
return this.chats.filter(c => {
return !c.isSystem &&
(!c.team || (c.team === 'leaders' && this.isLeader));
});
}
},
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>