Mit unserem Wissen über Web-APIs und JSON wollen wir nun die TicTacToe Web-App weiterentwickeln, so dass zwei Spieler:innen übers Web gegeneinander spielen können.
Dazu muss Folgendes geschehen:
/game/<gameid>
: gibt den Zustand des Games mit Id <gameid>
im JSON-Format zurück./join
: nimmt an einem Spiel teil (entweder an einem bereits mit einem Spieler wartenden, oder es wird ein neues Spiel gestartet). Gibt anschliessend den Game-Zustand zurück./set/<gameid>/<cellid>
: Setzt im Game mit Id <gameid>
die Zelle <cellid>
(muss im Bereich von 0-8 sein) an. Gibt anschliessend den Game-Zustand zurück.npm init
npm install express cookie-parser
package.json
: "type": "module"
node-modules
zu .gitignore
hinzu.
Zuerst benötigen wir eine Datei app.js
, die definiert, wie der Server funktioniert, und welche APIs wir bereitstellen:
import express from 'express' const app = express() const port = 3000 // Hello World. app.get('/hello', (req, res) => { res.send('Hello World!') }) // Listen on the given port app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
Mit dem Kommandozeilenaufruf node app.js
wird der Node-Server gestartet. Auf der Adresse http://localhost:3000/hello können wir auf die oben definierte API zugreifen.
Der Code bedeutet: Wann immer eine Anfrage für den Pfad /hello
ankommt, so sende als Response (res) den String Hello World
zurück.
Es ist auch möglich, Teile der im Request (req) verlangten Adresse als Parameter zu erhalten:
app.get('/hello/:name', (req, res) => { let name = req.params['name'] res.send(`Hello ${name}!``) })
Die HTML und CSS Dateien bleiben unverändert - der Server soll diese bitte direkt zum Browser übertragen. Das erreichen wir, indem wir einen Ordner static
anlegen, und die Dateien dorthin verschieben. Node wird instruiert, diese Dateien unverändert auszuliefern:
// Serve all files from static/ as is. // For example, a request for '/tictactoe.html' will be served from // 'static/tictactoe.html' app.use(express.static('static'))
Ausprobieren: Node neu starten und den URL http://localhost:3000/tictactoe.html laden - unser HTML + CSS sollte korrekt angezeigt werden!
Erstelle eine Kopie deiner tictactoe.js
Datei, um auf dem Server ein Spiel laufen zu lassen. Idealerweise ist deine Datei bereits objektorientiert, damit der Server eine Vielzahl von Spielen parallell betreuen kann.
Was muss sich ändern?
let grid = Array(9).fill(' ')
.toJson()
, die Code ähnlich wie den folgenden produziert:{ id: 0, // game id state: "ended", // "waiting", "playing", "ended" grid: [ // the nine game grid cells "X", " ", " ", "X", "O", " ", "X", "O", " " ], winner: "X", // the player who won, if there is one. player: "X", // the user's color next: "O" // the color of the player next in turn }
In app.js
erstellen wir ein Spiel, z.B. mit let game = TicTacToe()
. Damit das funktioniert, müssen wir alle Funktionen oder Klassen in tictactoe.js
mit export
markieren und in app.js
importieren:
export class TicTacToe { ... }
import {TicTacToe} from './tictactoe.js' let the_game = new TicTacToe(); app.get('/game/:gameid', (req, res) => { return the_game.toJson(); }) app.get('/set/:gameid/:cell', (req, res) => { the_game.set(req.params['cell']) res.json(the_game.toJson()) })
Nun sollte unser Server ein einziges Game betreiben. Der Zustand sollte unter http://localhost:3000/game/0 zurückgeliefert werden. Ein Spielzug wird über das set
API gemacht, z.B. sollte das mittlere Feld mit http://localhost:3000/set/0/4 gesetzt werden können. Kannst du (ganz ohne Grafik, nur mit den obigen URLs) ein Spiel spielen?
Was gibt es noch zu lösen?
set
API aufrufen und anschliessend den Inhalt des HTMLs an den Spielzustand anpassen.X
oder O
ist?gameid
oben an…!)
Der Browser benötigt auch Javascript. Der Click-Handler jedes Buttons soll das set
API aufrufen und anschliessend den Inhalt des HTMLs an den Spielzustand anpassen.
class Tictactoe_Client { game_id = 0; // TODO set when joining game /** * Fetches the game state, updates the UI, and installs click handlers. */ async init() { let i = 0; for (const button of this.view.grid.getElementsByTagName("button")) { const cell = i; button.addEventListener('click', () => { this.handleJsonUrl(`/set/${this.game_id}/${cell}`); }); i++; } } /** * Fetches the given URL and updates the internal state from the parsed JSON. * * Keeps polling for updates if the updated state could change remotely. * * @param {string} url the URL to fetch that will return tictactoe JSON. */ async handleJsonUrl(url) { let response = await fetch(url); if (!response.ok) { let error = await response.text(); console.log("Error: " + error); return; } const json = await response.json(); this.updateHtml(json); } /** Update the HTML based on JSON game state. */ updateHtml(json) { let i = 0; for (const button of this.grid.getElementsByTagName("button")) { const cellText = json.grid[i]; // Set the data-state attribute which drives CSS formatting. button.setAttribute('data-state', cellText); // Set the text contents of the cell. button.textContent = cellText; i++; } } }
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. Das Cookie wird gesetzt wenn zum ersten Mal einem Spiel beigetreten wird:
import express from 'express' import cookieParser from 'cookie-parser' import {TicTacToe, GameState} from './tictactoe.js' const app = express() app.use(cookieParser()) const port = 3000 function getUserId(req, res) { let userid = req.cookies.userid if (userid == undefined) { userid = crypto.randomUUID() res.cookie('userid', userid) } return userid } app.get('/join', (req, res) => { const userid = getUserId(req, res) for (let game of Object.values(games)) { if (game.isWaiting()) { game.join(userid) res.json(game.toJson(userid)) return } } // No waiting game found - create a new one let game = new TicTacToe(nextGameId) games[nextGameId] = game nextGameId += 1 game.join(userid) res.json(game.toJson(userid)) })