Um sicherzustellen, dass jede:r Benutzer:in nur in einem Spiel ist, und kein böswilliger Akteur einen falschen Spielzug ausführen kann, wollen wir eine Benutzer-Id für jede Verbindung erstellen. Dafür nützen wir Cookies.
In jedem HTTP Response kann der Server Cookies mitliefern, die der Browser speichert und beim nächsten Request an den gleichen Server im Request mitliefert.
Session-Cookies leben nur, solange das Browser-Fenster offen ist und sind in der Regel unproblematisch. Long-lived cookies können über Jahre installiert bleiben und bei jedem Seitenbesuch erneuert werden. Sie werden zum Teil benützt, um Benutzer über Webseiten hinweg zu verfolgen, und haben zur ganzen Diskussion um Cookies geführt. Um den Benutzer zu identifizieren (zum Beispiel nach erfolgreichem Login) sind sie aber unverzichtbar.
Das Cookie wird gesetzt wenn zum ersten Mal einem Spiel beigetreten wird. Wenn der Benutzer (Browser-Session) noch kein Cookie hat, so generieren wir eine nicht erratbare zufällige Id mit crypto.randomUUID():
import express from 'express' import cookieParser from 'cookie-parser' import { newGame, dropPiece, toJson, isWaiting, joinGame } from './connect4.js' const app = express() app.use(cookieParser()) const port = 3001 /** Retrieve the user's identifier from the cookies, or set a new one. */ function getUserId(req, res) { let userid = req.cookies.userid if (userid == undefined) { userid = crypto.randomUUID() res.cookie('userid', userid) } return userid } app.get('/:gameid/game', (req, res) => { const gameid = parseInt(req.params['gameid']) let game = games[gameid] if (game == undefined) { // Create a new game if it does not exist. game = newGame(gameid) games[gameid] = game } const userid = getUserId(req, res) /* join game that is waiting for players. */ if (isWaiting(game, userid)) { joinGame(game, userid) } res.json(game) })
Aha, wir benötigen die neuen Funktionen isWaiting und joinGame!
export function isWaiting(game, userid) { // a game is waiting if we are waiting and the player is not the first player already. return game.state == "waiting" && game.player1 != userid } export function joinGame(game, userid) { if (!isWaiting(game, userid)) { throw Error("Not waiting!") } if (game.player1 == undefined) { game.player1 = userid } else { game.player2 = userid game.state = "playing" } }
Nun können wir auch sicherstellen, dass nur ein berechtigter Spieler einen Stein setzen kann. In der /:gameid/set/ Schnittstelle wird die Benutzer-Id ausgelesen und überprüft, ob der Spieler überhaupt im Spiel mitspielt, und ob er an der Reihe ist. Andernfalls wird ein HTTP Status-Code aus dem 400er-Bereich zurückgegeben, z.B. 403 (Forbidden).
/** Make a play. Only allows the joined players to */ app.get('/:gameid/set/:column', (req, res) => { const userid = getUserId(req, res) const game = games[parseInt(req.params['gameid'])] if (game == undefined) { res.status(404).json("no such game") } else if (game.state != "playing") { res.status(403).json("game is not playing") } else if (getCurrentPlayer(game) != userid) { res.status(403) res.json("Not your turn, my friend") } else { let column = parseInt(req.params['column']) dropPiece(game, column) res.json(toJson(game, userid)) } })
/* Returns the playerid whose turn it is. */ export function getCurrentPlayer(game) { return game.next == 1 ? game.player1 : game.player2 }
Damit der Client weiss, ob er an der Reihe ist, müsste er seine eigene User-Id kennen. Wir könnten aber die Information auch einfach in das zurückgegebene JSON einbauen. Eine Möglichkeit wäre, dass wir für jede Zustandsabfrage überprüfen, ob der UserId des Requests gerade am Zug ist. Damit wir die Eigenschaft nicht auf der Server-Seite speichern, erstellen wir eine Kopie des JSON mit structuredClone (MDM).
export function toJson(game, userid) { let copy = structuredClone(game) // make a copy if (game.state == "playing") { if (copy.player1 == userid && copy.next == 1 || copy.player2 == userid && copy.next == 2) { copy["myturn"] = true } } else if (game.state == "won") { if (copy.player1 == userid && copy.next == 1 || copy.player2 == userid && copy.next == 2) { copy["winner"] = true } } return copy }
Wie weiss ein Spieler, dass er an der Reihe ist? Weiter geht's mit Polling!