]> git.cworth.org Git - empires-server/commitdiff
Add a minimal implementation of the TicTacToe game engine
authorCarl Worth <cworth@cworth.org>
Wed, 27 May 2020 04:01:13 +0000 (21:01 -0700)
committerCarl Worth <cworth@cworth.org>
Wed, 27 May 2020 16:47:56 +0000 (09:47 -0700)
This implements a /move endpoint to allow clients to send moves to the
server and an /events endpoint for the server to broadcast the moves
to all connected clients.

This TicTacToe class has some duplicated code from the empires Game
class. We should refactor this to have a common parent class to reduce
duplication.

tictactoe.js

index 18fe3428806bee30c2c533182b15e02fee36993b..82c5f7e839d770cb4b4bb33f6f040ae9f079c6aa 100644 (file)
@@ -14,6 +14,58 @@ nunjucks.configure("templates", {
   express: app
 });
 
+class TicTacToe {
+  constructor() {
+    this.moves = [];
+    this.board = Array(9).fill(null);
+    this.clients = [];
+    this.next_client_id = 1;
+  }
+
+  /* Returns Boolean indicating whether move was legal and added. */
+  add_move(square) {
+    /* Cannot move to an occupied square. */
+    if (this.board[square])
+      return false;
+
+    this.board[square] = 'X';
+    this.moves.push(square);
+
+    return true;
+  }
+
+  add_client(response) {
+    const id = this.next_client_id;
+    this.clients.push({id: id,
+                       response: response});
+    this.next_client_id++;
+
+    return id;
+  }
+
+  remove_client(id) {
+    this.clients = this.clients.filter(client => client.id !== id);
+  }
+
+  /* Send a string to all clients */
+  broadcast_string(str) {
+    this.clients.forEach(client => client.response.write(str + '\n'));
+  }
+
+  /* Send an event to all clients.
+   *
+   * An event has both a declared type and a separate data block.
+   * It also ends with two newlines (to mark the end of the event).
+   */
+  broadcast_event(type, data) {
+    this.broadcast_string(`event: ${type}\ndata: ${data}\n`);
+  }
+
+  broadcast_move(square) {
+    this.broadcast_event("move", square);
+  }
+}
+
 app.get('/', (request, response) => {
   const game = request.game;
 
@@ -23,4 +75,42 @@ app.get('/', (request, response) => {
     response.render('tictactoe-game.html');
 });
 
+app.post('/move', (request, response) => {
+  const game = request.game;
+  const square = request.body.square;
+
+  const legal = game.add_move(square);
+
+  /* Inform this client whether the move was legal. */
+  response.send(JSON.stringify(legal));
+
+  /* And only if legal, inform all clients. */
+  if (! legal)
+    return;
+
+  game.broadcast_move(square);
+});
+
+app.get('/events', (request, response) => {
+  const game = request.game;
+
+  /* These headers will keep the connection open so we can stream events. */
+  const headers = {
+    "Content-type": "text/event-stream",
+    "Connection": "keep-alive",
+    "Cache-Control": "no-cache"
+  };
+  response.writeHead(200, headers);
+
+  /* Add this new client to our list of clients. */
+  const id = game.add_client(response);
+
+  /* And queue up cleanup to be triggered on client close. */
+  request.on('close', () => {
+    game.remove_client(id);
+  });
+});
+
 exports.app = app;
+exports.name = "tictactoe";
+exports.Game = TicTacToe;