Skip to content
Snippets Groups Projects
Select Git revision
  • 6f6160388227bd0657623870ff247d9a6fa1f11e
  • main default
  • kyle
  • chris
  • ptl46
5 results

server.js

Blame
  • user avatar
    AceZephyr authored
    6f616038
    History
    server.js 11.86 KiB
    const express = require("express");
    const {createServer} = require("http");
    const {Server} = require("socket.io");
    const sgen = require('sudoku-gen');
    const login = require("./login.js")
    const fs = require("fs");
    
    const app = express();
    app.use(express.json());
    app.use(require('cookie-parser')());
    
    const httpServer = createServer(app);
    const io = new Server(httpServer);
    
    app.use(express.static("public"));
    
    const {
        hostname,
        port
    } = fs.existsSync("../settings.json") ? JSON.parse(fs.readFileSync("../settings.json").toString()) : {
        hostname: "localhost",
        port: 3000
    };
    
    const GAMES = new Map(); // maps game id -> game
    
    const DEBUG_FLAG = false;
    
    const ROOM_OPEN = 0;
    const ROOM_PLAYING = 1;
    const ROOM_TERMINATED = -1;
    
    const CONV = new Map();
    CONV.set('-', 0);
    CONV.set('1', 1);
    CONV.set('2', 2);
    CONV.set('3', 3);
    CONV.set('4', 4);
    CONV.set('5', 5);
    CONV.set('6', 6);
    CONV.set('7', 7);
    CONV.set('8', 8);
    CONV.set('9', 9);
    const ARR9 = [...new Array(9).keys()];
    
    const DIFFICULTIES = ['easy', 'medium', 'hard', 'expert'];
    
    function convertPuzzle(sgenPuzzle) {
        return ARR9.map(x => ARR9.map(y => CONV.get(sgenPuzzle.at(9 * x + y))));
    }
    
    function generateBoard(difficulty) {
        if (!DIFFICULTIES.includes(difficulty))
            throw new Error("Invalid difficulty");
        const {puzzle, solution} = sgen.getSudoku(difficulty);
        return [convertPuzzle(puzzle), convertPuzzle(solution)];
    }
    
    class Game {
    
        initGame() {
            const gen = generateBoard(this.difficulty);
            this.boardDisplay = gen[0];
            if (DEBUG_FLAG) {
                this.boardDisplay = JSON.parse(JSON.stringify(gen[1]));
                this.boardDisplay[0][0] = 0;
            }
            this.boardSolution = gen[1];
            this.boardLock = ARR9.map(y => ARR9.map(x => this.boardDisplay[y][x] ? 1 : 0));
            this.boardColors = ARR9.map(y => ARR9.map(x => this.boardDisplay[y][x] ? 1 : 0));
            this.boardSpectatorColors = ARR9.map(y => ARR9.map(x => this.boardDisplay[y][x] ? 1 : 0));
            this.playerUsers.forEach(user => this.playerScores[user] = 0);
            this.state = ROOM_PLAYING;
            this.sendBoard();
        }
    
        packageBoardState(editable) {
            const playerColors = {};
            playerColors[this.playerUsers[0]] = "Yellow";
            playerColors[this.playerUsers[1]] = "Blue";
            return {
                boardDisplay: this.boardDisplay,
                boardLock: this.boardLock,
                boardColors: editable ? this.boardColors : this.boardSpectatorColors,
                playerScores: this.playerScores,
                playerColors: playerColors,
                editable: editable
            };
        }
    
        sendBoard() {
            // do not send board state if game is not in progress
            if (this.state === ROOM_PLAYING) {
                const playerClients = this.playerUsers.map(u => this.usersToClients.get(u));
                this.sendEach("board state", this.connectedClients, id => this.packageBoardState(playerClients.includes(id)));
            }
        }
    
        isSolved() {
            for (let y = 0; y < 9; y++) {
                for (let x = 0; x < 9; x++) {
                    if (this.boardDisplay[y][x] !== this.boardSolution[y][x]) {
                        return false;
                    }
                }
            }
            return true;
        }
    
        endGame() {
            this.sendAll("game complete", this.packageBoardState(false));
            for (let user in this.playerScores) {
                login.writeRecord(user, this.playerScores[user]);
            }
        }
    
        input(client, user, x, y, val) {
            let dPoints = 0;
            if (!(typeof x === "number" && typeof y === "number" && typeof val === "number" && 0 <= x && x <= 8 && 0 <= y && y <= 8 && 1 <= val && val <= 9))
                return false;
            if (!this.playerUsers.includes(user))
                return false;
            if (this.boardLock[y][x])
                return false;
            if (this.boardSolution[y][x] === val) {
                this.boardColors[y][x] = 1;
                this.boardSpectatorColors[y][x] = this.playerUsers.indexOf(user) + 3;
                this.boardLock[y][x] = 1;
                dPoints = 1;
            } else {
                this.boardColors[y][x] = 2;
                this.boardSpectatorColors[y][x] = 2;
                dPoints = -1;
            }
            this.boardDisplay[y][x] = val;
            this.playerScores[user] += dPoints;
    
            if (this.isSolved()) {
                this.endGame();
            }
    
            return true;
        }
    
        userJoin(clientId, user) {
            if (this.state === ROOM_OPEN && this.playerUsers.length < 2) {
                this.playerUsers.push(user);
                this.usersToClients.set(user, clientId);
                console.log(`Client ${clientId} User ${user} has joined game ${this.id}`)
                if (this.playerUsers.length === 2) {
                    try {
                        this.initGame();
                    } catch (e) {
                        this.kill();
                    }
                }
            }
        }
    
        clientJoin(clientId) {
            if (this.state === ROOM_TERMINATED) {
                return;
            }
            this.connectedClients.push(clientId);
        }
    
        kill() {
            this.state = ROOM_TERMINATED;
            this.sendAll("game stop");
            this.connectedClients = [];
            this.playerUsers = [];
            this.usersToClients.clear();
            GAMES.delete(this.id);
        }
    
        leave(client, user) {
            if (user)
                this.usersToClients.delete(user);
            if (this.connectedClients.includes(client))
                this.connectedClients.splice(this.connectedClients.indexOf(client), 1);
        }
    
        send(name, clients, message = null) {
            for (let clientId of clients) {
                io.in(clientId).fetchSockets().then(sockets => {
                    sockets.forEach(socket => {
                        socket.emit(name, message)
                    });
                });
            }
        }
    
        sendEach(name, clients, msgFn) {
            for (let clientId of clients) {
                io.in(clientId).fetchSockets().then(sockets => {
                    sockets.forEach(socket => {
                        socket.emit(name, msgFn(socket.id))
                    });
                });
            }
        }
    
        sendAll(name, message = null) {
            this.send(name, this.connectedClients, message);
        }
    
        constructor(id, difficulty) {
            this.id = id;
            this.difficulty = difficulty;
            this.boardDisplay = [];
            this.boardSolution = [];
            this.boardLock = [];
            this.boardColors = [];
            this.connectedClients = [];
            this.playerUsers = [];
            this.playerScores = {};
            this.usersToClients = new Map();
            this.state = ROOM_OPEN;
        }
    }
    
    function getGame(id) {
        const game = GAMES.get(id);
        return game && game.state !== ROOM_TERMINATED ? game : null;
    }
    
    const ROOM_CODE_LEN = 8;
    const ALPHABET = "0123456789";
    
    function randomId() {
        for (let i = 0; i < 1000; i++) {
            let id = "";
            for (let j = 0; j < ROOM_CODE_LEN; j++)
                id += ALPHABET.at(Math.floor(Math.random() * ALPHABET.length));
            if (!GAMES.has(id))
                return id;
        }
        // paranoia
        throw new Error("Failed to generate random id");
    }
    
    function clearBadToken(req, res) {
        const user = login.getUserForToken(req.cookies.token);
        if (!user)
            res.clearCookie("token");
    }
    
    function ensureAuth(req, res, fn) {
        const user = login.getUserForToken(req.cookies.token);
        if (!user) {
            res.clearCookie("token");
            return res.redirect("/login");
        }
        fn(user);
    }
    
    app.get("/", (req, res) => {
        clearBadToken(req, res);
        return res.sendFile(__dirname + "/semipublic/index.html");
    });
    
    app.get("/signup", (req, res) => {
        clearBadToken(req, res);
        return res.sendFile(__dirname + "/semipublic/signup.html");
    });
    
    app.get("/login", (req, res) => {
        clearBadToken(req, res);
        return res.sendFile(__dirname + "/semipublic/login.html");
    })
    
    app.get("/leaderboard", (req, res) => {
        clearBadToken(req, res);
        return res.sendFile(__dirname + "/semipublic/leaderboard.html");
    });
    
    app.get("/get-leaderboard", (req, res) => {
        const queryfunc = {'score': login.getTopScores, 'player': login.getTopPlayers}[req.query['query']];
        if (!queryfunc)
            return res.status(400).send({"code": 400, "error": "Bad Query"});
        try {
            return res.status(200).send({"code": 200, "data": queryfunc()[0].values});
        } catch (e) {
            return res.status(500).send({"code": 500, "error": e});
        }
    });
    
    app.post("/signup", (req, res) => {
        const user = req.body['user'];
        const pass = req.body['pass'];
        login.signup(user, pass).then(obj => {
            if (obj['token'])
                res.cookie('token', obj['token']);
            return res.status(obj['code']).send(obj);
        });
    });
    
    app.post("/login", (req, res) => {
        const user = req.body['user'];
        const pass = req.body['pass'];
        login.login(user, pass).then(obj => {
            if (obj['token'])
                res.cookie('token', obj['token']);
            return res.status(obj['code']).send(obj);
        });
    });
    
    app.get("/logout", (req, res) => {
        res.clearCookie("token");
        return res.redirect("/");
    });
    
    // debug output for now
    app.get("/debug/listGames", (req, res) => {
        if (GAMES.size === 0)
            return res.status(404).send("No games");
        return res.send(Array.from(GAMES.values()).map(g => `${g.id} | ${g.difficulty} | ${g.state}<br>${g.boardSolution}<br>`).reduce((a, b) => a + b));
    });
    
    app.get("/openGames", (req, res) => {
        return res.send(Array.from(GAMES.values())
            .filter(g => g.state === ROOM_OPEN)
            .map(g => ({
                "id": g.id,
                "players": g.playerUsers,
                "difficulty": g.difficulty
            })).sort((a, b) => a.id.compare(b.id)));
    });
    
    app.get("/create", (req, res) => {
        ensureAuth(req, res, user => {
            const difficulty = (req.query && req.query.difficulty && DIFFICULTIES.includes(req.query.difficulty)) ? req.query.difficulty : null;
            if (!difficulty)
                return res.redirect("/");
            const id = randomId();
            const game = new Game(id, difficulty);
            GAMES.set(id, game);
            return res.redirect(`/game/${id}`);
        });
    });
    
    function concat(a, b) {
        return `${a}${b}`;
    }
    
    app.get("/cheat/:id", (req, res) => {
        clearBadToken(req, res);
        const game = getGame(req.params.id);
        if (!game)
            return res.status(404).send("No such game");
        return res.status(200).send(`<pre>${game.boardSolution.map(row => `${row.reduce(concat)}\n`).reduce(concat)}</pre>`);
    });
    
    app.get("/game/:id", (req, res) => {
        clearBadToken(req, res);
        const game = getGame(req.params.id);
        if (!game)
            return res.status(404).send("No such game");
        return res.sendFile(__dirname + "/semipublic/game.html");
    });
    
    
    io.on("connection", (socket) => {
        let socketId = socket.id;
        console.log("A new client has connected:", socketId);
        const gameId = socket.handshake.query.gameId;
        const token = socket.handshake.query.token;
        const user = login.getUserForToken(token);
        const game = getGame(gameId);
        if (!game) {
            console.log(`Invalid game from client ${socketId} game id: ${gameId}`)
            return socket.disconnect();
        }
        game.clientJoin(socketId);
        console.log(`Client connected: ${socketId} to game ${gameId}`);
    
        if (game.state === ROOM_PLAYING) {
            socket.emit("board state", game.packageBoardState(user && Object.keys(game.playerScores).includes(user)));
        }
    
        socket.on("cell input", args => {
            if (game.playerUsers.includes(user)) {
                game.input(socketId, user, args.x, args.y, args.num);
            }
            game.sendBoard();
        });
    
        socket.on("join game", () => {
            if (user) {
                if (!game.playerUsers.includes(user)) {
                    game.userJoin(socketId, user);
                } else {
                    fn("duplicate");
                }
            } else {
                socket.emit("please log in");
            }
        });
    
        socket.on("disconnect", () => {
            if (game) {
                game.leave(socketId, user);
            }
        });
    });
    
    httpServer.listen(port, hostname,() => {
        console.log(`Listening on hostname ${hostname} port ${port}`);
    });