From 90941051a42263fed5bc16d0624510e4bd171810 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Tue, 26 May 2020 21:01:13 -0700 Subject: [PATCH] Add a minimal implementation of the TicTacToe game engine 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 | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/tictactoe.js b/tictactoe.js index 18fe342..82c5f7e 100644 --- a/tictactoe.js +++ b/tictactoe.js @@ -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; -- 2.43.0