Newer
Older
codenames / index.html
@Vinzenz Rosenkranz Vinzenz Rosenkranz on 18 Apr 2020 17 KB add emojis
<!DOCTYPE html>
<html lang="en" class="h-100">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="Content-Language" content="en">
        <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>
        <script src="./js/emoji-button-3.0.0.min.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 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>
                <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.$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: {
                    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>