Newer
Older
codenames / src / js / components / App.vue
@Vinzenz Rosenkranz Vinzenz Rosenkranz on 9 May 2020 16 KB add .env config support (js only)
<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() {
		const protocol = process.env.USE_HTTPS === '1' ? 'wss' : 'ws';
                try {
                    this.socket = new WebSocket(`${protocol}://${process.env.HOST}:${process.env.PORT}${process.env.DIR}`);

                    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>