* 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
--- /dev/null
+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
socket:
build: ./socket
image: cliftonpalmer/go-socket
socket:
build: ./socket
image: cliftonpalmer/go-socket
httpd:
image: httpd:2.4
volumes:
- ./htdocs:/usr/local/apache2/htdocs
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
/* state of pieces
0: empty
1: white
2: black
*/
/* 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 state = [];
for (var i = 0; i < boardSize; i++)
{
var state = [];
for (var i = 0; i < boardSize; i++)
{
const connect = function() {
return new Promise((resolve, reject) => {
const socketProtocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:')
const connect = function() {
return new Promise((resolve, reject) => {
const socketProtocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:')
+ const port = window.location.port;
const socketUrl = `${socketProtocol}//${window.location.hostname}:${port}/ws/`
// Define socket
socket = new WebSocket(socketUrl);
socket.onopen = (e) => {
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();
}
// Resolve the promise - we are connected
resolve();
}
case "board":
console.log("Setting board state");
parsed.data.forEach( function (move, index) {
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;
+ 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);
}
default:
console.log(msg);
}
try {
// push state change to backend
if(isOpen(socket)) {
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,
socket.send(JSON.stringify({
"type":"move",
"data": {
"session":session,
"x":lastX,
"y":lastY,
- "state":getStone(stone)
+ "state":state[lastX][lastY] > 0 ? 0 : playerStone
+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}`);
+};
+
-<canvas id = "game-canvas" width="600" height="600"></canvas>
+<canvas id="game-canvas" width="800" height="800"></canvas>
+<div id="controls">
+<input id="new" type="button" value="New Game" />
+<label for="stones">Choose a stone:</label>
+<select name="stones" id="stones">
+ <option>Empty</option>
+ <option>White</option>
+ <option>Black</option>
+</select>
+</div>
<script src="go.js"></script>
<script src="go.js"></script>
--- /dev/null
+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;
+ }
+ }
+}
};
const pool = mariadb.createPool(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();
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,
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)
);
`);
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]);
INSERT INTO go.state (session_id, x, y, state)
values (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
state = VALUES(state);
`, [session_id, pos_x, pos_y, state]);
} catch (err) {
console.log(err);
} finally {
} catch (err) {
console.log(err);
} finally {
+exports.getMaxUpdatedState = getMaxUpdatedState;
+exports.deleteSession = deleteSession;
+exports.initBoard = initBoard;
exports.addMove = addMove;
exports.getBoardState = getBoardState;
exports.addMove = addMove;
exports.getBoardState = getBoardState;
}
async function pollStatefulChange(ws, session_id) {
}
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) {
- res = await db.getBoardState(session_id);
+ rowCount = newRowCount;
ws.send(JSON.stringify({
"type": "board",
ws.send(JSON.stringify({
"type": "board",
+ "data": await db.getBoardState(session_id)
}));
}
await sleep(1000);
}));
}
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;
}
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}`);
+ // 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);