Newer
Older
codenames / src / js / components / App.vue
@Vinzenz Rosenkranz Vinzenz Rosenkranz on 26 Apr 2020 14 KB move to webpack/npm build system
<template>
    <div class="row">
        <div v-if="nameChosen && connected">
            <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 v-html="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>
                        </div>
                    </div>
                </form>
            </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>
    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() {
                this.socket.send(`{
                            "type": "chat_send",
                            "msg": "${this.addslashes(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}"
                        }`);
            },
            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;
            }
        },
        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;
                } 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>