Select Git revision
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}`);
});