From 5981daf86aa3c70aacbfeb515b2f9285469c2a18 Mon Sep 17 00:00:00 2001 From: Sorrel Bri Date: Thu, 30 Jan 2020 21:03:27 -0800 Subject: [PATCH] stub game services to store active games --- .../src/components/GameUI/Board/Board.js | 1 + .../src/components/GameUI/Point/Point.js | 7 +- .../play-node-go/src/pages/Game/Game.js | 2 +- .../src/reducers/socket/reducer.socket.js | 11 + packages/play-node-go/server/services/Game.js | 404 ++++++++++++++++++ .../server/services/gameServices.js | 38 ++ packages/play-node-go/server/socket.js | 6 + .../server/test/gameServices.spec.js | 15 + 8 files changed, 481 insertions(+), 3 deletions(-) create mode 100644 packages/play-node-go/server/services/Game.js create mode 100644 packages/play-node-go/server/services/gameServices.js create mode 100644 packages/play-node-go/server/test/gameServices.spec.js diff --git a/packages/play-node-go/play-node-go/src/components/GameUI/Board/Board.js b/packages/play-node-go/play-node-go/src/components/GameUI/Board/Board.js index 014316b..dade349 100644 --- a/packages/play-node-go/play-node-go/src/components/GameUI/Board/Board.js +++ b/packages/play-node-go/play-node-go/src/components/GameUI/Board/Board.js @@ -17,6 +17,7 @@ const Board = (props) => { posX={posX} posY={posY} // point={board[posX][posY]} + dispatch={dispatch} {...props} /> ); i++; diff --git a/packages/play-node-go/play-node-go/src/components/GameUI/Point/Point.js b/packages/play-node-go/play-node-go/src/components/GameUI/Point/Point.js index b07359b..db8df00 100644 --- a/packages/play-node-go/play-node-go/src/components/GameUI/Point/Point.js +++ b/packages/play-node-go/play-node-go/src/components/GameUI/Point/Point.js @@ -2,7 +2,7 @@ import React from 'react'; import './Point.scss'; const Point = (props) => { - const { posX, posY, user, game, record } = props; + const { posX, posY, user, game, record, dispatch } = props; const xFlag = () => { if ( posX === 1 ) return `board__point--top` if ( posX === game.boardSize ) return `board__point--bottom` @@ -15,7 +15,10 @@ const Point = (props) => { } return ( -
+
dispatch({type: 'SOCKET', message: 'MAKE_MOVE', body: {user: {}, game: {}, room: {}, board: {}, move: {}}})} + >
); diff --git a/packages/play-node-go/play-node-go/src/pages/Game/Game.js b/packages/play-node-go/play-node-go/src/pages/Game/Game.js index 6aeb29f..8ae4227 100644 --- a/packages/play-node-go/play-node-go/src/pages/Game/Game.js +++ b/packages/play-node-go/play-node-go/src/pages/Game/Game.js @@ -60,7 +60,7 @@ const Game = (props) => {

Player Area

{ return connectGame(state, action); } + case 'MAKE_MOVE': { + return makeMove(state, action); + } + default: return state; } @@ -56,4 +60,11 @@ function connectGame (state, action) { const socket = updatedState.socket; socket.emit('connect_game', {user, game}); return {...updatedState}; +} + +function makeMove (state, action) { + const { user, game, room, board, move } = action.body; + const socket = state.socket; + socket.emit('make_move', {...action.body}); + return state; } \ No newline at end of file diff --git a/packages/play-node-go/server/services/Game.js b/packages/play-node-go/server/services/Game.js new file mode 100644 index 0000000..4b067b2 --- /dev/null +++ b/packages/play-node-go/server/services/Game.js @@ -0,0 +1,404 @@ +/*----- constants -----*/ +const STONES_DATA = { + '-1': 'white', + '0': 'none', + '1': 'black', + 'k': 'ko' +} + +const DOTS_DATA = { + '-1': 'white', + '0': 'none', + '1': 'black', + 'd': 'dame', +} + +// index corresponds to difference in player rank +const KOMI_REC = { + '9': [ + 5.5, 2.5, -0.5, -3.5, -6.5, -9.5, 12.5, 15.5, 18.5, 21.5 + ], + '13': [ + 5.5, 0.5, -5.5, 0.5, -5.5, 0.5, -5.5, 0.5, -5.5, 0.5 + ], + '19': [ + 7.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5 + ] +} + +const HANDI_REC = { + '9': [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ], + '13': [ + 0, 0, 0, 2, 2, 3, 3, 4, 4, 5 + ], + '19': [ + 0, 0, 2, 3, 4, 5, 6, 7, 8, 9 + ] +} + +class Game { + constructor(gameData, gameRecord) { + this.winner = gameData.winner || null, + this.turn = gameData.turn || 1, // turn logic depends on handicap stones + this.pass = gameData.pass || 0, // -1 represents state in which resignation has been submitted, not confirmed + this.komi = gameData.komi || 6.5, // komi depends on handicap stones + player rank + this.handicap = gameData.handicap || 0, + this.boardSize = gameData.boardSize || 19, + this.playerState = gameData.playerState || { + bCaptures: 0, + wCaptures: 0, + bScore: 0, + wScore: 0 + }, + this.gameRecord = gameRecord || [], + + this.groups = {}, + this.boardState = [] + } + + initGame = () => { + this.winner = null; + this.pass = null; + this.turn = this.handicap ? -1 : 1; + return this.initBoard(); + } + + initBoard = () => { + let i = 0; + while (i < this.boardSize * this.boardSize) { + let point = new Point( Math.floor(i / this.boardSize) + 1, i % this.boardSize + 1, this) + this.boardState.push(point); + i++; + } + this.initHandi(); + } + + initHandi = () => { + if (this.handicap < 2) return; + HANDI_PLACE[this.boardSize][this.handicap].forEach(pt => { + if (!pt) return; + let handi = findPointFromIdx(pt); + handi.stone = 1; + handi.joinGroup(); + }) + } + + getBoardState = () => { + return this.boardState.reduce((boardState, point) => { + boardState[point.pos[0]][point.pos[1]] = point.legal || point.stone + }, {}) + } + +} + +// index represents handicap placement for different board-sizes, eg handiPlace['9][1] = { (3, 3), (7, 7) } +// last array in each property also used for hoshi rendering +const HANDI_PLACE = { + '9' : [ + 0, 0, + [[ 7, 3 ], [ 3, 7 ] ], + [ [ 7, 7 ], [ 7, 3 ], [ 3, 7 ] ], + [ [ 3, 3 ], [ 7, 7 ], [ 3, 7 ], [ 7, 3 ] ] + ], + '13' : [ + 0, 0, + [ [ 4, 10 ], [ 10, 4 ] ], + [ [ 10, 10 ], [ 4, 10 ], [ 10, 4] ], + [ [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ], + [ [ 7, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ], + [ [ 7, 4 ], [ 4, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ], + [ [ 7, 7 ], [ 7, 4 ], [ 4, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ], + [ [ 10, 7 ], [ 7, 4 ], [ 7, 10 ], [ 4, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ], + [ [ 7, 7 ], [ 10, 7 ], [ 7, 4 ], [ 7, 10 ], [ 4, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ], + ], + '19' : [ + 0, 0, + [ [ 4, 16 ], [ 16, 4 ] ], + [ [ 16, 16 ], [ 4, 16 ], [ 16, 4] ], + [ [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ], + [ [ 10, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ], + [ [ 10, 4 ], [ 4, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ], + [ [ 10, 10 ], [ 10, 4 ], [ 4, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ], + [ [ 16, 10 ], [ 10, 4 ], [ 10, 16 ], [ 4, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ], + [ [ 10, 10 ], [ 16, 10 ], [ 10, 4 ], [ 10, 16 ], [ 4, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ], + ] +}; + +class Point { + constructor(x, y, Game) { + this.pos = [ x, y ] + this.stone = 0; // this is where move placement will go 0, 1, -1, also contains ko: 'k' + this.legal; + this.territory; + this.capturing = []; + this.groupMembers = [ this ]; + this.neighbors = { + top: {}, + btm: {}, + lft: {}, + rgt: {} + } + this.neighbors.top = x > 1 ? [ x - 1, y ] : null; + this.neighbors.btm = x < Game.boardSize ? [ x + 1, y ] : null; + this.neighbors.rgt = y < Game.boardSize ? [ x, y + 1 ] : null; + this.neighbors.lft = y > 1 ? [ x, y - 1 ] : null; + } + + checkNeighbors = () => { + let neighborsArr = []; + for (let neighbor in this.neighbors) { + let nbr = this.neighbors[neighbor]; + // neighbor exists it's point is stored as { rPos, cPos} + if ( nbr !== null ) { + neighborsArr.push(boardState.find(pt => pt.pos[0] === nbr[0] && pt.pos[1] === nbr[1])) + } + }; + // returns array of existing neighbors to calling function + return neighborsArr; + } + + getLiberties = () => { + let neighborsArr = this.checkNeighbors().filter(pt => pt.stone === 0); + return neighborsArr; + } + + joinGroup = () => { + this.groupMembers = this.groupMembers.filter(grp => grp.stone === this.stone); + this.groupMembers.push(this); + let frns = this.checkNeighbors().filter(nbr => nbr.stone === this.stone); + for (let frn of frns) { + this.groupMembers.push(frn); + } + this.groupMembers = Array.from(new Set(this.groupMembers)); + for (let grpMem in this.groupMembers) { + this.groupMembers = Array.from(new Set(this.groupMembers.concat(this.groupMembers[grpMem].groupMembers))); + } + for (let grpMem in this.groupMembers) { + this.groupMembers[grpMem].groupMembers = Array.from(new Set(this.groupMembers[grpMem].groupMembers.concat(this.groupMembers))); + } + } + + checkCapture = () => { + let opps = this.checkNeighbors().filter(nbr => nbr.stone === gameState.turn * -1 + && nbr.getLiberties().every(liberty => liberty === this)); + for (let opp of opps) { + if (opp.groupMembers.every(stone => stone.getLiberties().filter(liberty => liberty !== this).length === 0)) { + this.capturing = this.capturing.concat(opp.groupMembers); + }; + } + this.capturing = Array.from(new Set(this.capturing)); + return this.capturing; + } + + checkGroup = () => { // liberty is true when called by move false when called by check Capture + let frns = this.checkNeighbors().filter(nbr => nbr.stone === gameState.turn); + for (let frn in frns) { + if (frns[frn].groupMembers.find(stone => stone.getLiberties().find(liberty => liberty !== this))) return true; + continue; + } + } + + cycleTerritory = () => { + if (this.stone) { + this.groupMembers.forEach(pt => pt.territory = pt.territory * -1); + } else { + this.groupMembers.forEach(pt => { + switch (pt.territory) { + case 1: + pt.territory = -1; + break; + case -1: + pt.territory = 'd'; + break; + case 'd': + pt.territory = 1; + break; + } + }); + } + } +} + +function findPointFromIdx(arr) { + return pointFromIdx = boardState.find( point => point.pos[0] === arr[0] && point.pos[1] === arr[1] ); +} + +function clickBoard(evt) { + evt.stopPropagation(); + if (gameState.pass > 1 || gameState.winner) return editTerritory(evt); + // checks for placement and pushes to cell + let placement = [ parseInt(evt.target.closest('td').id.split('-')[0]), parseInt(evt.target.closest('td').id.split('-')[1]) ]; + let point = findPointFromIdx(placement); + //checks that this placement was marked as legal + if ( !checkLegal(point) ) return; + clearKo(); + clearPass(); + resolveCaptures(point); + point.stone = gameState.turn; + point.joinGroup(); + playSound(point); + clearCaptures(); + gameState.gameRecord.push(`${STONES_DATA[gameState.turn]}: ${point.pos}`) + gameState.turn*= -1; +} + +function clearKo() { + for (let point in boardState) { + point = boardState[point]; + point.stone = point.stone === 'k' ? 0 : point.stone; + } +} + +function clearPass() { + gameState.pass = 0; +} + +function resolveCaptures(point) { + if(!point.capturing.length) { + point.checkCapture(); + } + if(point.capturing.length) { + point.capturing.forEach(cap => { + gameState.playerState[gameState.turn > 0 ? 'bCaptures' : 'wCaptures']++; + cap.stone = checkKo(point) ? 'k' : 0; + cap.groupMembers = []; + }) + } +} + +function checkLegal(point) { + clearOverlay(); + // first step in logic: is point occupied, or in ko + if (point.stone) return false; + // if point is not empty check if liberties + if (point.getLiberties().length < 1) { + //if no liberties check if enemy group has liberties + if ( point.checkCapture().length ) return true; + //if neighboring point is not empty check if friendly group is alive + if (point.checkGroup()) return true; + return false; + } + return true; +} + +function clearOverlay() { + for (let point in boardState) { + point = boardState[point]; + point.legal = false; + } +} + +function checkKo(point) { // currently prevents snapback // capturing point has no liberties and is only capturing one stone and + if (!point.getLiberties().length && point.capturing.length === 1 && !point.checkNeighbors().some(stone => stone.stone === gameState.turn)) return true; +} + + +function clearCaptures() { + for (let point in boardState) { + point = boardState[point]; + point.capturing = []; + } +} + +function playerPass() { + // display confirmation message + clearKo(); + clearCaptures(); + gameState.gameRecord.push(`${STONES_DATA[gameState.turn]}: pass`) + gameState.pass++; + if (gameState.pass === 2) return endGame(); + gameState.turn*= -1; +} + + +/*----- endgame functions -----*/ + +function playerResign() { + // display confirmation message + gameState.pass = -1; +} + +function clickGameHud() { + if (gameState.pass > 1 && !gameState.winner) calculateWinner(); + if (gameState.pass < 0) confirmResign(); +} + +function confirmResign() { + gameState.gameRecord.push(`${STONES_DATA[gameState.turn]}: resign`); + gameState.winner = STONES_DATA[gameState.turn * -1]; + endGame(); +} + +function endGame() { + if (!gameState.winner) endGameSetTerritory() +} + +function calculateWinner() { + let whiteTerritory = boardState.reduce((acc, pt) => { + if (pt.territory === -1 && pt.stone !== -1) { + return acc = acc + (pt.stone === 0 ? 1 : 2); + } + return acc; + }, 0); + let blackTerritory = boardState.reduce((acc, pt) => { + if (pt.territory === 1 && pt.stone !== 1) { + return acc + (pt.stone === 0 ? 1 : 2); + } + return acc; + }, 0); + gameState.playerState.wScore = + gameState.playerState.wCaptures + + (gameState.komi < 0 ? gameState.komi * -1 : 0) + + whiteTerritory; + gameState.playerState.bScore = + gameState.playerState.bCaptures + + (gameState.komi > 0 ? gameState.komi : 0) + + blackTerritory; + gameState.winner = gameState.playerState.wScore > gameState.playerState.bScore ? -1 : 1; + gameState.gameRecord.push(`${STONES_DATA[gameState.winner]}: +${Math.abs(gameState.playerState.wScore - gameState.playerState.bScore)}`) +} + +function endGameSetTerritory() { + let emptyPoints = boardState.filter(pt => !pt.stone); + emptyPoints.forEach(pt => pt.joinGroup()); + emptyPointSetTerritory(emptyPoints); + groupsMarkDeadLive(); +} + +function groupsMarkDeadLive() { + boardState.filter(pt => (!pt.territory )) + .forEach(pt => { + if (pt.groupMembers.some(grpMem => { + return grpMem.checkNeighbors().some(nbr => nbr.territory === pt.stone && nbr.stone === 0) + })) { + pt.groupMembers.forEach(grpMem => grpMem.territory = pt.stone); + } + }); + boardState.filter(pt => (!pt.territory)).forEach(pt => { + pt.territory = pt.stone * -1; + }); +} + +function emptyPointSetTerritory(emptyPoints) { + emptyPoints.filter(pt => !pt.territory && pt.checkNeighbors().filter(nbr => nbr.stone !== 0)) + .forEach(pt => { + let b = pt.groupMembers.reduce((acc, grpMem) => { + let bNbr = grpMem.checkNeighbors().filter(nbr => nbr.stone === 1).length; + return acc + bNbr; + }, 0); + let w = pt.groupMembers.reduce((acc, grpMem) => { + let wNbr = grpMem.checkNeighbors().filter(nbr => nbr.stone === -1).length; + return acc + wNbr; + }, 0); + pt.groupMembers.forEach(grp => { + if (Math.abs(b - w) < 4 && b && w) grp.territory = 'd' + else grp.territory = b > w ? 1 : -1; + }) + }); +} + +module.exports = { + Game +} diff --git a/packages/play-node-go/server/services/gameServices.js b/packages/play-node-go/server/services/gameServices.js new file mode 100644 index 0000000..6fc775a --- /dev/null +++ b/packages/play-node-go/server/services/gameServices.js @@ -0,0 +1,38 @@ +const Game = require('./Game').Game; + +const gamesInProgress = { } + +const storeGame = (game) => { + gamesInProgress[game.id] = new Game(game); +} + +const initGame = (game) => { + gamesInProgress[game.id] = new Game(game) + return gamesInProgress[game.id].initGame(); +} + +const placeMove = (game, move) => { + if (!gamesInProgress[game]) { + gamesInProgress[game] = storeGame(game) + } + // gamesInProgress[] + let meta = {}; + // let newBoard = {...board}; + let board = []; + return {board, meta} +} + +const getBoard = (gameId) => { + return gamesInProgress[gameId].getBoardState(); +} + +const getAllGames = () => { + return gamesInProgress; +} + +module.exports = { + placeMove, + getAllGames, + getBoard, + initGame +} \ No newline at end of file diff --git a/packages/play-node-go/server/socket.js b/packages/play-node-go/server/socket.js index b218a4d..46ea33e 100644 --- a/packages/play-node-go/server/socket.js +++ b/packages/play-node-go/server/socket.js @@ -3,6 +3,7 @@ const socketIO = require('socket.io'); const io = socketIO({ cookie: false }); const gameQueries = require('./data/queries/game'); +const gameServices = require('./services/gameServices'); io.on('connection', socket=> { socket.emit('connected', {message: 'socket connected'}); @@ -22,6 +23,11 @@ io.on('connection', socket=> { io.of(room).to(game).emit('game_connected', {}) }); }); + socket.on('make_move', data => { + const { user, move, board, game, room } = data; + gameServices.placeMove(1, {player: 'black', move: '7,4'}) + console.log(data) + }) }); }) }) diff --git a/packages/play-node-go/server/test/gameServices.spec.js b/packages/play-node-go/server/test/gameServices.spec.js new file mode 100644 index 0000000..38499e8 --- /dev/null +++ b/packages/play-node-go/server/test/gameServices.spec.js @@ -0,0 +1,15 @@ +const gameServices = require('../services/gameServices'); + +describe('game services', () => { + it('games services persists game data', done => { + gameServices.placeMove({id: 1}, {player: 'black', move: '3,3'}) + console.log(gameServices.getAllGames()) + done(); + }); + + it('init game returns game board', done => { + gameServices.initGame({id: 1}) + console.log(gameServices.getBoard(1)) + done(); + }) +}) \ No newline at end of file