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:

  • Die Spiel-Logik muss vom Browser auf den Server wandern.
    • Dazu muss ein Server-Prozess gestartet werden, wir verwenden dafür nodejs und express.
    • Der Server muss geeignete Web-APIs zur Verfügung stellen:
      • /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.
  • Das Javascript im Browser muss sich nur noch darum kümmern, die richtigen Web-APIs auf dem Server aufzurufen, und den Zustand des Spiels im HTML darzustellen.
  • Node.js installieren:
  • Projekt initialisieren:
    • Eine Eingabeaufforderung im Ordner der Webapp öffnen.
      • npm init
      • npm install express cookie-parser
    • Öffne die Datei package.json:
      • füge einen Eintrag hinzu: "type": "module"
    • Falls du git verwendest:
      • füge node-modules zu .gitignore hinzu.

Zuerst benötigen wir eine Datei app.js, die definiert, wie der Server funktioniert, und welche APIs wir bereitstellen:

app.js
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:

app.js
// 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?

  • Der Spielzustand kann nicht mehr im HTML gespeichert werden. Es bietet sich an, ein Array von Buchstaben zu verwenden let grid = Array(9).fill(' ').
  • Der Spielzustand muss als JSON vom Server zu den Clients gesendet werden. Schreibe eine Funktion 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:

tictactoe.js
export class TicTacToe {
  ...
}
app.js
import {TicTacToe} from './tictactoe.js'

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:

app.js
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))
})
  • ksk_ef/web/apps/server.1723374259.txt.gz
  • Zuletzt geändert: 2024-08-11 11:04
  • von hof