From c333cf27ccfa279f10a2e5f86f67c3a26b0e2875 Mon Sep 17 00:00:00 2001 From: Clifton Palmer Date: Wed, 15 Sep 2021 13:16:59 -0500 Subject: [PATCH] Added board state background (#1) Board state now gets polling updates from the websocket --- .gitignore | 1 + docker-compose.yml | 12 ++ htdocs/go.js | 350 ++++++++++++++++++++++++++++++++++----------- socket/Dockerfile | 9 ++ socket/db.js | 57 ++++++++ socket/server.js | 75 ++++++++++ 6 files changed, 423 insertions(+), 81 deletions(-) create mode 100644 .gitignore create mode 100644 socket/Dockerfile create mode 100644 socket/db.js create mode 100644 socket/server.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/docker-compose.yml b/docker-compose.yml index 6c7bfd6..2db2369 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,17 @@ version: '3' 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 + ports: + - 3000:3000 httpd: image: httpd:2.4 volumes: diff --git a/htdocs/go.js b/htdocs/go.js index 6c69223..d02e55d 100644 --- a/htdocs/go.js +++ b/htdocs/go.js @@ -1,7 +1,10 @@ var canvas = document.getElementById("game-canvas"); var context = canvas.getContext("2d"); +var backgroundColor = '#F5DEB3'; +var gridColor = '#8B4513'; var boardSize = 19; + var border = canvas.width / 10; var boardWidth = canvas.width - (border * 2); var boardHeight = canvas.height - (border * 2); @@ -11,107 +14,292 @@ 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 state = []; +for (var i = 0; i < boardSize; i++) +{ + state[i] = []; + for (var j = 0; j < boardSize; j++) + { + state[i][j] = 0; + } +} + +// @connect +// Connect to the websocket +var socket; +const connect = function() { + return new Promise((resolve, reject) => { + const socketProtocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:') + const port = 3000; + 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(); + } + + socket.onmessage = (msg) => { + // Any data from the server can be manipulated here. + let parsed = JSON.parse(msg.data); + switch (parsed.type) { + 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; + }); + drawGrid(); + break; + default: + console.log(msg); + } + } + + socket.onerror = (e) => { + // Return an error if any occurs + console.log(e); + resolve(); + // Try to connect again + connect(); + } + }); +} + +// @isOpen +// check if a websocket is open +const isOpen = function(ws) { + console.log(ws.readyState); + return ws.readyState === ws.OPEN +} -drawGrid(); +// finish grid function drawGrid() -{ - var backgroundColor = '#F5DEB3'; - var gridColor = '#8B4513'; - - context.fillStyle = backgroundColor; - context.fillRect(0, 0, canvas.width, canvas.height); - +{ + + /* draw board square */ + context.fillStyle = backgroundColor; + context.fillRect(0, 0, canvas.width, canvas.height); + + /* draw board grid */ context.fillStyle = gridColor; - - for (var i = 0; i < boardSize - 1; i++) - { - for (var j = 0; j < boardSize - 1; j++) - { - context.fillRect(i * cellWidth + border, j * cellHeight + border, cellWidth - 1, cellHeight - 1); - } - } - - var quater = Math.floor((boardSize - 1) / 4); - var markerSize = 8; - var markerMargin = (markerSize / 2) + 0.5; - - context.fillStyle = backgroundColor; - - if (!!(boardSize % 2)) - { - context.fillRect((((boardSize - 1) / 2) * cellWidth) + border - markerMargin, (((boardSize - 1) / 2) * cellWidth) + border - markerMargin, markerSize, markerSize); - } - - context.fillRect((quater * cellWidth) + border - markerMargin, (quater * cellWidth) + border - markerMargin, markerSize, markerSize); - context.fillRect((((boardSize - 1) - quater) * cellWidth) + border - markerMargin, (quater * cellWidth) + border - markerMargin, markerSize, markerSize); - - context.fillRect((((boardSize - 1) - quater) * cellWidth) + border - markerMargin, (((boardSize - 1) - quater) * cellWidth) + border - markerMargin, markerSize, markerSize); - context.fillRect((quater * cellWidth) + border - markerMargin, (((boardSize - 1) - quater) * cellWidth) + border - markerMargin, markerSize, markerSize); - - var size = canvas.width / 40; - var textSpacing = 10; - context.fillStyle = '#000000'; + for (var i = 0; i < boardSize - 1; i++) + { + for (var j = 0; j < boardSize - 1; j++) + { + context.fillRect( + i * cellWidth + border, + j * cellHeight + border, + cellWidth - 1, + cellHeight - 1 + ); + } + } + + /* draw quarter marks and center mark on grid */ + var quarter = Math.floor((boardSize - 1) / 4); + var markerSize = 8; + var markerMargin = (markerSize / 2) + 0.5; + + context.fillStyle = backgroundColor; + if (!!(boardSize % 2)) + { + context.fillRect( + (((boardSize - 1) / 2) * cellWidth) + border - markerMargin, + (((boardSize - 1) / 2) * cellWidth) + border - markerMargin, + markerSize, + markerSize + ); + } + context.fillRect( + (quarter * cellWidth) + border - markerMargin, + (quarter * cellWidth) + border - markerMargin, + markerSize, + markerSize + ); + context.fillRect( + (((boardSize - 1) - quarter) * cellWidth) + border - markerMargin, + (quarter * cellWidth) + border - markerMargin, + markerSize, + markerSize + ); + context.fillRect( + (((boardSize - 1) - quarter) * cellWidth) + border - markerMargin, + (((boardSize - 1) - quarter) * cellWidth) + border - markerMargin, + markerSize, + markerSize + ); + context.fillRect( + (quarter * cellWidth) + border - markerMargin, + (((boardSize - 1) - quarter) * cellWidth) + border - markerMargin, + markerSize, + markerSize + ); + + /* draw text labels for grid */ + var size = canvas.width / 40; + var textSpacing = 10; + context.fillStyle = '#000000'; context.font = size + "px Arial"; - - for (i = 0; i < boardSize; i++) - { - context.fillText((i + 1), textSpacing, ((canvas.height - border) - (i * cellHeight)) + (size / 3)); - context.fillText((i + 1), canvas.width - (size + textSpacing), ((canvas.height - border) - (i * cellHeight)) + (size / 3)); - - context.fillText((String.fromCharCode(97 + i)), (border + (i * cellHeight) + (size / 3)) - (size / 1.5), textSpacing + size); - context.fillText((String.fromCharCode(97 + i)), (border + (i * cellHeight) + (size / 3)) - (size / 1.5), canvas.height - (textSpacing * 2)); - } + + for (i = 0; i < boardSize; i++) + { + context.fillText( + (i + 1), textSpacing, + ((canvas.height - border) - (i * cellHeight)) + (size / 3) + ); + context.fillText( + (i + 1), canvas.width - (size + textSpacing), + ((canvas.height - border) - (i * cellHeight)) + (size / 3) + ); + + context.fillText( + (String.fromCharCode(97 + i)), + (border + (i * cellHeight) + (size / 3)) - (size / 1.5), + textSpacing + size + ); + context.fillText( + (String.fromCharCode(97 + i)), + (border + (i * cellHeight) + (size / 3)) - (size / 1.5), + canvas.height - (textSpacing * 2) + ); + } + + drawPieces(); +} + +function drawPieces() { + /* draw pieces */ + for (var i = 0; i < boardSize; i++) + { + for (var j = 0; j < boardSize; j++) + { + switch(state[i][j]) { + case 1: + placeStone( + (i * cellWidth) + border, + (j * cellWidth) + border, + 'rgba(255, 255, 255, 1)' + ); + break; + case 2: + placeStone( + (i * cellWidth) + border, + (j * cellWidth) + border, + 'rgba(0, 0, 0, 1)' + ); + break; + default: + } + } + } } canvas.addEventListener('mousedown', function(evt) { - if (lastX && lastY) { - placeStone((lastX * cellWidth) + border, (lastY * cellWidth) + border, 'rgba(0, 0, 0, 1)'); + 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) + } + })); + } else { + console.log("Websocket is closed"); + } + } catch(e) { + console.log(e); } }); canvas.addEventListener('mousemove', function(evt) { - var position = getGridPoint(evt); - - if ((position.x != lastX) || (position.y != lastY)) - { - drawGrid(); - - if (((position.x >=0) && (position.x < boardSize)) && ((position.y >=0) && (position.y < boardSize))) - { - placeStone((position.x * cellWidth) + border, (position.y * cellWidth) + border, 'rgba(0, 0, 0, 0.2)'); - } - } - - lastX = position.x; - lastY = position.y; + var position = getGridPoint(evt); + + if ((position.x != lastX) || (position.y != lastY)) + { + drawGrid(); + if ( + ((position.x >=0) && (position.x < boardSize)) && + ((position.y >=0) && (position.y < boardSize)) + ) { + placeStone( + (position.x * cellWidth) + border, + (position.y * cellWidth) + border, + 'rgba(0, 0, 0, 0.2)' + ); + } + } + lastX = position.x; + lastY = position.y; }); function placeStone(x, y, color) { - var radius = cellWidth / 2; - - context.beginPath(); - context.arc(x, y, radius, 0, 2 * Math.PI, false); - context.fillStyle = color; - context.fill(); - context.lineWidth = 5; + var radius = cellWidth / 2; + + context.beginPath(); + context.arc(x, y, radius, 0, 2 * Math.PI, false); + context.fillStyle = color; + context.fill(); + context.lineWidth = 5; } function getGridPoint(evt) { - var rect = canvas.getBoundingClientRect(); - - var x = Math.round((evt.clientX-border-rect.left)/(rect.right-2*border-rect.left)* boardWidth); - var y = Math.round((evt.clientY-border-rect.top)/(rect.bottom-2*border-rect.top)* boardHeight); - - var roundX = Math.round(x / cellWidth); - var roundY = Math.round(y / cellHeight); - - return { - x: roundX, - y: roundY - }; + var rect = canvas.getBoundingClientRect(); + + var x = Math.round( + (evt.clientX-border-rect.left) / (rect.right-2*border-rect.left) * boardWidth + ); + var y = Math.round( + (evt.clientY-border-rect.top) / (rect.bottom-2*border-rect.top) * boardHeight + ); + + var roundX = Math.round(x / cellWidth); + var roundY = Math.round(y / cellHeight); + + return { + x: roundX, + y: roundY + }; } + +// finish +connect(); diff --git a/socket/Dockerfile b/socket/Dockerfile new file mode 100644 index 0000000..052fda9 --- /dev/null +++ b/socket/Dockerfile @@ -0,0 +1,9 @@ +FROM node:16.9-alpine + +WORKDIR /usr/src/app + +RUN npm install mariadb express express-ws +COPY server.js db.js ./ + +EXPOSE 3000 +CMD [ "node", "server.js" ] diff --git a/socket/db.js b/socket/db.js new file mode 100644 index 0000000..89fbef0 --- /dev/null +++ b/socket/db.js @@ -0,0 +1,57 @@ +const mariadb = require('mariadb'); +const dsn = { + host: 'db', + user: 'socket', + password: 'socketpw', + connectionLimit: 10 +}; +const pool = mariadb.createPool(dsn); + +async function addMove(session_id, pos_x, pos_y, state) { + let conn; + try { + conn = await pool.getConnection(); + + var res = 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'), + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY(session_id, x, y) +); + `); + res = 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 { + if (conn) conn.end(); + } +} + +async function getBoardState(session_id) { + let conn; + try { + conn = await pool.getConnection(); + return await conn.query(` +SELECT x, y, state from go.state where session_id = ? + `, [session_id]); + } catch (err) { + console.log(err); + } finally { + if (conn) conn.end(); + } +} + +exports.pool = pool; +exports.addMove = addMove; +exports.getBoardState = getBoardState; diff --git a/socket/server.js b/socket/server.js new file mode 100644 index 0000000..7aeb30d --- /dev/null +++ b/socket/server.js @@ -0,0 +1,75 @@ +var express = require('express'); +var app = express(); +var expressWs = require('express-ws')(app); + +var db = require('./db'); + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, 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!"); + lastUpdated = updatedAt; + res = await db.getBoardState(session_id); + ws.send(JSON.stringify({ + "type": "board", + "data": res + })); + } + await sleep(1000); + } + } 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); + + ws.on('message', async function(msg) { + console.log(`ws message: ${msg}`); + + 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); + } + }); +}); + +app.listen(3000); -- 2.47.2