server for my chess game
try to sanify game joining
| -rwxr-xr-x | log.sh | 6 | ||||
| -rw-r--r-- | src/game.js | 119 | ||||
| -rw-r--r-- | src/index.js | 203 | ||||
| -rw-r--r-- | src/infos.js | 30 | ||||
| -rw-r--r-- | src/utils.js | 13 |
5 files changed, 214 insertions, 157 deletions
@@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +heroku logs --tail --app=gd-chess-server | while read -r LINE; do + echo -ne "\e[0;36m$(date -d "$(echo "$LINE" | cut -d ' ' -f 1)" +%X): \e[0m" + echo "$LINE" | cut -d ' ' -f 3- +done diff --git a/src/game.js b/src/game.js new file mode 100644 index 0000000..1966982 --- /dev/null +++ b/src/game.js @@ -0,0 +1,119 @@ +import { Chess } from "chess.js"; +import { Infos } from "./infos.js"; +import { str_obj, flip_int, send_group_packet } from "./utils.js"; + +export class Game { + constructor(data, ws, wss) { + this.wss = wss; + this.hosterIndex = Number(!data.team); + this.clients = [undefined, undefined]; + this.clients[this.hosterIndex] = ws; // team === true : 0 ? 1 + this.gamecode = data.gamecode; + this.ids = [undefined, undefined]; // not a set so i can play against myself + this.ids[this.hosterIndex] = data.id; + this.infos = new Infos(this.hosterIndex, data.name, data.country); + this.spectators = []; + this.game = new Chess(); + if (data.hasOwnProperty("moves")) this.game.load_pgn(data.moves.join(" ")); + } + get pgn() { + return this.game.pgn(); + } + + get players() { + let players = 0; + this.ids.forEach((id) => { + if (id !== undefined) players++; + }); + return players; + } + + get client_count() { + let clients = 0; + this.clients.forEach((client) => { + if (client !== undefined) clients++; + }); + return clients; + } + + get joinerIndex() { + return flip_int(this.hosterIndex); + } + //true for valid + validate_move(move) { + const res = this.game.move(move); + if (str_obj(res) == "{}") return false; + this.game.undo(); + return true; + } + move(move) { + this.game.move(move); + } + undo() { + this.game.undo(); + } + remove_client(ws) { + this.clients[this.clients.indexOf(ws)] = undefined; + } + + add_client(ws, data) { + this.clients[this.joinerIndex] = ws; + this.ids[this.joinerIndex] = data.id; + this.infos.add(data.name, data.country); + } + + clean_clients(set_is_alive = true) { + function remove_client(c, wss, removal_func) { + if (c) { + if (c.is_alive === false) { + removal_func(c); + c.terminate(); + wss.clients.delete(c); + } else if (set_is_alive) c.is_alive = false; // becomes true on next ping + } + } + + this.spectators.forEach((spec) => { + remove_client(spec, this.wss, this.remove_spectator.bind(this)); + }); + + this.clients.forEach((client) => { + remove_client(client, this.wss, this.remove_client.bind(this)); + }); + } + + get dead() { + return this.client_count === 0 && this.spectators.length == 0; + } + + add_spectator(ws) { + this.spectators.push(ws); + } + remove_spectator(ws) { + this.spectators.slice(this.spectators.indexOf(ws)); + } + send_group_packet(packet, header) { + delete packet.gamecode; + send_group_packet(packet, header, this.clients); + send_group_packet(packet, header, this.spectators); + } + send_signal_packet(data, ws, header) { + let i = this.clients.indexOf(ws); + if (i !== -1) { + let sendto = this.clients[i ? 0 : 1]; + delete data.gamecode; // dont send the gamecode to the other player: waste of bytes + if (sendto) { + sendto.send_packet(data, header); + send_group_packet(data, header, this.spectators); // give it to the specs + return true; + } + } else console.log(`could not find client in game ${data.gamecode}`); + return false; + } + is_game_over() { + return this.game.game_over(); + } + reset_game() { + this.game = new Chess(); + } +} diff --git a/src/index.js b/src/index.js index 27d28c3..58921fe 100644 --- a/src/index.js +++ b/src/index.js @@ -8,7 +8,8 @@ import utils from "@gd-com/utils"; const { putVar, getVar } = utils; import { command } from "./pg.js"; import { self_ping } from "./ping.js"; -import { Chess } from "chess.js"; +import { Game } from "./game.js"; +import { str_obj, flip_int, send_group_packet } from "./utils.js"; const HEADERS = { relay: "R", @@ -28,16 +29,6 @@ const HEADERS = { // { gamecode: {clients: {}, ids: {}, infos: {names: [], countrys:[]} turn = true, pgn: ""} } let games = {}; -function str_obj(obj) { - return JSON.stringify(obj, null, 0); -} - -function send_group_packet(data, header, clients) { - clients.forEach((client) => { - if (client) client.send_packet(data, header); - }); -} - const auto_clean_clients = setInterval(() => { const keys = Object.keys(games); keys.forEach((key) => { @@ -84,7 +75,11 @@ wss.on("close", function close() { console.log(`Server started on port ${PORT}`); wss.on("connection", (ws, req) => { - console.log(`client connected (${req.socket.remoteAddress})`); + console.log( + `client connected (${ + req.headers["x-forwarded-for"] || req.socket.remoteAddress + })` + ); ws.is_alive = true; ws.heartbeat = function heartbeat() { this.is_alive = true; @@ -153,7 +148,6 @@ async function signin(data, ws) { } async function signup(data, ws) { - console.log("attempting to create user"); const res = await get_propertys(data.name); if (res) { ws.send_packet("err: user already exists", HEADERS.create_user); @@ -179,151 +173,53 @@ async function signup(data, ws) { ws.send_packet(id, HEADERS.create_user); } +// hoster does *not* call this function with itself. joiner must tell hoster about itself function handle_joinrequest(data, ws) { - function done(rejoin = false) { - console.log("joinrequest:", data.gamecode); - const i = game.clients.indexOf(undefined); - game.clients[i] = ws; - ws.send_packet({ idx: i }, HEADERS.joinrequest); - if (!rejoin) { - game.ids.push(data.id); - game.infos.names.push(data.name); - game.infos.countrys.push(data.country); - send_group_packet(game.pgn, HEADERS.loadpgn, game.clients); - if (game.clients[0] !== undefined) - game.clients[0].send_packet(game.infos.get(1), HEADERS.info); - if (game.clients[1] !== undefined) - game.clients[1].send_packet(game.infos.get(0), HEADERS.info); - } else ws.send_packet(game.pgn, HEADERS.loadpgn); // rejoin: dont send to both - let other = i == 0 ? 1 : 0; - if (game.clients[other] !== undefined) - game.clients[other].send_packet(game.infos.get(i), HEADERS.info); + function send_info(they_know_about_me = false) { + const us = game.ids.indexOf(data.id); + const them = flip_int(us); + // in a ideal world, clients dont disconnect and become undefined. we do not live in a ideal world. (sadly) (or they just left the game :/) + if (game.clients[them] !== undefined && !they_know_about_me) { + game.clients[them].send_packet(game.infos.get(us), HEADERS.info); + } + game.clients[us].send_packet(game.infos.get(them), HEADERS.info); } const game = games[data.gamecode]; - if (data.gamecode !== undefined && data.id !== undefined) { + if (data.gamecode !== undefined && data.id) { if (game !== undefined) { - game.clean_clients(false); // clean before attempting to join - if (game.ids.length < 2) done(); - else if ( - game.ids.includes(data.id) && - game.clients.indexOf(undefined) !== -1 && - data.id !== "" - ) - done(true); - else ws.send_packet("err: game full", HEADERS.joinrequest); + if (game.players < 2) { + // hoster is waiting for someone to join + game.add_client(ws, data); + ws.send_packet({ idx: game.joinerIndex }, HEADERS.joinrequest); // tell them what team they are + send_group_packet(game.pgn, HEADERS.loadpgn, game.clients); // hoster doesnt send a joinrequest, so tell it to load pgn too + send_info(); + console.log(`${data.name} joined ${data.gamecode}`); + } else if (game.ids.includes(data.id)) { + // someone is trying to rejoin + console.log( + `rejoin ${data.name} to ${ + game.ids.indexOf(data.id) === 0 ? "white" : "black" + }` + ); + game.clients[game.ids.indexOf(data.id)] = ws; + ws.send_packet(game.pgn, HEADERS.loadpgn); // pass them the pgn + send_info(true); // and send them their opponents info + } else { + console.log( + `rejected join to ${data.gamecode} (by ${data.name}): game full / id(${data.id}) not included in ${game.ids}` + ); + ws.send_packet("err: game full", HEADERS.joinrequest); + } } else ws.send_packet(`err: game does not exist`, HEADERS.joinrequest); } else ws.send_packet("err: gamecode or id not defined", HEADERS.joinrequest); } -class Game { - constructor(data, ws) { - this.clients = [undefined, undefined]; - this.clients[Number(!data.team)] = ws; - this.gamecode = data.gamecode; - this.ids = [data.id]; // not a set so i can play against myself - this.infos = { - names: [data.name], - countrys: [data.country], - get(index) { - return { - name: this.names[index], - country: this.countrys[index], - }; - }, - }; - this.spectators = []; - this.game = new Chess(); - if (data.hasOwnProperty("moves")) this.game.load_pgn(data.moves.join(" ")); - } - get pgn() { - return this.game.pgn(); - } - //true for valid - validate_move(move) { - const res = this.game.move(move); - if (str_obj(res) == "{}") return false; - this.game.undo(); - return true; - } - move(move) { - this.game.move(move); - } - undo() { - this.game.undo(); - } - remove_client(ws) { - this.clients[this.clients.indexOf(ws)] = undefined; - } - - clean_clients(set_is_alive = true) { - this.spectators.forEach((spec) => { - if (spec.is_alive === false) { - spec.terminate(); - wss.clients.delete(spec); - this.remove_spectator(spec); - } - }); - - this.clients.forEach((client) => { - if (client) { - if (client.is_alive === false) { - client.terminate(); - wss.clients.delete(client); - this.remove_client(client); - console.log("removed dead client from", this.gamecode); - } else if (set_is_alive) client.is_alive = false; // becomes true on next ping - } - }); - } - - get dead() { - this.clean_clients(false); - return ( - this.clients[0] == undefined && - this.clients[1] == undefined && - this.spectators.length == 0 - ); - } - add_spectator(ws) { - this.spectators.push(ws); - } - remove_spectator(ws) { - this.spectators.slice(this.spectators.indexOf(ws)); - } - send_group_packet(packet, header) { - delete packet.gamecode; - send_group_packet(packet, header, this.clients); - send_group_packet(packet, header, this.spectators); - } - send_signal_packet(data, ws, header) { - let i = this.clients.indexOf(ws); - if (i !== -1) { - let sendto = this.clients[i ? 0 : 1]; - delete data.gamecode; // dont send the gamecode to the other player: waste of bytes - if (sendto) { - sendto.send_packet(data, header); - console.log(`sending signal ${str_obj(data)}`); - send_group_packet(data, header, this.spectators); // give it to the specs - return true; - } - } else console.log(`could not find client in game ${data.gamecode}`); - return false; - } - is_game_over() { - return this.game.game_over(); - } - reset_game() { - this.game = new Chess(); - } -} - function handle_hostrequest(data, ws) { if (data.gamecode !== undefined && data.id !== undefined) { delete_game_if_empty(games[data.gamecode]); // see if its dead if (games[data.gamecode] === undefined) { if (data.team == undefined) data.team = true; - console.log("hostrequest:", data.gamecode); - games[data.gamecode] = new Game(data, ws); + games[data.gamecode] = new Game(data, ws, wss); ws.send_packet({ idx: Number(!data.team) }, HEADERS.hostrequest); console.log(`game ${data.gamecode} created`); } else { @@ -340,7 +236,8 @@ function handle_move(data, ws) { games[gc].validate_move(data.move) && signal_other(data, ws, HEADERS.move) ) - games[gc].move(data.move); + console.log("made move", data.move, "on", gc); + games[gc].move(data.move); } function handle_undo(data, ws) { @@ -355,22 +252,15 @@ function handle_undo(data, ws) { function handle_rematch(data, ws) { const gc = data.gamecode; if (games.hasOwnProperty(gc)) { - if (games[gc].is_game_over() === true) { - signal_other(data, ws, HEADERS.rematch); - // check if its a request, and if the request is accepted - if (data.accepted === true) games[gc].reset_game(); // reset if it is - } else - ws.send_packet( - { accepted: false, err: "Game not over" }, - HEADERS.rematch - ); + signal_other(data, ws, HEADERS.rematch); + // check if its a request, and if the request is accepted + if (data.accepted === true) games[gc].reset_game(); // reset if it is } } function handle_spectate(data, ws) { if (games.hasOwnProperty(data.gamecode)) { const game = games[data.gamecode]; - console.log("spectate " + data.gamecode); game.add_spectator(ws); const packet = { white: game.infos.get(0), @@ -386,7 +276,6 @@ function handle_spectate(data, ws) { function dual_relay(data, ws, header = HEADERS.relay) { if (games.hasOwnProperty(data.gamecode)) { games[data.gamecode].send_group_packet(data, header); - console.log(`relaying ${str_obj(data)} to both clients`); return true; } else console.log(`dual relay: game ${data.gamecode} does not exist`); return false; diff --git a/src/infos.js b/src/infos.js new file mode 100644 index 0000000..0e2e129 --- /dev/null +++ b/src/infos.js @@ -0,0 +1,30 @@ +import { flip_int } from "./utils.js"; + +export class Infos { + constructor(hosterIndex, hosterName, hosterCountry) { + this.hosterIndex = hosterIndex; + this.names = ["Anonymous", "Anonymous"]; + this.names[hosterIndex] = hosterName; + this.countrys = ["rainbow", "rainbow"]; + this.countrys[hosterIndex] = hosterCountry; + } + get(index) { + return { + name: this.names[index], + country: this.countrys[index], + }; + } + + get_all() { + return { w: this.get(0), b: this.get(1) }; + } + + get joinerIndex() { + return flip_int(this.hosterIndex); + } + + add(name, country) { + this.names[this.joinerIndex] = name; + this.countrys[this.joinerIndex] = country; + } +} diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..200df5d --- /dev/null +++ b/src/utils.js @@ -0,0 +1,13 @@ +export function str_obj(obj) { + return JSON.stringify(obj, null, 0); +} + +export function flip_int(int) { + return int === 0 ? 1 : 0; +} + +export function send_group_packet(data, header, clients) { + clients.forEach((client) => { + if (client) client.send_packet(data, header); + }); +} |