Mit unserem Wissen über Web-APIs und JSON wollen wir nun die Vier-Gewinnt 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.
    • Der Server muss die folgenden zwei dynamische Web-APIs zur Verfügung stellen:
      • /<gameid>/game: gibt den Zustand des Games mit Id <gameid> im JSON-Format zurück.
      • /<gameid>/set/<column>: Setzt im Game mit Id <gameid> die Spalte <column> (muss im Bereich von 0-6 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.

Wir haben express bereits so konfiguriert, dass er unsere bestehenden Dateien statisch ausliefert. Das bedeutet, dass die Dateien 1:1 so zum Client übertragen werden, so wie sie der Server in seinem static Ordner antrifft.

Im Gegensatz zu statischen Dateien wird der Inhalt eines dynamischen Pfads (Route) erst zur Laufzeit bestimmt. Wir schreiben also Javascript-Code, um den Inhalt der HTTP-Response zusammenzusetzen. Für unsere Routes bietet sich an, JSON als Austauschformat zu verwenden.

Wir schauen nochmals in unsere index.js Datei und fügen eine JSON-Route hinzu:

index.js
import express from 'express'
 
const app = express()
const port = 3001
 
app.get('/get_info', (req, res) => {
  // ein JS Object (wie ein Dictionary in Python)
  let content = {
    message: 'Hello',
    name: 'World'
  }
  // Response verwendet JSON
  res.json(content)
})
 
app.use('/:gameid/', express.static('static'))
 
// Listen on the given port
const server = app.listen(port, () => {
    console.log(`Example app listening on http://localhost:${server.address().port}/`)
})

Mit dem Kommandozeilenaufruf node index.js wird der Node-Server gestartet. Auf der Adresse http://localhost:3001/get_info können wir auf die oben definierte API zugreifen. Natürlich kann der Server den zurückgegebenen Inhalt beliebig verändern.

Ab jetzt soll der Server das Spiel verwalten. Die Clients können über den Aufruf der gewünschten URLs den Spielzustand verändern. Danach erhalten sie den neuen Zustand im JSON-Format und stellen ihn im HTML dar.

Dazu ist es nötig, unsere bisherige Spiel-JS-Datei (in static) in zwei Teile aufzuteilen:

  • Server-side:
    • Spiel-Erzeugung
    • Spielzüge und -logik (Gewinner, Wer ist am Zug?)
  • Client-side (im Browser):
    • alles was mit HTML zu tun hat (Click-Handler)
    • aufruf der korrekten URLs auf dem Server
    • Update des HTML, wenn ein neuer Spielzustand eintrifft

Erstelle eine Kopie deiner static/connect4.js Datei als connect4_server.js im gleichen Verzeichnis wie index.js.

Was muss sich ändern?

  • Entferne alles, was mit dem HTML zu tun hat (updateHtml, Click-Handler-Installation…)
  • Der Spielzustand muss als JSON vom Server zu den Clients gesendet werden. Glücklicherweise ist unser Spielzustand bereits JSON-kompatibel.
  • Wir möchten mehr als ein Spiel betreuen können - statt eine globale game Variable erstellen wir eine Funktion newGame in connect4_server.js, die ein neues Spiel erzeugt.

In index.js erstellen wir ein Spiel, z.B. mit let game = newGame(). Damit das funktioniert, müssen wir alle Funktionen oder Klassen in connect4_server.js mit export markieren und in app.js importieren (newGame und dropPiece):

connect4_server.js
/* Returns a fresh game. */
export function newGame(id = -1) {
  return {
    "id": id,
    "state": "waiting",  // or "waiting" or "won" or "tie"
    "board": [
      0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0,
    ],
    "next": 1,  // 1 or 2, the player whose turn it is, the winner if state is "won"
    "player1": undefined,
    "player2": undefined,
  }
}
 
/* Drops a piece into given column. */
export function dropPiece(game, column) {
  ...
}
index.js
import {newGame, dropPiece} from './connect4.js'
 
// FIXME: support multiple games in the future!
let the_game = newGame()
 
app.get('/:gameid/game', (req, res) => {
    // FIXME: find the game for the given gameid.
    res.json(the_game)
})
 
app.get('/:gameid/set/:column', (req, res) => {
    let column = parseInt(req.params['cell'])
    dropPiece(the_game, column)
    res.json(the_game)
})

Nun sollte unser Server ein einziges Game betreiben. Der Zustand sollte unter http://localhost:3001/0/game zurückgeliefert werden. Ein Spielzug wird über das set API gemacht, z.B. sollte die mittlere Spalte mit http://localhost:3001/0/set/4 gesetzt werden können. Kannst du (ganz ohne Grafik, nur mit den obigen URLs) ein Spiel spielen?

Wir möchten mehrere Games unterstützen. Statt the_game unterhalten wir ein Dictionary von gameid auf den zugehörigen Gamestate:

index.js
/** All games by gameid. */
let games = {}
 
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
    }
    res.json(game)
})
 
app.get('/:gameid/set/:column', (req, res) => {
    const game = games[parseInt(req.params['gameid'])]
    if (game == undefined) {
        // Send error code
        res.status(404).json("no such game")
    } else {
        let column = parseInt(req.params['column'])
        dropPiece(game, column)
        res.json(game)
    }
})

Was gibt es noch zu lösen?

  • Der Browser benötigt auch Javascript. Der Click-Handler jedes Buttons soll den set URL aufrufen und anschliessend den Inhalt des HTMLs an den Spielzustand anpassen.
  • Wie weiss der Browser, ob er gelb oder rot spielt?

Weiter gehts mit Client-Side Javascript!

  • talit/web/webapps/routes.txt
  • Zuletzt geändert: 2026-09-14 14:34
  • von hof