From 1ab140d932b72040eb3b32af2f7fb367b6401805 Mon Sep 17 00:00:00 2001 From: Clifton Palmer Date: Mon, 20 Sep 2021 21:06:48 -0500 Subject: [PATCH] Added basic controls, refactored database sync (#2) * Updated board with better sync * Let players choose stones * Added a clear button that works locally * Added nginx proxy for websocket port flattening * Added compose files for prod and test --- docker-compose-prod.yml | 26 +++++++++++ docker-compose.yml | 18 +++++-- htdocs/go.js | 55 ++++++++++++---------- htdocs/index.html | 11 ++++- nginx.conf | 23 +++++++++ socket/db.js | 60 +++++++++++++++++++++--- socket/server.js | 101 ++++++++++++++++++++++------------------ 7 files changed, 214 insertions(+), 80 deletions(-) create mode 100644 docker-compose-prod.yml create mode 100644 nginx.conf diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml new file mode 100644 index 0000000..a3d1860 --- /dev/null +++ b/docker-compose-prod.yml @@ -0,0 +1,26 @@ +version: '3' +networks: + frontend: + external: + name: proxy +services: + db: + image: mariadb:10.6 + environment: + MARIADB_ROOT_PASSWORD: admin + MARIADB_DATABASE: go + MARIADB_USER: socket + MARIADB_PASSWORD: socketpw + socket: + build: ./socket + image: cliftonpalmer/go-socket + networks: + - default + - frontend + httpd: + image: httpd:2.4 + volumes: + - ./htdocs:/usr/local/apache2/htdocs + networks: + - default + - frontend diff --git a/docker-compose.yml b/docker-compose.yml index 2db2369..c80c306 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,11 +10,23 @@ services: socket: build: ./socket image: cliftonpalmer/go-socket - ports: - - 3000:3000 httpd: image: httpd:2.4 volumes: - ./htdocs:/usr/local/apache2/htdocs + web: + image: nginx:1.17 + restart: on-failure + deploy: + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 5 + window: 10s + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + environment: + - NGINX_HOST=purplebirdman.com + - NGINX_PORT=80 ports: - - 8100:80 + - 8000:80 diff --git a/htdocs/go.js b/htdocs/go.js index d02e55d..7c417ca 100644 --- a/htdocs/go.js +++ b/htdocs/go.js @@ -14,25 +14,15 @@ var cellHeight = boardHeight / (boardSize - 1); var lastX; var lastY; -var playCount = 0; /* state of pieces 0: empty 1: white 2: black */ -function getStone(i) { - switch (i) { - case 1: - return 'white'; - case 2: - return 'black'; - default: - return 'empty'; - } -} - var session = 0; +var playerStone; + var state = []; for (var i = 0; i < boardSize; i++) { @@ -49,14 +39,13 @@ var socket; const connect = function() { return new Promise((resolve, reject) => { const socketProtocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:') - const port = 3000; + const port = window.location.port; const socketUrl = `${socketProtocol}//${window.location.hostname}:${port}/ws/` // Define socket socket = new WebSocket(socketUrl); socket.onopen = (e) => { - // Send a little test data, which we can use on the server if we want // Resolve the promise - we are connected resolve(); } @@ -68,13 +57,17 @@ const connect = function() { case "board": console.log("Setting board state"); parsed.data.forEach( function (move, index) { - state[move.x][move.y] = - move.state === 'white' ? 1 : - move.state === 'black' ? 2 : - 0; + state[move.x][move.y] = move.state; }); drawGrid(); break; + case "new": + for (var i = 0; i < boardSize; i++) { + for (var j = 0; j < boardSize; j++) { + state[i][j] = 0; + } + } + drawGrid(); default: console.log(msg); } @@ -225,19 +218,13 @@ canvas.addEventListener('mousedown', function(evt) try { // push state change to backend if(isOpen(socket)) { - var stone; - if (state[lastX][lastY] === 0) { - stone = playCount++ % 2 + 1; - } else { - stone = 0; - } socket.send(JSON.stringify({ "type":"move", "data": { "session":session, "x":lastX, "y":lastY, - "state":getStone(stone) + "state":state[lastX][lastY] > 0 ? 0 : playerStone } })); } else { @@ -302,4 +289,22 @@ function getGridPoint(evt) } // finish +document.getElementById("new").onclick = function () { + // new game, new session, etc + socket.send(JSON.stringify({ + "type":"new", + "data": { + "session":session + } + })); +}; + +const stones = document.getElementById("stones"); +playerStone = stones.selectedIndex; +stones.onchange = function () { + // let player pick stone type + playerStone = stones.selectedIndex; + console.log(`Player changed stone to ${playerStone}`); +}; + connect(); diff --git a/htdocs/index.html b/htdocs/index.html index 8c25bb1..f42815d 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -1,4 +1,13 @@
- +
+
+ + + +
diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..f27608f --- /dev/null +++ b/nginx.conf @@ -0,0 +1,23 @@ +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + server { + server_name go.purplebirdman.com; + location / { + proxy_pass http://httpd; + proxy_set_header Host $host; + } + location /ws { + proxy_pass http://socket:3000; + # websocket magic + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + } + } +} diff --git a/socket/db.js b/socket/db.js index 89fbef0..b439ee5 100644 --- a/socket/db.js +++ b/socket/db.js @@ -7,30 +7,76 @@ const dsn = { }; const pool = mariadb.createPool(dsn); -async function addMove(session_id, pos_x, pos_y, state) { +async function getMaxUpdatedState(session_id) { let conn; try { conn = await pool.getConnection(); + return await conn.query(` +SELECT + count(*) AS num_rows, + UNIX_TIMESTAMP(max_updated_at) AS last_updated +FROM go.state g JOIN ( + SELECT MAX(updated_at) AS max_updated_at + FROM go.state + WHERE session_id = ? +) x +ON x.max_updated_at = g.updated_at + `, [session_id]); + } catch (err) { + console.log(err); + } finally { + if (conn) conn.end(); + } +} + +async function deleteSession(session_id) { + let conn; + try { + conn = await pool.getConnection(); + return await conn.query( + "DELETE FROM go.state WHERE session_id = ?", + [session_id] + ); + } catch (err) { + console.log(err); + } finally { + if (conn) conn.end(); + } +} - var res = await conn.query(` +async function initBoard() { + let conn; + try { + conn = await pool.getConnection(); + return await conn.query(` CREATE TABLE IF NOT EXISTS go.state ( session_id INT UNSIGNED, x TINYINT UNSIGNED, y TINYINT UNSIGNED, - state ENUM('empty', 'white', 'black'), + state TINYINT UNSIGNED, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY(session_id, x, y) ); `); - res = await conn.query(` + } catch (err) { + console.log(err); + } finally { + if (conn) conn.end(); + } +} + +async function addMove(session_id, pos_x, pos_y, state) { + let conn; + try { + conn = await pool.getConnection(); + return await conn.query(` INSERT INTO go.state (session_id, x, y, state) values (?, ?, ?, ?) ON DUPLICATE KEY UPDATE state = VALUES(state); `, [session_id, pos_x, pos_y, state]); - return res; } catch (err) { console.log(err); } finally { @@ -52,6 +98,8 @@ SELECT x, y, state from go.state where session_id = ? } } -exports.pool = pool; +exports.getMaxUpdatedState = getMaxUpdatedState; +exports.deleteSession = deleteSession; +exports.initBoard = initBoard; exports.addMove = addMove; exports.getBoardState = getBoardState; diff --git a/socket/server.js b/socket/server.js index 7aeb30d..017bebd 100644 --- a/socket/server.js +++ b/socket/server.js @@ -9,67 +9,78 @@ function sleep(ms) { } async function pollStatefulChange(ws, session_id) { - let conn; - try { - conn = await db.pool.getConnection(); - var lastUpdated = 0; - while(true) { - var res = await conn.query(` -select UNIX_TIMESTAMP(MAX(updated_at)) as last_updated -from go.state -where session_id = ?; - `, [session_id]); - console.log(res); - let updatedAt = res[0].last_updated; - if ( updatedAt > lastUpdated) { - console.log("websocket poll: board updated!"); + var lastUpdated = 0; + var rowCount = 0; + while (true) { + try { + var res = await db.getMaxUpdatedState(session_id); + var newRowCount = res[0].num_rows; + var updatedAt = res[0].last_updated; + + // update board state of client if more moves + // have been added since max last timestamp + // If more moves have been added in <1 sec, + // use the row count for the max last updated timestamp + if (updatedAt > lastUpdated || rowCount < newRowCount) { lastUpdated = updatedAt; - res = await db.getBoardState(session_id); + rowCount = newRowCount; ws.send(JSON.stringify({ "type": "board", - "data": res + "data": await db.getBoardState(session_id) })); } await sleep(1000); + } catch(err) { + console.log(`websocket poll error: ${err}`); } - } catch(err) { - console.log(`websocket poll error: ${err}`); - } finally { - if (conn) conn.end(); - } + } } app.ws('/ws', async function(ws, req) { // poll for stateful change and notify clients to update their boards var session_id = 0; - pollStatefulChange(ws, 0); + db.initBoard(); - ws.on('message', async function(msg) { - console.log(`ws message: ${msg}`); + // send initial message to draw client board + ws.send(JSON.stringify({ + "type": "board", + "data": await db.getBoardState(session_id) + })); - var parsed = JSON.parse(msg); - switch (parsed.type) { - case "move": - await db.addMove( - parsed.data.session, - parsed.data.x, - parsed.data.y, - parsed.data.state, - ); - // fall through and return new board state - case "board": - var res = await db.getBoardState( - parsed.data.session - ); - ws.send(JSON.stringify({ - "type": "board", - "data": res - })); - break; - default: - console.log("ws message: Unknown message type: " + type); + ws.on('message', async function(msg) { + let parsed; + try { + parsed = JSON.parse(msg); + switch (parsed.type) { + case "new": + ws.send(JSON.stringify({ + "type": "new", + "data": await db.deleteSession(parsed.data.session) + })); + break; + case "move": + await db.addMove( + parsed.data.session, + parsed.data.x, + parsed.data.y, + parsed.data.state, + ); + // fall through and return new board state + case "board": + ws.send(JSON.stringify({ + "type": "board", + "data": await db.getBoardState(parsed.data.session) + })); + break; + default: + console.log("ws message: Unknown message type: " + type); + } + } catch(err) { + console.log(`ws message error: ${err}`); } }); + + pollStatefulChange(ws, session_id); }); app.listen(3000); -- 2.47.2