diff --git a/play-node-go/src/components/GameUI/Board/Board.js b/play-node-go/src/components/GameUI/Board/Board.js
index 014316b..dade349 100644
--- a/play-node-go/src/components/GameUI/Board/Board.js
+++ b/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/play-node-go/src/components/GameUI/Point/Point.js b/play-node-go/src/components/GameUI/Point/Point.js
index b07359b..db8df00 100644
--- a/play-node-go/src/components/GameUI/Point/Point.js
+++ b/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/play-node-go/src/pages/Game/Game.js b/play-node-go/src/pages/Game/Game.js
index 6aeb29f..8ae4227 100644
--- a/play-node-go/src/pages/Game/Game.js
+++ b/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/server/services/Game.js b/server/services/Game.js
new file mode 100644
index 0000000..4b067b2
--- /dev/null
+++ b/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/server/services/gameServices.js b/server/services/gameServices.js
new file mode 100644
index 0000000..6fc775a
--- /dev/null
+++ b/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/server/socket.js b/server/socket.js
index b218a4d..46ea33e 100644
--- a/server/socket.js
+++ b/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/server/test/gameServices.spec.js b/server/test/gameServices.spec.js
new file mode 100644
index 0000000..38499e8
--- /dev/null
+++ b/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