diff --git a/packages/server/services/Game.js b/packages/server/services/Game.js index c2f9834..db31308 100644 --- a/packages/server/services/Game.js +++ b/packages/server/services/Game.js @@ -1,12 +1,3 @@ -/*----- constants -----*/ -const STONES_DATA = { - '-1': 'white', - '0': 'none', - '1': 'black', - 'k': 'ko' -} - - // index corresponds to difference in player rank const KOMI_REC = { '9': [ @@ -35,388 +26,384 @@ const HANDI_REC = { // 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' : [ + 9 : [ 0, 0, - [[ 7, 3 ], [ 3, 7 ] ], - [ [ 7, 7 ], [ 7, 3 ], [ 3, 7 ] ], - [ [ 3, 3 ], [ 7, 7 ], [ 3, 7 ], [ 7, 3 ] ] + [ '7-3', '3-7' ], // 2 + [ '7-7', '7-3', '3-7' ], + [ '3-3', '7-7', '3-7', '7-3' ] ], - '13' : [ + 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] ], + [ '4-10', '10-4' ], // 2 + [ '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' : [ + 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] ], + [ '4-16', '16-4' ], // 2 + [ '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 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.groups = {}, - this.boardState = [], - this.gameRecord = gameRecord || [], - this.playerState = gameData.playerState || { +const getSingleItemFromSet = set => { + let entry; + for (entry of set.entries()) { + } + return entry[0]; +} + +const pipeMap = (...funcs) => obj => { + const arr = Object.entries(obj).reduce((acc, [key, value], i, arr) => { + funcs.forEach(func => value = func(value, i, arr)); + return [...acc, [key, value]]; + },[]); + return arr.reduce((acc, [key, value]) => { + return { ...acc, [key]: value } + }, {}); +} + +const checkLegal = ({ point, Game }) => { + // if stone (includes ko) return false + if (point.stone) { + point.legal = false; + return point; + } + const neighbors = getNeighbors({Game, point}); + + const isEmpty = point => point.stone === 0 && point.legal === true; + const isEmptyAdjacent = neighbors.filter(isEmpty); + + // if empty point adjacent return true + if (!isEmptyAdjacent.length) { + + // if group has liberties return true + const isTurnStone = neighbor => neighbor.stone === Game.turn; + const getGroupLiberties = point => Array.from(Game.groups[point.group].liberties); + const isNotSamePoint = liberty => liberty.pos.x !== point.pos.x && liberty.pos.y !== point.pos.y; + const isInGroupWithLiberties = neighbor => getGroupLiberties(neighbor).filter(isNotSamePoint).length; + const isInLiveGroup = neighbors.filter(isTurnStone).filter(isInGroupWithLiberties).length; + + if (isInLiveGroup) { + point.legal = true; + return point; + } + + // if move would capture opposing group return true + if (point.capturing[Game.turn].size) { + point.legal = true; + return point; + } + + point.legal = false; + return point; + } + point.legal = true; + return point; +} + +const getBoardState = (Game) => { + const getLegal = point => checkLegal({ point, Game }) + const boardState = pipeMap(getLegal)(Game.boardState); + Game.kos.forEach(ko => { + boardState[ko].legal = false; + }); + return boardState; +} + +const getLegalMoves = (Game) => { + const mapLegal = point => point.legal ? 'l' : point.stone; + const legalMoves = pipeMap(mapLegal)(Game.boardState); + Game.kos.forEach(ko => { + legalMoves[ko] = 'k'; + }); + return legalMoves; +} + +const getNeighbors = ({ Game, point }) => { + let { top = null, btm = null, lft = null, rgt = null} = point.neighbors; + const { boardState, boardSize } = Game; + // boardState[0] = [ '1-1', Point({x:1, y:1, boardSize}) ] + if (top) top = boardState[top]; + if (btm) btm = boardState[btm]; + if (lft) lft = boardState[lft]; + if (rgt) rgt = boardState[rgt]; + return [ top, btm, lft, rgt ].filter(value => value); +} + +const initBoard = (game) => { + const boardState = {}; + const { boardSize, handicap } = game; + for (let i = 0; i < Math.pow(boardSize, 2); i++) { + const point = Point({ + x: Math.floor(i / boardSize) + 1, + y: i % boardSize + 1, + boardSize + }); + boardState[`${point.pos.x}-${point.pos.y}`] = point; + } + + if (handicap) { + HANDI_PLACE[boardSize][handicap].forEach(pt => { + boardState[pt].makeMove({...game, boardState}); + }); + game.turn *= -1; + } + return boardState; +} + +// returns Game object +const Game = ({gameData = {}, gameRecord = []} = {}) => { + if (gameRecord.length) { + // play through all the moves + return gameRecord.reduce((game, move) => game.makeMove(move), Game({gameData}).initGame()) + } + return { + winner: gameData.winner ||null, + turn: gameData.turn || 0, // turn logic depends on handicap stones + pass: gameData.pass || 0, // -1 represents state in which resignation has been submitted, not confirmed + komi: gameData.komi || 6.5, // komi depends on handicap stones + player rank + handicap: gameData.handicap || 0, + boardSize: gameData.boardSize || 19, + groups: {}, + boardState: {}, + kos: [], + gameRecord: gameRecord, + playerState: gameData.playerState || { bCaptures: 0, wCaptures: 0, bScore: 0, wScore: 0 - } - } + }, - initGame = () => { - this.winner = null; - this.pass = null; - this.turn = this.handicap ? -1 : 1; + initGame: function() { + this.winner = null; + this.pass = 0; + this.turn = 1; + this.boardState = initBoard(this); + this.boardState = getBoardState(this); + this.legalMoves = getLegalMoves(this) + return this; + }, - this.initBoard(); - return this.getBoardState(); - } + addToRecord: function(moveObject) { + this.gameRecord.push(moveObject); + }, - 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 = this.findPointFromIdx(pt); - handi.stone = 1; - handi.joinGroup(this); - }) - } - - getBoardState = () => { - this.boardState.forEach(point => point.legal = checkLegal(point, this)) - return this.boardState.reduce((boardState, point) => { - boardState[`${point.pos[0]}-${point.pos[1]}`] = point.legal || point.stone; - return boardState; - }, {}) - } - - getMeta = () => { - return { winner: this.winner, turn: this.turn, pass: this.pass, playerState: this.playerState, gameRecord: this.gameRecord } - } - - findPointFromIdx = (arr) => { - return this.boardState.find( point => point.pos[0] === arr[0] && point.pos[1] === arr[1] ); - } - - makeMove = (move) => { - const player = move.player === 'white' ? -1 : 1; - const point = this.findPointFromIdx([move.pos.x, move.pos.y]) - if ( !checkLegal(point, this) ) throw Error('illegal move'); - clearKo(this); - clearPass(this); - resolveCaptures(point, this); - point.stone = this.turn; - point.joinGroup(this); - clearCaptures(this); - this.gameRecord.push(move) - this.turn*= -1; - return { board: this.getBoardState(), meta: this.getMeta()}; - } - - 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; - } - -} - - -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 = (Game) => { - 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(Game.boardState.find(pt => pt.pos[0] === nbr[0] && pt.pos[1] === nbr[1])) + getMeta: function() { + // cannot be chained + // does not affect game object + return { + winner: this.winner, + turn: this.turn, + pass: this.pass, + playerState: this.playerState, + gameRecord: this.gameRecord, + boardSize: this.boardSize, + handicap: this.handicap, + komi: this.komi } - }; - // returns array of existing neighbors to calling function - return neighborsArr; - } + }, + + clearKo: function() { + this.kos.forEach(ko => { + this.boardState[ko] = { ...this.boardState[ko], legal: true, ko: false }; + }) + this.kos = []; + }, - getLiberties = (Game) => { - let neighborsArr = this.checkNeighbors(Game).filter(pt => pt.stone === 0); - return neighborsArr; - } - - joinGroup = (Game) => { - this.groupMembers = this.groupMembers.filter(grp => grp.stone === this.stone); - this.groupMembers.push(this); - let frns = this.checkNeighbors(Game).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 = (Game) => { - let opps = this.checkNeighbors(Game).filter(nbr => nbr.stone === Game.turn * -1 - && nbr.getLiberties(Game).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; + makeMove: function({ player, pos: {x, y}}) { + let game = this; + let success = false; + const point = game.boardState[`${x}-${y}`]; + const isTurn = ( game.turn === 1 && player === 'black' ) + || ( game.turn === -1 && player === 'white' ); + if (isTurn) { + if (point.legal) { + game.addToRecord({ player, pos: { x, y } }); + if (this.kos.length) this.clearKo(); + point.makeMove(game); + game.turn *= -1; + success = true; } + } + game.boardState = getBoardState(game); + return {...game, legalMoves: getLegalMoves(game), success }; + }, + + initGroup: function(point) { + const group = Symbol(`${point.pos.x}-${point.pos.y}`); + this.groups[group] = { stones: new Set(), liberties: new Set()}; + return { game: this, group }; + }, + + returnToMove: function(lastMove) { + const { komi, handicap, boardSize } = this; + if (lastMove === 0) { + return Game({ + gameData: { komi, handicap, boardSize } + }).initGame(); + } + const length = this.gameRecord.length; + const index = lastMove < 0 ? length + lastMove : lastMove; + if (lastMove >= length && lastMove > 0) return this; + return Game({ + gameData: { komi, handicap, boardSize }, + gameRecord: [...this.gameRecord.slice(0, index)] }); } } -} +}; +const Point = ({x, y, boardSize = 19}) => { + let point = { + pos: {x, y}, + key: `${x}-${y}`, + stone: 0, // can be 1, -1, 0, + ko: false, + legal: true, + territory: 0, + capturing: { + '1': new Set(), + '-1': new Set() + }, + group: null, + neighbors: { + top: x > 1 ? `${ x - 1 }-${ y }` : null, + btm: x < boardSize ? `${ x + 1 }-${ y }` : null, + rgt: y < boardSize ? `${ x }-${ y + 1 }` : null, + lft: y > 1 ? `${ x }-${ y - 1 }` : null + }, -function clearKo(Game) { - for (let point in Game.boardState) { - point = Game.boardState[point]; - point.stone = point.stone === 'k' ? 0 : point.stone; - } -} - -function clearPass(Game) { - Game.pass = 0; -} - -function resolveCaptures(point, Game) { - if(!point.capturing.length) { - point.checkCapture(Game); - } - if(point.capturing.length) { - point.capturing.forEach(cap => { - Game.playerState[gameState.turn > 0 ? 'bCaptures' : 'wCaptures']++; - cap.stone = checkKo(point) ? 'k' : 0; - cap.groupMembers = []; - }) - } -} - -function checkLegal(point, Game) { - // clearOverlay(); - // first step in logic: is point occupied, or in ko - if (point.stone) return 0; - // if point is not empty check if liberties - if (point.getLiberties(Game).length < 1) { - //if no liberties check if enemy group has liberties - if ( point.checkCapture(Game).length ) return 'l'; - //if neighboring point is not empty check if friendly group is alive - if (point.checkGroup(Game)) return 'l'; - return 0; - } - return 'l'; -} - -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(Game) { - for (let point in Game.boardState) { - point = Game.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); + makeMove: function(Game) { + this.stone = Game.turn; + this.legal = false; + if (this.capturing[this.stone].size) { + Game = this.makeCaptures(Game); } - }); - boardState.filter(pt => (!pt.territory)).forEach(pt => { - pt.territory = pt.stone * -1; - }); -} + Game = this.joinGroup({ point: this, Game }); + return this.checkCaptures(Game); + }, + + joinGroup: function({ point, Game }) { + if (point.group !== this.group || !point.group) { + // if point has no group set current group to new Symbol in game object + if (!point.group) { + const { game, group } = Game.initGroup(point); + this.group = group; + Game = game; + } + + // add current point to global group and override current group + Game.groups[point.group].stones.add(this); + if (this.group !== point.group) { + this.group = point.group; + } + Game = this.setLiberties(Game); + getNeighbors({ point:this, Game }).forEach(neighbor => { + if ( neighbor.stone === this.stone + // this check prevents infinite call chains + && neighbor.group !== this.group + ) { + Game = neighbor.joinGroup({ point: this, Game }); + } + }) + } + return Game; + }, -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; - }) - }); -} + setLiberties: function(Game) { + const neighbors = getNeighbors({ point: this, Game }); + const liberties = Game.groups[this.group].liberties; + // if point is occupied remove it from liberties set of point group, else add it + neighbors.forEach(neighbor => { + if (neighbor.stone !== 0) { + liberties.delete(neighbor); + Game.groups[neighbor.group].liberties.delete(this); + } + if (neighbor.stone === 0) { + liberties.add(neighbor) + } + }); + return Game; + }, + + checkCaptures: function(game) { + // if this stone has one liberty + const liberties = game.groups[this.group].liberties; + if (liberties.size === 1) { + const lastLiberty = getSingleItemFromSet(liberties); + lastLiberty.capturing[this.stone * -1].add(this.group); + } + + // if neighbors have one liberty + const neighbors = getNeighbors({point: this, Game: game}).filter(neighbor => neighbor.stone === -1 * this.stone) + neighbors.forEach( neighbor => { + const liberties = game.groups[neighbor.group] && game.groups[neighbor.group].liberties; + if (liberties && liberties.size === 1) { + const lastLiberty = getSingleItemFromSet(liberties); + lastLiberty.capturing[neighbor.stone * -1].add(neighbor.group); + } + }); + return game; + }, + + makeCaptures: function(game) { + // for each group + for (let [captureGroup, _] of this.capturing[this.stone].entries()) { + + const capturesSet = game.groups[captureGroup].stones; + for (let [capture, _] of capturesSet.entries()) { + game = capture.removeStone(game); + if (capturesSet.size === 1) { + const neighbors = getNeighbors({ point: this, Game: game }) + const liberties = neighbors.filter(neighbor => neighbor.stone === 0); + const groupStones = neighbors.filter(neighbor => neighbor.stone === this.stone); + if (liberties.length === 1 && groupStones.length === 0) { + capture.ko = true; + game.kos.push(capture.key) + } + } + } + + } + // points with stones cannot be played to capture + this.capturing = { '1': new Set(), '-1': new Set() } + return {...game, boardState: { ...game.boardState, [this.key]: this } }; + }, + + removeStone: function(game) { + if (this.stone = 0) { + return game; + } + // reset point + this.stone = 0; + this.group = null; + this.capturing[game.turn] = new Set(); + // add captures + const player = game.turn > 0 ? 'b' : 'w'; + game.playerState[`${player}Captures`] += 1; + return {...game, boardState: {...game.boardState, [this.key]: this}}; + } + } + for (let [key, value] of Object.entries(point.neighbors)) { + if (value) continue; + delete point.neighbors[key]; + } + return point; +}; module.exports = { - Game -} + Game, + Point +} \ No newline at end of file diff --git a/packages/server/services/Game.v1.js b/packages/server/services/Game.v1.js new file mode 100644 index 0000000..c2f9834 --- /dev/null +++ b/packages/server/services/Game.v1.js @@ -0,0 +1,422 @@ +/*----- constants -----*/ +const STONES_DATA = { + '-1': 'white', + '0': 'none', + '1': 'black', + 'k': 'ko' +} + + +// 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 + ] +} + +// 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 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.groups = {}, + this.boardState = [], + this.gameRecord = gameRecord || [], + this.playerState = gameData.playerState || { + bCaptures: 0, + wCaptures: 0, + bScore: 0, + wScore: 0 + } + } + + initGame = () => { + this.winner = null; + this.pass = null; + this.turn = this.handicap ? -1 : 1; + + this.initBoard(); + return this.getBoardState(); + } + + 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 = this.findPointFromIdx(pt); + handi.stone = 1; + handi.joinGroup(this); + }) + } + + getBoardState = () => { + this.boardState.forEach(point => point.legal = checkLegal(point, this)) + return this.boardState.reduce((boardState, point) => { + boardState[`${point.pos[0]}-${point.pos[1]}`] = point.legal || point.stone; + return boardState; + }, {}) + } + + getMeta = () => { + return { winner: this.winner, turn: this.turn, pass: this.pass, playerState: this.playerState, gameRecord: this.gameRecord } + } + + findPointFromIdx = (arr) => { + return this.boardState.find( point => point.pos[0] === arr[0] && point.pos[1] === arr[1] ); + } + + makeMove = (move) => { + const player = move.player === 'white' ? -1 : 1; + const point = this.findPointFromIdx([move.pos.x, move.pos.y]) + if ( !checkLegal(point, this) ) throw Error('illegal move'); + clearKo(this); + clearPass(this); + resolveCaptures(point, this); + point.stone = this.turn; + point.joinGroup(this); + clearCaptures(this); + this.gameRecord.push(move) + this.turn*= -1; + return { board: this.getBoardState(), meta: this.getMeta()}; + } + + 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; + } + +} + + +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 = (Game) => { + 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(Game.boardState.find(pt => pt.pos[0] === nbr[0] && pt.pos[1] === nbr[1])) + } + }; + // returns array of existing neighbors to calling function + return neighborsArr; + } + + getLiberties = (Game) => { + let neighborsArr = this.checkNeighbors(Game).filter(pt => pt.stone === 0); + return neighborsArr; + } + + joinGroup = (Game) => { + this.groupMembers = this.groupMembers.filter(grp => grp.stone === this.stone); + this.groupMembers.push(this); + let frns = this.checkNeighbors(Game).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 = (Game) => { + let opps = this.checkNeighbors(Game).filter(nbr => nbr.stone === Game.turn * -1 + && nbr.getLiberties(Game).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 clearKo(Game) { + for (let point in Game.boardState) { + point = Game.boardState[point]; + point.stone = point.stone === 'k' ? 0 : point.stone; + } +} + +function clearPass(Game) { + Game.pass = 0; +} + +function resolveCaptures(point, Game) { + if(!point.capturing.length) { + point.checkCapture(Game); + } + if(point.capturing.length) { + point.capturing.forEach(cap => { + Game.playerState[gameState.turn > 0 ? 'bCaptures' : 'wCaptures']++; + cap.stone = checkKo(point) ? 'k' : 0; + cap.groupMembers = []; + }) + } +} + +function checkLegal(point, Game) { + // clearOverlay(); + // first step in logic: is point occupied, or in ko + if (point.stone) return 0; + // if point is not empty check if liberties + if (point.getLiberties(Game).length < 1) { + //if no liberties check if enemy group has liberties + if ( point.checkCapture(Game).length ) return 'l'; + //if neighboring point is not empty check if friendly group is alive + if (point.checkGroup(Game)) return 'l'; + return 0; + } + return 'l'; +} + +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(Game) { + for (let point in Game.boardState) { + point = Game.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/server/services/Game.v2.js b/packages/server/services/Game.v2.js deleted file mode 100644 index 88d1f9d..0000000 --- a/packages/server/services/Game.v2.js +++ /dev/null @@ -1,400 +0,0 @@ -// 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 - ] -} - -// 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' ], // 2 - [ '7-7', '7-3', '3-7' ], - [ '3-3', '7-7', '3-7', '7-3' ] - ], - 13 : [ - 0, 0, - [ '4-10', '10-4' ], // 2 - [ '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' ], // 2 - [ '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' ], - ] -}; - -const getSingleItemFromSet = set => { - let entry; - for (entry of set.entries()) { - } - return entry[0]; -} - -const pipeMap = (...funcs) => obj => { - const arr = Object.entries(obj).reduce((acc, [key, value], i, arr) => { - funcs.forEach(func => value = func(value, i, arr)); - return [...acc, [key, value]]; - },[]); - return arr.reduce((acc, [key, value]) => { - return { ...acc, [key]: value } - }, {}); -} - -const checkLegal = ({ point, Game }) => { - // if stone (includes ko) return false - if (point.stone) { - point.legal = false; - return point; - } - const neighbors = getNeighbors({Game, point}); - - const isEmpty = point => point.stone === 0 && point.legal === true; - const isEmptyAdjacent = neighbors.filter(isEmpty); - - // if empty point adjacent return true - if (!isEmptyAdjacent.length) { - - // if group has liberties return true - const isTurnStone = neighbor => neighbor.stone === Game.turn; - const getGroupLiberties = point => Array.from(Game.groups[point.group].liberties); - const isNotSamePoint = liberty => liberty.pos.x !== point.pos.x && liberty.pos.y !== point.pos.y; - const isInGroupWithLiberties = neighbor => getGroupLiberties(neighbor).filter(isNotSamePoint).length; - const isInLiveGroup = neighbors.filter(isTurnStone).filter(isInGroupWithLiberties).length; - - if (isInLiveGroup) { - point.legal = true; - return point; - } - - // if move would capture opposing group return true - if (point.capturing[Game.turn].size) { - point.legal = true; - return point; - } - - point.legal = false; - return point; - } - point.legal = true; - return point; -} - -const getBoardState = (Game) => { - const getLegal = point => checkLegal({ point, Game }) - const boardState = pipeMap(getLegal)(Game.boardState); - Game.kos.forEach(ko => { - boardState[ko].legal = false; - }); - return boardState; -} - -const getLegalMoves = (Game) => { - const mapLegal = point => point.legal ? 'l' : point.stone; - const legalMoves = pipeMap(mapLegal)(Game.boardState); - Game.kos.forEach(ko => { - legalMoves[ko] = 'k'; - }); - return legalMoves; -} - -const getNeighbors = ({ Game, point }) => { - let { top = null, btm = null, lft = null, rgt = null} = point.neighbors; - const { boardState, boardSize } = Game; - // boardState[0] = [ '1-1', Point({x:1, y:1, boardSize}) ] - if (top) top = boardState[top]; - if (btm) btm = boardState[btm]; - if (lft) lft = boardState[lft]; - if (rgt) rgt = boardState[rgt]; - return [ top, btm, lft, rgt ].filter(value => value); -} - -const initBoard = (game) => { - const boardState = {}; - const { boardSize, handicap } = game; - for (let i = 0; i < Math.pow(boardSize, 2); i++) { - const point = Point({ - x: Math.floor(i / boardSize) + 1, - y: i % boardSize + 1, - boardSize - }); - boardState[`${point.pos.x}-${point.pos.y}`] = point; - } - - if (handicap) { - HANDI_PLACE[boardSize][handicap].forEach(pt => { - boardState[pt].makeMove({...game, boardState}); - }); - game.turn *= -1; - } - return boardState; -} - -// returns Game object -const Game = ({gameData = {}, gameRecord = []} = {}) => { - if (gameRecord.length) { - // play through all the moves - return gameRecord.reduce((game, move) => game.makeMove(move), Game({gameData}).initGame()) - } - return { - winner: gameData.winner ||null, - turn: gameData.turn || 0, // turn logic depends on handicap stones - pass: gameData.pass || 0, // -1 represents state in which resignation has been submitted, not confirmed - komi: gameData.komi || 6.5, // komi depends on handicap stones + player rank - handicap: gameData.handicap || 0, - boardSize: gameData.boardSize || 19, - groups: {}, - boardState: {}, - kos: [], - gameRecord: gameRecord, - playerState: gameData.playerState || { - bCaptures: 0, - wCaptures: 0, - bScore: 0, - wScore: 0 - }, - - initGame: function() { - this.winner = null; - this.pass = 0; - this.turn = 1; - this.boardState = initBoard(this); - this.boardState = getBoardState(this); - this.legalMoves = getLegalMoves(this) - return this; - }, - - addToRecord: function(moveObject) { - this.gameRecord.push(moveObject); - }, - - getMeta: function() { - // cannot be chained - // does not affect game object - return { winner: this.winner, turn: this.turn, pass: this.pass, playerState: this.playerState, gameRecord: this.gameRecord } - }, - - clearKo: function() { - this.kos.forEach(ko => { - this.boardState[ko] = { ...this.boardState[ko], legal: true, ko: false }; - }) - this.kos = []; - }, - - makeMove: function({ player, pos: {x, y}}) { - let game = this; - let success = false; - const point = game.boardState[`${x}-${y}`]; - const isTurn = ( game.turn === 1 && player === 'black' ) - || ( game.turn === -1 && player === 'white' ); - if (isTurn) { - if (point.legal) { - game.addToRecord({ player, pos: { x, y } }); - if (this.kos.length) this.clearKo(); - point.makeMove(game); - game.turn *= -1; - success = true; - } - } - game.boardState = getBoardState(game); - return {...game, legalMoves: getLegalMoves(game), success }; - }, - - initGroup: function(point) { - const group = Symbol(`${point.pos.x}-${point.pos.y}`); - this.groups[group] = { stones: new Set(), liberties: new Set()}; - return { game: this, group }; - }, - - returnToMove: function(lastMove) { - const { komi, handicap, boardSize } = this; - if (lastMove === 0) { - return Game({ - gameData: { komi, handicap, boardSize } - }).initGame(); - } - const length = this.gameRecord.length; - const index = lastMove < 0 ? length + lastMove : lastMove; - if (lastMove >= length && lastMove > 0) return this; - return Game({ - gameData: { komi, handicap, boardSize }, - gameRecord: [...this.gameRecord.slice(0, index)] - }); - } - } -}; - -const Point = ({x, y, boardSize = 19}) => { - let point = { - pos: {x, y}, - key: `${x}-${y}`, - stone: 0, // can be 1, -1, 0, - ko: false, - legal: true, - territory: 0, - capturing: { - '1': new Set(), - '-1': new Set() - }, - group: null, - neighbors: { - top: x > 1 ? `${ x - 1 }-${ y }` : null, - btm: x < boardSize ? `${ x + 1 }-${ y }` : null, - rgt: y < boardSize ? `${ x }-${ y + 1 }` : null, - lft: y > 1 ? `${ x }-${ y - 1 }` : null - }, - - makeMove: function(Game) { - this.stone = Game.turn; - this.legal = false; - if (this.capturing[this.stone].size) { - Game = this.makeCaptures(Game); - } - Game = this.joinGroup({ point: this, Game }); - return this.checkCaptures(Game); - }, - - joinGroup: function({ point, Game }) { - if (point.group !== this.group || !point.group) { - // if point has no group set current group to new Symbol in game object - if (!point.group) { - const { game, group } = Game.initGroup(point); - this.group = group; - Game = game; - } - - // add current point to global group and override current group - Game.groups[point.group].stones.add(this); - if (this.group !== point.group) { - this.group = point.group; - } - Game = this.setLiberties(Game); - getNeighbors({ point:this, Game }).forEach(neighbor => { - if ( neighbor.stone === this.stone - // this check prevents infinite call chains - && neighbor.group !== this.group - ) { - Game = neighbor.joinGroup({ point: this, Game }); - } - }) - } - return Game; - }, - - setLiberties: function(Game) { - const neighbors = getNeighbors({ point: this, Game }); - const liberties = Game.groups[this.group].liberties; - // if point is occupied remove it from liberties set of point group, else add it - neighbors.forEach(neighbor => { - if (neighbor.stone !== 0) { - liberties.delete(neighbor); - Game.groups[neighbor.group].liberties.delete(this); - } - if (neighbor.stone === 0) { - liberties.add(neighbor) - } - }); - return Game; - }, - - checkCaptures: function(game) { - // if this stone has one liberty - const liberties = game.groups[this.group].liberties; - if (liberties.size === 1) { - const lastLiberty = getSingleItemFromSet(liberties); - lastLiberty.capturing[this.stone * -1].add(this.group); - } - - // if neighbors have one liberty - const neighbors = getNeighbors({point: this, Game: game}).filter(neighbor => neighbor.stone === -1 * this.stone) - neighbors.forEach( neighbor => { - const liberties = game.groups[neighbor.group] && game.groups[neighbor.group].liberties; - if (liberties && liberties.size === 1) { - const lastLiberty = getSingleItemFromSet(liberties); - lastLiberty.capturing[neighbor.stone * -1].add(neighbor.group); - } - }); - return game; - }, - - makeCaptures: function(game) { - // for each group - for (let [captureGroup, _] of this.capturing[this.stone].entries()) { - - const capturesSet = game.groups[captureGroup].stones; - for (let [capture, _] of capturesSet.entries()) { - game = capture.removeStone(game); - if (capturesSet.size === 1) { - const neighbors = getNeighbors({ point: this, Game: game }) - const liberties = neighbors.filter(neighbor => neighbor.stone === 0); - const groupStones = neighbors.filter(neighbor => neighbor.stone === this.stone); - if (liberties.length === 1 && groupStones.length === 0) { - capture.ko = true; - game.kos.push(capture.key) - } - } - } - - } - // points with stones cannot be played to capture - this.capturing = { '1': new Set(), '-1': new Set() } - return {...game, boardState: { ...game.boardState, [this.key]: this } }; - }, - - removeStone: function(game) { - if (this.stone = 0) { - return game; - } - // reset point - this.stone = 0; - this.group = null; - this.capturing[game.turn] = new Set(); - // add captures - const player = game.turn > 0 ? 'b' : 'w'; - game.playerState[`${player}Captures`] += 1; - return {...game, boardState: {...game.boardState, [this.key]: this}}; - } - } - for (let [key, value] of Object.entries(point.neighbors)) { - if (value) continue; - delete point.neighbors[key]; - } - return point; -}; - -module.exports = { - Game, - Point -} \ No newline at end of file diff --git a/packages/server/services/gameServices.js b/packages/server/services/gameServices.js index 7170e6a..10dca77 100644 --- a/packages/server/services/gameServices.js +++ b/packages/server/services/gameServices.js @@ -3,22 +3,27 @@ const Game = require('./Game').Game; const gamesInProgress = { } const storeGame = (game) => { - gamesInProgress[game.id] = new Game(game); + gamesInProgress[game.id] = Game(game); } -const initGame = (game) => { - gamesInProgress[game.id] = new Game(game) - return gamesInProgress[game.id].initGame(); +const initGame = ({id, gameRecord = [], ...gameData}) => { + gamesInProgress[id] = Game({ gameData, gameRecord }) + gamesInProgress[id].initGame(); + return getDataForUI(id) } -const makeMove = (game, move) => { - if (!gamesInProgress[game.id]) initGame(game); - const newState = gamesInProgress[game.id].makeMove(move); - return {...newState} +const makeMove = ({id, move}) => { + if (!gamesInProgress[id]) return { message: 'no game'}; + gamesInProgress[id] = gamesInProgress[id].makeMove(move) + if (gamesInProgress[id].success === false) return { message: 'illegal move' }; + return getDataForUI(id) } -const getBoard = (gameId) => { - return gamesInProgress[gameId].getBoardState(); +const getDataForUI = (id) => { + return { + board: gamesInProgress[id].legalMoves, + ...gamesInProgress[id].getMeta() + }; } const getAllGames = () => { @@ -28,6 +33,6 @@ const getAllGames = () => { module.exports = { makeMove, getAllGames, - getBoard, + getDataForUI, initGame -} \ No newline at end of file +} diff --git a/packages/server/test/Game.spec.js b/packages/server/test/Game.spec.js index dadf087..aa0e45e 100644 --- a/packages/server/test/Game.spec.js +++ b/packages/server/test/Game.spec.js @@ -1,9 +1,540 @@ const chai = require('chai'); const should = chai.should(); -const Game = require('../services/Game'); +const { Game, Point } = require('../services/Game'); describe('Game', () => { - it('init Game', done => { + it('smoke test Game()', done => { + (typeof Game()) + .should.eql('object'); + done(); + }); + + it('smoke test Point()', done => { + (typeof Point({x: 1, y: 1})) + .should.eql('object'); + done(); + }); + + it('smoke test initGame()', done => { + (typeof Game().initGame()) + .should.eql('object'); + done(); + }); + + it('Get meta returns proper data for games with no record', done => { + Game().getMeta() + .should.eql(initialMeta); + // Game().initGame().getMeta() + // .should.eql({ ...initialMeta, turn: 1 }); + done(); + }); +}); + +describe('Game().initGame() returns legalMoves', () => { + it('initGame() returns default 19x19', done => { + Game().initGame() + .legalMoves.should.eql(emptyBoard); + done(); + }); + + it('initGame() with 2 handicap returns legalMoves with stones', done => { + Game({gameData: { handicap: 2 }}).initGame() + .legalMoves.should.eql({...emptyBoard, '4-16': 1, '16-4': 1}); + done(); + }); + + it('handicap stone has proper liberties', done => { + const game = Game({gameData: { handicap: 2 }}).initGame(); + const group = game.boardState['4-16'].group + game.groups[group].liberties.size.should.eql(4); + done(); + }); + + it('initGame( 19x19 ) with all levels of handicap returns legalMoves with stones', done => { + Game({gameData: { boardSize: 19, handicap: 2 }}).initGame() + .legalMoves.should.eql({...emptyBoard, '4-16': 1, '16-4': 1 }); + Game({gameData: { boardSize: 19, handicap: 3 }}).initGame() + .legalMoves.should.eql({...emptyBoard, '16-16': 1, '4-16': 1, '16-4': 1 }); + Game({gameData: { boardSize: 19, handicap: 4 }}).initGame() + .legalMoves.should.eql({...emptyBoard, '4-4': 1, '16-16': 1, '4-16': 1, '16-4': 1 }); + Game({gameData: { boardSize: 19, handicap: 5 }}).initGame() + .legalMoves.should.eql({...emptyBoard, '10-10': 1, '4-4': 1, '16-16': 1, '4-16': 1, '16-4': 1 }); + Game({gameData: { boardSize: 19, handicap: 6 }}).initGame() + .legalMoves.should.eql({...emptyBoard, '10-4': 1, '4-10': 1, '4-4': 1, '16-16': 1, '4-16': 1, '16-4': 1 }); + Game({gameData: { boardSize: 19, handicap: 7 }}).initGame() + .legalMoves.should.eql({...emptyBoard, '10-10': 1, '10-4': 1, '4-10': 1, '4-4': 1, '16-16': 1, '4-16': 1, '16-4': 1 }); + Game({gameData: { boardSize: 19, handicap: 8 }}).initGame() + .legalMoves.should.eql({...emptyBoard, '16-10': 1, '10-4': 1, '10-16': 1, '4-10': 1, '4-4': 1, '16-16': 1, '4-16': 1, '16-4': 1 }); + Game({gameData: { boardSize: 19, handicap: 9 }}).initGame() + .legalMoves.should.eql({...emptyBoard, '10-10': 1, '16-10': 1, '10-4': 1, '10-16': 1, '4-10': 1, '4-4': 1, '16-16': 1, '4-16': 1, '16-4': 1 }); + done(); + }) + + it('initGame( 13x13) returns legalMoves', done => { + Game({gameData: { boardSize: 13 }}).initGame() + .legalMoves.should.eql(emptyBoard13); + done(); + }); + + it('initGame( 13x13 ) with all levels of handicap returns legalMoves with stones', done => { + Game({gameData: { boardSize: 13, handicap: 2 }}).initGame() + .legalMoves.should.eql({...emptyBoard13, '4-10': 1, '10-4': 1 }); + Game({gameData: { boardSize: 13, handicap: 3 }}).initGame() + .legalMoves.should.eql({...emptyBoard13, '10-10': 1, '4-10': 1, '10-4': 1 }); + Game({gameData: { boardSize: 13, handicap: 4 }}).initGame() + .legalMoves.should.eql({...emptyBoard13, '4-4': 1, '10-10': 1, '4-10': 1, '10-4': 1 }); + Game({gameData: { boardSize: 13, handicap: 5 }}).initGame() + .legalMoves.should.eql({...emptyBoard13, '7-7': 1, '4-4': 1, '10-10': 1, '4-10': 1, '10-4': 1 }); + Game({gameData: { boardSize: 13, handicap: 6 }}).initGame() + .legalMoves.should.eql({...emptyBoard13, '7-4': 1, '4-7': 1, '4-4': 1, '10-10': 1, '4-10': 1, '10-4': 1 }); + Game({gameData: { boardSize: 13, handicap: 7 }}).initGame() + .legalMoves.should.eql({...emptyBoard13, '7-7': 1, '7-4': 1, '4-7': 1, '4-4': 1, '10-10': 1, '4-10': 1, '10-4': 1 }); + Game({gameData: { boardSize: 13, handicap: 8 }}).initGame() + .legalMoves.should.eql({...emptyBoard13, '10-7': 1, '7-4': 1, '7-10': 1, '4-7': 1, '4-4': 1, '10-10': 1, '4-10': 1, '10-4': 1 }); + Game({gameData: { boardSize: 13, handicap: 9 }}).initGame() + .legalMoves.should.eql({...emptyBoard13, '7-7': 1, '10-7': 1, '7-4': 1, '7-10': 1, '4-7': 1, '4-4': 1, '10-10': 1, '4-10': 1, '10-4': 1 }); + done(); + }); + + it('initGame( 9x9 ) returns legalMoves', done => { + Game({gameData: { boardSize: 9 }}).initGame() + .legalMoves.should.eql(emptyBoard9); + done(); + }); + + it('initGame( 9x9 ) with all levels of handicap returns legalMoves with stones', done => { + Game({gameData: { boardSize: 9, handicap: 2 }}).initGame() + .legalMoves.should.eql({...emptyBoard9, '3-7': 1, '7-3': 1 }); + Game({gameData: { boardSize: 9, handicap: 3 }}).initGame() + .legalMoves.should.eql({...emptyBoard9, '7-7': 1, '3-7': 1, '7-3': 1 }); + Game({gameData: { boardSize: 9, handicap: 4 }}).initGame() + .legalMoves.should.eql({...emptyBoard9, '3-3': 1, '7-7': 1, '3-7': 1, '7-3': 1 }); + done(); + }); +}); + +describe('Game.makeMove({ player: str, pos: { x: int, y: int } })', () => { + it('makeMove returns game object with proper board', done => { + Game().initGame().makeMove({ player: 'black', pos: { x: 4, y: 4 } }) + .legalMoves.should.eql({ ...emptyBoard, '4-4': 1 }); + Game({ gameData: { handicap: 2 } }).initGame().makeMove({ player: 'white', pos: { x: 4, y: 4 } }) + .legalMoves.should.eql({ ...emptyBoard, '4-16': 1, '16-4': 1, '4-4': -1 }); + done(); + }); + + it('makeMove returns success: false with move out of turn', done => { + Game().initGame().makeMove({ player: 'white', pos: { x: 4, y: 4 } }) + .success.should.eql(false); + Game({ gameData: { handicap: 2 } }).initGame().makeMove({ player: 'black', pos: { x: 4, y: 4 } }) + .success.should.eql(false); + done(); + }); + + it('makeMove returns success: false when move is at occupied point', done => { + Game({ gameData: { handicap: 2 } }).initGame().makeMove({ player: 'white', pos: { x: 4, y: 16 } }) + .success.should.eql(false); + done(); + }); + + it('makeMove next to adjacent stone of the same color joins stones as a group', done => { + const game = Game({ gameData: { handicap: 2 } }).initGame() // 4 3 4 + .makeMove({ player: 'white', pos: { x: 4, y: 4 } }) // 14 1 4 -1 -1 + .makeMove({ player: 'black', pos: { x: 4, y: 15 }}) // 15 1 5 -1 + .makeMove({ player: 'white', pos: { x: 3, y: 4 } }) // 16 1h + .makeMove({ player: 'black', pos: { x: 4, y: 14 }}) + .makeMove({ player: 'white', pos: { x: 4, y: 5 }}) + + const blackGroupKey = game.boardState['4-14'].group; + const blackGroup = game.groups[blackGroupKey].stones; + blackGroup.has(game.boardState['4-14']).should.eql(true); + blackGroup.has(game.boardState['4-15']).should.eql(true); + blackGroup.has(game.boardState['4-16']).should.eql(true); + const whiteGroupKey = game.boardState['4-4'].group; + const whiteGroup = game.groups[whiteGroupKey].stones; + whiteGroup.has(game.boardState['4-4']).should.eql(true); + whiteGroup.has(game.boardState['3-4']).should.eql(true); + whiteGroup.has(game.boardState['4-5']).should.eql(true); + done(); + }); + + const noGroupGame = Game({ gameData: { handicap: 2 } }).initGame() // 3 4 + .makeMove({ player: 'white', pos: { x: 4, y: 15 } }) // 14 1 + .makeMove({ player: 'black', pos: { x: 4, y: 14 }}) // 15 1 -1 no groups + .makeMove({ player: 'white', pos: { x: 3, y: 16 } }) // 16 -1 1h + .makeMove({ player: 'black', pos: { x: 3, y: 15 }}); + + it('makeMove next to adjacent stone of different color does not join stones as a group', done => { + const hoshiGroupKey = noGroupGame.boardState['4-16'].group; + const hoshiGroup = noGroupGame.groups[hoshiGroupKey].stones; + hoshiGroup.has(noGroupGame.boardState['4-16']).should.eql(true); + hoshiGroup.has(noGroupGame.boardState['4-15']).should.eql(false); + hoshiGroup.has(noGroupGame.boardState['3-14']).should.eql(false); + hoshiGroup.has(noGroupGame.boardState['3-15']).should.eql(false); + done(); + }) + + it('makeMove next to adjacent stone of different color should yield proper liberties', done => { + const hoshiGroup = noGroupGame.boardState['4-16'].group; + const hoshiGroupLiberties = noGroupGame.groups[hoshiGroup].liberties; + hoshiGroupLiberties.size.should.eql(2); + const fourFifteen = noGroupGame.boardState['4-15'].group; + const fourFifteenLiberties = noGroupGame.groups[fourFifteen].liberties; + fourFifteenLiberties.size.should.eql(1); + done(); + }) + + it('makeMove returns success: false when move is made in point with no liberties', done => { + const point = Game({ gameData: { handicap: 2 } }).initGame() // 15 16 17 + .makeMove({ player: 'white', pos: { x: 4, y: 4 } }).makeMove({ player: 'black', pos: { x: 6, y: 16 } }) // 4 1 + .makeMove({ player: 'white', pos: { x: 16, y: 16 }}).makeMove({ player: 'black', pos: { x: 5, y: 15 } }) // 5 1 x 1 + .makeMove({ player: 'white', pos: { x: 16, y: 10 }}).makeMove({ player: 'black', pos: { x: 5, y: 17 } }) // 6 1 + .makeMove({ player: 'white', pos: { x: 5, y: 16 }}) + point.success.should.eql(false); + done(); + }); +}); + +describe('makeMove group join and basic capture logic', () => { + const joinGame = Game().initGame() + .makeMove({ player: 'black', pos: { x: 4, y: 17 } }) // 3 4 5 + .makeMove({ player: 'white', pos: { x: 3, y: 16 } }) // 15 -1 + .makeMove({ player: 'black', pos: { x: 5, y: 16 } }) // 16 -1 1 1 + .makeMove({ player: 'white', pos: { x: 4, y: 15 } }) // 17 1 + .makeMove({ player: 'black', pos: { x: 4, y: 16 } }); + + it('gain liberties from group smoke test', done => { + joinGame.success.should.eql(true); + done(); + }); + + it('stones in group have same group property', done => { + joinGame.boardState['4-16'].group.should.eql(joinGame.boardState['5-16'].group); + joinGame.boardState['4-16'].group.should.eql(joinGame.boardState['4-17'].group); + joinGame.boardState['4-17'].group.should.eql(joinGame.boardState['4-16'].group); + joinGame.boardState['4-17'].group.should.eql(joinGame.boardState['5-16'].group); + joinGame.boardState['5-16'].group.should.eql(joinGame.boardState['4-17'].group); + joinGame.boardState['5-16'].group.should.eql(joinGame.boardState['4-16'].group); + done(); + }) + + it('stones in group should have proper liberties', done => { + const group = joinGame.boardState['4-16'].group; + joinGame.groups[group] + .liberties.size.should.eql(5); + done(); + }) + + it('group with only remaining liberty at point to be played returns success: false', done => { + Game({ gameData: { handicap: 2 } }).initGame() + .makeMove({ player: 'white', pos: { x: 4, y: 15 } }) // 3 4 5 6 + .makeMove({ player: 'black', pos: { x: 4, y: 4 } }) // 15 -1 -1 + .makeMove({ player: 'white', pos: { x: 5, y: 15 } }) // 16 -1 1h 0 -1 + .makeMove({ player: 'black', pos: { x: 16, y: 16 } }) // 17 -1 -1 + .makeMove({ player: 'white', pos: { x: 3, y: 16 } }) + .makeMove({ player: 'black', pos: { x: 4, y: 10 } }) + .makeMove({ player: 'white', pos: { x: 6, y: 16 } }) + .makeMove({ player: 'black', pos: { x: 10, y: 4 } }) + .makeMove({ player: 'white', pos: { x: 4, y: 17 } }) + .makeMove({ player: 'black', pos: { x: 10, y: 16 } }) + .makeMove({ player: 'white', pos: { x: 5, y: 17 } }) + .makeMove({ player: 'black', pos: { x: 5, y: 16 } }) + .success.should.eql(false); + done(); + }) + + const captureGame = () => Game({ gameData: { handicap: 2 } }).initGame() + .makeMove({ player: 'white', pos: { x: 4, y: 15 } }) // 3 4 5 + .makeMove({ player: 'black', pos: { x: 4, y: 4 } }) // 15 -1 + .makeMove({ player: 'white', pos: { x: 3, y: 16 } }) // 16 -1 0 -1 + .makeMove({ player: 'black', pos: { x: 4, y: 10 } }) // 17 -1 + .makeMove({ player: 'white', pos: { x: 5, y: 16 } }) // 4,16 captured + .makeMove({ player: 'black', pos: { x: 10, y: 4 } }) + + it('makeMove capture smoke test', done => { + captureGame().makeMove({ player: 'white', pos: { x: 4, y: 17 } }) + .success.should.eql(true); + done(); + }); + + it('makeMove assesses captures', done => { + captureGame().boardState['4-17'].capturing[-1].size.should.eql(1); + done(); + }) + + it('makeMove capture removes captured stone', done => { + captureGame().makeMove({ player: 'white', pos: { x: 4, y: 17 } }) + .boardState['4-16'].stone.should.eql(0); + done(); + }); + + it('makeMove capture increases capturing players captures', done => { + captureGame().makeMove({ player: 'white', pos: { x: 4, y: 17 } }) + .playerState.wCaptures.should.eql(1); + done(); + }); + + const multiCaptureGame = () => Game().initGame() + .makeMove({ player: 'black', pos: { x: 4, y: 17 } }) + .makeMove({ player: 'white', pos: { x: 3, y: 16 } }) + .makeMove({ player: 'black', pos: { x: 5, y: 16 } }) + .makeMove({ player: 'white', pos: { x: 4, y: 15 } }) + .makeMove({ player: 'black', pos: { x: 4, y: 16 } }) + .makeMove({ player: 'black', pos: { x: 4, y: 10 } }) // 3 4 5 6 + .makeMove({ player: 'white', pos: { x: 3, y: 17 } }) // 15 -1 -1 + .makeMove({ player: 'black', pos: { x: 10, y: 4 } }) // 16 -1 1 1 -1 + .makeMove({ player: 'white', pos: { x: 5, y: 15 } }) // 17 -1 1 -1 + .makeMove({ player: 'black', pos: { x: 10, y: 8 } }) // 18 -1 + .makeMove({ player: 'white', pos: { x: 4, y: 18} }) + .makeMove({ player: 'black', pos: { x: 3, y: 6 } }) + .makeMove({ player: 'white', pos: { x: 5, y: 17} }) + .makeMove({ player: 'black', pos: { x: 6, y: 3 } }); + + it('smoke test multi stone group capture', done => { + multiCaptureGame().makeMove({ player: 'white', pos: { x: 6, y: 16} }) + .success.should.eql(true); + done(); + }); + + it('multi stone group full group is in capturing', done => { + const game = multiCaptureGame() + const group = game.boardState['4-16'].group; + game.boardState['6-16'].capturing[-1].has(group).should.eql(true); + done(); + }); + + it('multi stone group capture all points are 0', done => { + const game = multiCaptureGame(); + game.makeMove({ player: 'white', pos: { x: 6, y: 16} }); + game.boardState['5-16'].stone.should.eql(0) + game.boardState['4-16'].stone.should.eql(0) + game.boardState['4-17'].stone.should.eql(0) + done(); + }); + + it('multi stone group capture scores points properly', done => { + const game = multiCaptureGame(); + game.makeMove({ player: 'white', pos: { x: 6, y: 16} }); + game.playerState.wCaptures.should.eql(3); + done(); + }) +}); + +describe('capture logic: snapback, ko and playing in eyes', () => { + it('playing in an eye formed by capture yields success: true', done => { + Game().initGame() + .makeMove({ player: 'black', pos: { x: 4, y: 4 } }) // 3 4 5 + .makeMove({ player: 'white', pos: { x: 5, y: 4 } }) // 4 1 + .makeMove({ player: 'black', pos: { x: 5, y: 5 } }) // 5 1 -1 1 + .makeMove({ player: 'white', pos: { x: 16, y: 16 } }) // 6 1 + .makeMove({ player: 'black', pos: { x: 5, y: 3 } }) // (9) at {5, 4} + .makeMove({ player: 'white', pos: { x: 16, y: 4 } }) + .makeMove({ player: 'black', pos: { x: 6, y: 4 } }) + .makeMove({ player: 'white', pos: { x: 4, y: 16 } }) + .makeMove({ player: 'black', pos: { x: 5, y: 4 } }) + .success.should.eql(true); + done(); + }); + + const snapbackGame = () => Game().initGame() + .makeMove({ player: 'black', pos: { x: 4, y: 4 } }) // 3 4 5 6 7 + .makeMove({ player: 'white', pos: { x: 5, y: 4 } }) // 4 1 1 -1 + .makeMove({ player: 'black', pos: { x: 5, y: 6 } }) // 5 1 -1 -1 1 -1 + .makeMove({ player: 'white', pos: { x: 5, y: 7 } }) // 6 1 1 -1 + .makeMove({ player: 'black', pos: { x: 4, y: 5 } }) // (13) at {5,6} + .makeMove({ player: 'white', pos: { x: 4, y: 6 } }) + .makeMove({ player: 'black', pos: { x: 5, y: 3 } }) + .makeMove({ player: 'white', pos: { x: 6, y: 6 } }) + .makeMove({ player: 'black', pos: { x: 6, y: 5 } }) + .makeMove({ player: 'white', pos: { x: 16, y: 16 } }) + .makeMove({ player: 'black', pos: { x: 6, y: 4 } }) + .makeMove({ player: 'white', pos: { x: 5, y: 5 } }) + .makeMove({ player: 'black', pos: { x: 5, y: 6 } }); + + it('snapback functions properly', done => { + snapbackGame() + .success.should.eql(true); + done(); + }); + + const koGame = () => Game().initGame() + .makeMove({ player: 'black', pos: { x: 4, y: 4 } }) // 3 4 5 6 + .makeMove({ player: 'white', pos: { x: 4, y: 5 } }) // 4 1 -1 + .makeMove({ player: 'black', pos: { x: 5, y: 3 } }) // 5 1 -1 1 -1 + .makeMove({ player: 'white', pos: { x: 5, y: 6 } }) // 6 1 -1 + .makeMove({ player: 'black', pos: { x: 6, y: 4 } }) + .makeMove({ player: 'white', pos: { x: 6, y: 5 } }) + .makeMove({ player: 'black', pos: { x: 5, y: 5 } }) + .makeMove({ player: 'white', pos: { x: 5, y: 4 } }) + + it('ko recognized properly on Point', done => { + koGame() + .boardState['5-5'].ko.should.eql(true); + done(); + }) + + it('ko marked on Game object', done => { + koGame().kos.should.eql(['5-5']); + done(); + }); + + it('ko marked in legalMoves', done => { + koGame().legalMoves['5-5'].should.eql('k'); + done(); + }) + + it('ko cleared on Point after move', done => { + koGame().makeMove({ player: 'black', pos: { x: 16, y: 16 } }) + .makeMove({ player: 'white', pos: { x: 4, y: 16 } }) + .boardState['5-5'].ko.should.eql(false); + done(); + }); + + it('ko cleared on Game after move', done => { + koGame().makeMove({ player: 'black', pos: { x: 16, y: 16 } }) + .makeMove({ player: 'white', pos: { x: 4, y: 16 } }) + .kos.should.eql([]) + done(); + }); + + it('ko cleared on legalMoves after move', done => { + koGame().makeMove({ player: 'black', pos: { x: 16, y: 16 } }) + .makeMove({ player: 'white', pos: { x: 4, y: 16 } }) + .legalMoves['5-5'].should.eql('l'); + done(); + }); +}); + +describe('Game history functionality', () => { + const firstMove = { player: 'black', pos: { x: 4, y: 4 }}; + const secondMove = { player: 'white', pos: { x: 16, y: 16 }}; + const thirdMove = { player: 'black', pos: { x: 16, y: 4 } }; + const fourthMove = { player: 'white', pos: { x: 4, y: 16 }}; + const fifthMove = { player: 'black', pos: { x: 10, y: 4 } }; + const sixthMove = { player: 'white', pos: { x: 4, y: 10 }}; + const seventhMove = { player: 'black', pos: { x: 10, y: 16 } }; + const eighthMove = { player: 'white', pos: { x: 16, y: 10 }}; + + it('makeMove creates gameRecord item', done => { + Game().initGame() + .makeMove(firstMove).gameRecord[0].should.eql(firstMove); + done(); + }); + + it('makeMove holds history', done => { + const game = Game().initGame() + .makeMove(firstMove).makeMove(secondMove); + game.gameRecord[0].should.eql(firstMove); + game.gameRecord[1].should.eql(secondMove) + done(); + }); + + const rewoundGame = () => Game().initGame() + .makeMove(firstMove) + .makeMove(secondMove) + .makeMove(thirdMove) + .returnToMove(-1); + + it('Game.returnToMove returns new Game with gameRecord', done => { + rewoundGame() + .gameRecord.should.eql([ firstMove, secondMove ]) + done(); + }); + + it('Game.returnToMove returns new Game with new board state', done => { + rewoundGame() + .boardState['16-4'].stone.should.eql(0); + rewoundGame() + .boardState['4-4'].stone.should.eql(1); + rewoundGame() + .boardState['16-16'].stone.should.eql(-1); + done(); + }); + + const resetGame = () => [ + firstMove, secondMove, thirdMove, fourthMove, fifthMove, sixthMove, seventhMove, eighthMove + ].reduce((game, move) => game.makeMove(move), Game().initGame()); + + it('Game.returnToMove(0) returns to init board state', done => { + const erasedGame = resetGame() + .returnToMove(0) + erasedGame.gameRecord.should.eql([]) + erasedGame.boardState['4-4'].stone.should.eql(0) + done(); + }); + + it('Game.returnToMove(5) returns to state after 5th move', done => { + const fifthMoveGame = resetGame() + .returnToMove(5); + fifthMoveGame.gameRecord.should.eql([firstMove, secondMove, thirdMove, fourthMove, fifthMove]); + fifthMoveGame.boardState['10-4'].stone.should.eql(1) + fifthMoveGame.boardState['4-10'].stone.should.eql(0) done(); }) }) + + +const initialMeta = { + winner: null, + turn: 0, + pass: 0, + komi: 6.5, + handicap: 0, + boardSize: 19, + playerState: { + bCaptures: 0, + wCaptures: 0, + bScore: 0, + wScore: 0 + }, + gameRecord: [] +} + +const emptyBoard9 = { + '1-1': 'l','1-2': 'l','1-3': 'l','1-4': 'l','1-5': 'l','1-6': 'l','1-7': 'l','1-8': 'l','1-9': 'l', + '2-1': 'l','2-2': 'l','2-3': 'l','2-4': 'l','2-5': 'l','2-6': 'l','2-7': 'l','2-8': 'l','2-9': 'l', + '3-1': 'l','3-2': 'l','3-3': 'l','3-4': 'l','3-5': 'l','3-6': 'l','3-7': 'l','3-8': 'l','3-9': 'l', + '4-1': 'l','4-2': 'l','4-3': 'l','4-4': 'l','4-5': 'l','4-6': 'l','4-7': 'l','4-8': 'l','4-9': 'l', + '5-1': 'l','5-2': 'l','5-3': 'l','5-4': 'l','5-5': 'l','5-6': 'l','5-7': 'l','5-8': 'l','5-9': 'l', + '6-1': 'l','6-2': 'l','6-3': 'l','6-4': 'l','6-5': 'l','6-6': 'l','6-7': 'l','6-8': 'l','6-9': 'l', + '7-1': 'l','7-2': 'l','7-3': 'l','7-4': 'l','7-5': 'l','7-6': 'l','7-7': 'l','7-8': 'l','7-9': 'l', + '8-1': 'l','8-2': 'l','8-3': 'l','8-4': 'l','8-5': 'l','8-6': 'l','8-7': 'l','8-8': 'l','8-9': 'l', + '9-1': 'l','9-2': 'l','9-3': 'l','9-4': 'l','9-5': 'l','9-6': 'l','9-7': 'l','9-8': 'l','9-9': 'l' +} + +const emptyBoard13 = { + '1-1': 'l','1-2': 'l','1-3': 'l','1-4': 'l','1-5': 'l','1-6': 'l','1-7': 'l','1-8': 'l','1-9': 'l','1-10': 'l','1-11': 'l','1-12': 'l','1-13': 'l', + '2-1': 'l','2-2': 'l','2-3': 'l','2-4': 'l','2-5': 'l','2-6': 'l','2-7': 'l','2-8': 'l','2-9': 'l','2-10': 'l','2-11': 'l','2-12': 'l','2-13': 'l', + '3-1': 'l','3-2': 'l','3-3': 'l','3-4': 'l','3-5': 'l','3-6': 'l','3-7': 'l','3-8': 'l','3-9': 'l','3-10': 'l','3-11': 'l','3-12': 'l','3-13': 'l', + '4-1': 'l','4-2': 'l','4-3': 'l','4-4': 'l','4-5': 'l','4-6': 'l','4-7': 'l','4-8': 'l','4-9': 'l','4-10': 'l','4-11': 'l','4-12': 'l','4-13': 'l', + '5-1': 'l','5-2': 'l','5-3': 'l','5-4': 'l','5-5': 'l','5-6': 'l','5-7': 'l','5-8': 'l','5-9': 'l','5-10': 'l','5-11': 'l','5-12': 'l','5-13': 'l', + '6-1': 'l','6-2': 'l','6-3': 'l','6-4': 'l','6-5': 'l','6-6': 'l','6-7': 'l','6-8': 'l','6-9': 'l','6-10': 'l','6-11': 'l','6-12': 'l','6-13': 'l', + '7-1': 'l','7-2': 'l','7-3': 'l','7-4': 'l','7-5': 'l','7-6': 'l','7-7': 'l','7-8': 'l','7-9': 'l','7-10': 'l','7-11': 'l','7-12': 'l','7-13': 'l', + '8-1': 'l','8-2': 'l','8-3': 'l','8-4': 'l','8-5': 'l','8-6': 'l','8-7': 'l','8-8': 'l','8-9': 'l','8-10': 'l','8-11': 'l','8-12': 'l','8-13': 'l', + '9-1': 'l','9-2': 'l','9-3': 'l','9-4': 'l','9-5': 'l','9-6': 'l','9-7': 'l','9-8': 'l','9-9': 'l','9-10': 'l','9-11': 'l','9-12': 'l','9-13': 'l', + '10-1': 'l','10-2': 'l','10-3': 'l','10-4': 'l','10-5': 'l','10-6': 'l','10-7': 'l','10-8': 'l','10-9': 'l','10-10': 'l','10-11': 'l','10-12': 'l','10-13': 'l', + '11-1': 'l','11-2': 'l','11-3': 'l','11-4': 'l','11-5': 'l','11-6': 'l','11-7': 'l','11-8': 'l','11-9': 'l','11-10': 'l','11-11': 'l','11-12': 'l','11-13': 'l', + '12-1': 'l','12-2': 'l','12-3': 'l','12-4': 'l','12-5': 'l','12-6': 'l','12-7': 'l','12-8': 'l','12-9': 'l','12-10': 'l','12-11': 'l','12-12': 'l','12-13': 'l', + '13-1': 'l','13-2': 'l','13-3': 'l','13-4': 'l','13-5': 'l','13-6': 'l','13-7': 'l','13-8': 'l','13-9': 'l','13-10': 'l','13-11': 'l','13-12': 'l','13-13': 'l' +} + +const emptyBoard = { + '1-1': 'l','1-2': 'l','1-3': 'l','1-4': 'l','1-5': 'l','1-6': 'l','1-7': 'l','1-8': 'l','1-9': 'l','1-10': 'l','1-11': 'l','1-12': 'l','1-13': 'l','1-14': 'l','1-15': 'l','1-16': 'l','1-17': 'l','1-18': 'l','1-19': 'l', + '2-1': 'l','2-2': 'l','2-3': 'l','2-4': 'l','2-5': 'l','2-6': 'l','2-7': 'l','2-8': 'l','2-9': 'l','2-10': 'l','2-11': 'l','2-12': 'l','2-13': 'l','2-14': 'l','2-15': 'l','2-16': 'l','2-17': 'l','2-18': 'l','2-19': 'l', + '3-1': 'l','3-2': 'l','3-3': 'l','3-4': 'l','3-5': 'l','3-6': 'l','3-7': 'l','3-8': 'l','3-9': 'l','3-10': 'l','3-11': 'l','3-12': 'l','3-13': 'l','3-14': 'l','3-15': 'l','3-16': 'l','3-17': 'l','3-18': 'l','3-19': 'l', + '4-1': 'l','4-2': 'l','4-3': 'l','4-4': 'l','4-5': 'l','4-6': 'l','4-7': 'l','4-8': 'l','4-9': 'l','4-10': 'l','4-11': 'l','4-12': 'l','4-13': 'l','4-14': 'l','4-15': 'l','4-16': 'l','4-17': 'l','4-18': 'l','4-19': 'l', + '5-1': 'l','5-2': 'l','5-3': 'l','5-4': 'l','5-5': 'l','5-6': 'l','5-7': 'l','5-8': 'l','5-9': 'l','5-10': 'l','5-11': 'l','5-12': 'l','5-13': 'l','5-14': 'l','5-15': 'l','5-16': 'l','5-17': 'l','5-18': 'l','5-19': 'l', + '6-1': 'l','6-2': 'l','6-3': 'l','6-4': 'l','6-5': 'l','6-6': 'l','6-7': 'l','6-8': 'l','6-9': 'l','6-10': 'l','6-11': 'l','6-12': 'l','6-13': 'l','6-14': 'l','6-15': 'l','6-16': 'l','6-17': 'l','6-18': 'l','6-19': 'l', + '7-1': 'l','7-2': 'l','7-3': 'l','7-4': 'l','7-5': 'l','7-6': 'l','7-7': 'l','7-8': 'l','7-9': 'l','7-10': 'l','7-11': 'l','7-12': 'l','7-13': 'l','7-14': 'l','7-15': 'l','7-16': 'l','7-17': 'l','7-18': 'l','7-19': 'l', + '8-1': 'l','8-2': 'l','8-3': 'l','8-4': 'l','8-5': 'l','8-6': 'l','8-7': 'l','8-8': 'l','8-9': 'l','8-10': 'l','8-11': 'l','8-12': 'l','8-13': 'l','8-14': 'l','8-15': 'l','8-16': 'l','8-17': 'l','8-18': 'l','8-19': 'l', + '9-1': 'l','9-2': 'l','9-3': 'l','9-4': 'l','9-5': 'l','9-6': 'l','9-7': 'l','9-8': 'l','9-9': 'l','9-10': 'l','9-11': 'l','9-12': 'l','9-13': 'l','9-14': 'l','9-15': 'l','9-16': 'l','9-17': 'l','9-18': 'l','9-19': 'l', + '10-1': 'l','10-2': 'l','10-3': 'l','10-4': 'l','10-5': 'l','10-6': 'l','10-7': 'l','10-8': 'l','10-9': 'l','10-10': 'l','10-11': 'l','10-12': 'l','10-13': 'l','10-14': 'l','10-15': 'l','10-16': 'l','10-17': 'l','10-18': 'l','10-19': 'l', + '11-1': 'l','11-2': 'l','11-3': 'l','11-4': 'l','11-5': 'l','11-6': 'l','11-7': 'l','11-8': 'l','11-9': 'l','11-10': 'l','11-11': 'l','11-12': 'l','11-13': 'l','11-14': 'l','11-15': 'l','11-16': 'l','11-17': 'l','11-18': 'l','11-19': 'l', + '12-1': 'l','12-2': 'l','12-3': 'l','12-4': 'l','12-5': 'l','12-6': 'l','12-7': 'l','12-8': 'l','12-9': 'l','12-10': 'l','12-11': 'l','12-12': 'l','12-13': 'l','12-14': 'l','12-15': 'l','12-16': 'l','12-17': 'l','12-18': 'l','12-19': 'l', + '13-1': 'l','13-2': 'l','13-3': 'l','13-4': 'l','13-5': 'l','13-6': 'l','13-7': 'l','13-8': 'l','13-9': 'l','13-10': 'l','13-11': 'l','13-12': 'l','13-13': 'l','13-14': 'l','13-15': 'l','13-16': 'l','13-17': 'l','13-18': 'l','13-19': 'l', + '14-1': 'l','14-2': 'l','14-3': 'l','14-4': 'l','14-5': 'l','14-6': 'l','14-7': 'l','14-8': 'l','14-9': 'l','14-10': 'l','14-11': 'l','14-12': 'l','14-13': 'l','14-14': 'l','14-15': 'l','14-16': 'l','14-17': 'l','14-18': 'l','14-19': 'l', + '15-1': 'l','15-2': 'l','15-3': 'l','15-4': 'l','15-5': 'l','15-6': 'l','15-7': 'l','15-8': 'l','15-9': 'l','15-10': 'l','15-11': 'l','15-12': 'l','15-13': 'l','15-14': 'l','15-15': 'l','15-16': 'l','15-17': 'l','15-18': 'l','15-19': 'l', + '16-1': 'l','16-2': 'l','16-3': 'l','16-4': 'l','16-5': 'l','16-6': 'l','16-7': 'l','16-8': 'l','16-9': 'l','16-10': 'l','16-11': 'l','16-12': 'l','16-13': 'l','16-14': 'l','16-15': 'l','16-16': 'l','16-17': 'l','16-18': 'l','16-19': 'l', + '17-1': 'l','17-2': 'l','17-3': 'l','17-4': 'l','17-5': 'l','17-6': 'l','17-7': 'l','17-8': 'l','17-9': 'l','17-10': 'l','17-11': 'l','17-12': 'l','17-13': 'l','17-14': 'l','17-15': 'l','17-16': 'l','17-17': 'l','17-18': 'l','17-19': 'l', + '18-1': 'l','18-2': 'l','18-3': 'l','18-4': 'l','18-5': 'l','18-6': 'l','18-7': 'l','18-8': 'l','18-9': 'l','18-10': 'l','18-11': 'l','18-12': 'l','18-13': 'l','18-14': 'l','18-15': 'l','18-16': 'l','18-17': 'l','18-18': 'l','18-19': 'l', + '19-1': 'l','19-2': 'l','19-3': 'l','19-4': 'l','19-5': 'l','19-6': 'l','19-7': 'l','19-8': 'l','19-9': 'l','19-10': 'l','19-11': 'l','19-12': 'l','19-13': 'l','19-14': 'l','19-15': 'l','19-16': 'l','19-17': 'l','19-18': 'l','19-19': 'l' +}; diff --git a/packages/server/test/Game.v2.spec.js b/packages/server/test/Game.v2.spec.js deleted file mode 100644 index 1f16d19..0000000 --- a/packages/server/test/Game.v2.spec.js +++ /dev/null @@ -1,537 +0,0 @@ -const chai = require('chai'); -const should = chai.should(); -const { Game, Point } = require('../services/Game.v2'); - -describe('Game', () => { - it('smoke test Game()', done => { - (typeof Game()) - .should.eql('object'); - done(); - }); - - it('smoke test Point()', done => { - (typeof Point({x: 1, y: 1})) - .should.eql('object'); - done(); - }); - - it('smoke test initGame()', done => { - (typeof Game().initGame()) - .should.eql('object'); - done(); - }); - - it('Get meta returns proper data for games with no record', done => { - Game().getMeta() - .should.eql(initialMeta); - Game().initGame().getMeta() - .should.eql({ ...initialMeta, turn: 1 }); - done(); - }); -}); - -describe('Game().initGame() returns legalMoves', () => { - it('initGame() returns default 19x19', done => { - Game().initGame() - .legalMoves.should.eql(emptyBoard); - done(); - }); - - it('initGame() with 2 handicap returns legalMoves with stones', done => { - Game({gameData: { handicap: 2 }}).initGame() - .legalMoves.should.eql({...emptyBoard, '4-16': 1, '16-4': 1}); - done(); - }); - - it('handicap stone has proper liberties', done => { - const game = Game({gameData: { handicap: 2 }}).initGame(); - const group = game.boardState['4-16'].group - game.groups[group].liberties.size.should.eql(4); - done(); - }); - - it('initGame( 19x19 ) with all levels of handicap returns legalMoves with stones', done => { - Game({gameData: { boardSize: 19, handicap: 2 }}).initGame() - .legalMoves.should.eql({...emptyBoard, '4-16': 1, '16-4': 1 }); - Game({gameData: { boardSize: 19, handicap: 3 }}).initGame() - .legalMoves.should.eql({...emptyBoard, '16-16': 1, '4-16': 1, '16-4': 1 }); - Game({gameData: { boardSize: 19, handicap: 4 }}).initGame() - .legalMoves.should.eql({...emptyBoard, '4-4': 1, '16-16': 1, '4-16': 1, '16-4': 1 }); - Game({gameData: { boardSize: 19, handicap: 5 }}).initGame() - .legalMoves.should.eql({...emptyBoard, '10-10': 1, '4-4': 1, '16-16': 1, '4-16': 1, '16-4': 1 }); - Game({gameData: { boardSize: 19, handicap: 6 }}).initGame() - .legalMoves.should.eql({...emptyBoard, '10-4': 1, '4-10': 1, '4-4': 1, '16-16': 1, '4-16': 1, '16-4': 1 }); - Game({gameData: { boardSize: 19, handicap: 7 }}).initGame() - .legalMoves.should.eql({...emptyBoard, '10-10': 1, '10-4': 1, '4-10': 1, '4-4': 1, '16-16': 1, '4-16': 1, '16-4': 1 }); - Game({gameData: { boardSize: 19, handicap: 8 }}).initGame() - .legalMoves.should.eql({...emptyBoard, '16-10': 1, '10-4': 1, '10-16': 1, '4-10': 1, '4-4': 1, '16-16': 1, '4-16': 1, '16-4': 1 }); - Game({gameData: { boardSize: 19, handicap: 9 }}).initGame() - .legalMoves.should.eql({...emptyBoard, '10-10': 1, '16-10': 1, '10-4': 1, '10-16': 1, '4-10': 1, '4-4': 1, '16-16': 1, '4-16': 1, '16-4': 1 }); - done(); - }) - - it('initGame( 13x13) returns legalMoves', done => { - Game({gameData: { boardSize: 13 }}).initGame() - .legalMoves.should.eql(emptyBoard13); - done(); - }); - - it('initGame( 13x13 ) with all levels of handicap returns legalMoves with stones', done => { - Game({gameData: { boardSize: 13, handicap: 2 }}).initGame() - .legalMoves.should.eql({...emptyBoard13, '4-10': 1, '10-4': 1 }); - Game({gameData: { boardSize: 13, handicap: 3 }}).initGame() - .legalMoves.should.eql({...emptyBoard13, '10-10': 1, '4-10': 1, '10-4': 1 }); - Game({gameData: { boardSize: 13, handicap: 4 }}).initGame() - .legalMoves.should.eql({...emptyBoard13, '4-4': 1, '10-10': 1, '4-10': 1, '10-4': 1 }); - Game({gameData: { boardSize: 13, handicap: 5 }}).initGame() - .legalMoves.should.eql({...emptyBoard13, '7-7': 1, '4-4': 1, '10-10': 1, '4-10': 1, '10-4': 1 }); - Game({gameData: { boardSize: 13, handicap: 6 }}).initGame() - .legalMoves.should.eql({...emptyBoard13, '7-4': 1, '4-7': 1, '4-4': 1, '10-10': 1, '4-10': 1, '10-4': 1 }); - Game({gameData: { boardSize: 13, handicap: 7 }}).initGame() - .legalMoves.should.eql({...emptyBoard13, '7-7': 1, '7-4': 1, '4-7': 1, '4-4': 1, '10-10': 1, '4-10': 1, '10-4': 1 }); - Game({gameData: { boardSize: 13, handicap: 8 }}).initGame() - .legalMoves.should.eql({...emptyBoard13, '10-7': 1, '7-4': 1, '7-10': 1, '4-7': 1, '4-4': 1, '10-10': 1, '4-10': 1, '10-4': 1 }); - Game({gameData: { boardSize: 13, handicap: 9 }}).initGame() - .legalMoves.should.eql({...emptyBoard13, '7-7': 1, '10-7': 1, '7-4': 1, '7-10': 1, '4-7': 1, '4-4': 1, '10-10': 1, '4-10': 1, '10-4': 1 }); - done(); - }); - - it('initGame( 9x9 ) returns legalMoves', done => { - Game({gameData: { boardSize: 9 }}).initGame() - .legalMoves.should.eql(emptyBoard9); - done(); - }); - - it('initGame( 9x9 ) with all levels of handicap returns legalMoves with stones', done => { - Game({gameData: { boardSize: 9, handicap: 2 }}).initGame() - .legalMoves.should.eql({...emptyBoard9, '3-7': 1, '7-3': 1 }); - Game({gameData: { boardSize: 9, handicap: 3 }}).initGame() - .legalMoves.should.eql({...emptyBoard9, '7-7': 1, '3-7': 1, '7-3': 1 }); - Game({gameData: { boardSize: 9, handicap: 4 }}).initGame() - .legalMoves.should.eql({...emptyBoard9, '3-3': 1, '7-7': 1, '3-7': 1, '7-3': 1 }); - done(); - }); -}); - -describe('Game.makeMove({ player: str, pos: { x: int, y: int } })', () => { - it('makeMove returns game object with proper board', done => { - Game().initGame().makeMove({ player: 'black', pos: { x: 4, y: 4 } }) - .legalMoves.should.eql({ ...emptyBoard, '4-4': 1 }); - Game({ gameData: { handicap: 2 } }).initGame().makeMove({ player: 'white', pos: { x: 4, y: 4 } }) - .legalMoves.should.eql({ ...emptyBoard, '4-16': 1, '16-4': 1, '4-4': -1 }); - done(); - }); - - it('makeMove returns success: false with move out of turn', done => { - Game().initGame().makeMove({ player: 'white', pos: { x: 4, y: 4 } }) - .success.should.eql(false); - Game({ gameData: { handicap: 2 } }).initGame().makeMove({ player: 'black', pos: { x: 4, y: 4 } }) - .success.should.eql(false); - done(); - }); - - it('makeMove returns success: false when move is at occupied point', done => { - Game({ gameData: { handicap: 2 } }).initGame().makeMove({ player: 'white', pos: { x: 4, y: 16 } }) - .success.should.eql(false); - done(); - }); - - it('makeMove next to adjacent stone of the same color joins stones as a group', done => { - const game = Game({ gameData: { handicap: 2 } }).initGame() // 4 3 4 - .makeMove({ player: 'white', pos: { x: 4, y: 4 } }) // 14 1 4 -1 -1 - .makeMove({ player: 'black', pos: { x: 4, y: 15 }}) // 15 1 5 -1 - .makeMove({ player: 'white', pos: { x: 3, y: 4 } }) // 16 1h - .makeMove({ player: 'black', pos: { x: 4, y: 14 }}) - .makeMove({ player: 'white', pos: { x: 4, y: 5 }}) - - const blackGroupKey = game.boardState['4-14'].group; - const blackGroup = game.groups[blackGroupKey].stones; - blackGroup.has(game.boardState['4-14']).should.eql(true); - blackGroup.has(game.boardState['4-15']).should.eql(true); - blackGroup.has(game.boardState['4-16']).should.eql(true); - const whiteGroupKey = game.boardState['4-4'].group; - const whiteGroup = game.groups[whiteGroupKey].stones; - whiteGroup.has(game.boardState['4-4']).should.eql(true); - whiteGroup.has(game.boardState['3-4']).should.eql(true); - whiteGroup.has(game.boardState['4-5']).should.eql(true); - done(); - }); - - const noGroupGame = Game({ gameData: { handicap: 2 } }).initGame() // 3 4 - .makeMove({ player: 'white', pos: { x: 4, y: 15 } }) // 14 1 - .makeMove({ player: 'black', pos: { x: 4, y: 14 }}) // 15 1 -1 no groups - .makeMove({ player: 'white', pos: { x: 3, y: 16 } }) // 16 -1 1h - .makeMove({ player: 'black', pos: { x: 3, y: 15 }}); - - it('makeMove next to adjacent stone of different color does not join stones as a group', done => { - const hoshiGroupKey = noGroupGame.boardState['4-16'].group; - const hoshiGroup = noGroupGame.groups[hoshiGroupKey].stones; - hoshiGroup.has(noGroupGame.boardState['4-16']).should.eql(true); - hoshiGroup.has(noGroupGame.boardState['4-15']).should.eql(false); - hoshiGroup.has(noGroupGame.boardState['3-14']).should.eql(false); - hoshiGroup.has(noGroupGame.boardState['3-15']).should.eql(false); - done(); - }) - - it('makeMove next to adjacent stone of different color should yield proper liberties', done => { - const hoshiGroup = noGroupGame.boardState['4-16'].group; - const hoshiGroupLiberties = noGroupGame.groups[hoshiGroup].liberties; - hoshiGroupLiberties.size.should.eql(2); - const fourFifteen = noGroupGame.boardState['4-15'].group; - const fourFifteenLiberties = noGroupGame.groups[fourFifteen].liberties; - fourFifteenLiberties.size.should.eql(1); - done(); - }) - - it('makeMove returns success: false when move is made in point with no liberties', done => { - const point = Game({ gameData: { handicap: 2 } }).initGame() // 15 16 17 - .makeMove({ player: 'white', pos: { x: 4, y: 4 } }).makeMove({ player: 'black', pos: { x: 6, y: 16 } }) // 4 1 - .makeMove({ player: 'white', pos: { x: 16, y: 16 }}).makeMove({ player: 'black', pos: { x: 5, y: 15 } }) // 5 1 x 1 - .makeMove({ player: 'white', pos: { x: 16, y: 10 }}).makeMove({ player: 'black', pos: { x: 5, y: 17 } }) // 6 1 - .makeMove({ player: 'white', pos: { x: 5, y: 16 }}) - point.success.should.eql(false); - done(); - }); -}); - -describe('makeMove group join and basic capture logic', () => { - const joinGame = Game().initGame() - .makeMove({ player: 'black', pos: { x: 4, y: 17 } }) // 3 4 5 - .makeMove({ player: 'white', pos: { x: 3, y: 16 } }) // 15 -1 - .makeMove({ player: 'black', pos: { x: 5, y: 16 } }) // 16 -1 1 1 - .makeMove({ player: 'white', pos: { x: 4, y: 15 } }) // 17 1 - .makeMove({ player: 'black', pos: { x: 4, y: 16 } }); - - it('gain liberties from group smoke test', done => { - joinGame.success.should.eql(true); - done(); - }); - - it('stones in group have same group property', done => { - joinGame.boardState['4-16'].group.should.eql(joinGame.boardState['5-16'].group); - joinGame.boardState['4-16'].group.should.eql(joinGame.boardState['4-17'].group); - joinGame.boardState['4-17'].group.should.eql(joinGame.boardState['4-16'].group); - joinGame.boardState['4-17'].group.should.eql(joinGame.boardState['5-16'].group); - joinGame.boardState['5-16'].group.should.eql(joinGame.boardState['4-17'].group); - joinGame.boardState['5-16'].group.should.eql(joinGame.boardState['4-16'].group); - done(); - }) - - it('stones in group should have proper liberties', done => { - const group = joinGame.boardState['4-16'].group; - joinGame.groups[group] - .liberties.size.should.eql(5); - done(); - }) - - it('group with only remaining liberty at point to be played returns success: false', done => { - Game({ gameData: { handicap: 2 } }).initGame() - .makeMove({ player: 'white', pos: { x: 4, y: 15 } }) // 3 4 5 6 - .makeMove({ player: 'black', pos: { x: 4, y: 4 } }) // 15 -1 -1 - .makeMove({ player: 'white', pos: { x: 5, y: 15 } }) // 16 -1 1h 0 -1 - .makeMove({ player: 'black', pos: { x: 16, y: 16 } }) // 17 -1 -1 - .makeMove({ player: 'white', pos: { x: 3, y: 16 } }) - .makeMove({ player: 'black', pos: { x: 4, y: 10 } }) - .makeMove({ player: 'white', pos: { x: 6, y: 16 } }) - .makeMove({ player: 'black', pos: { x: 10, y: 4 } }) - .makeMove({ player: 'white', pos: { x: 4, y: 17 } }) - .makeMove({ player: 'black', pos: { x: 10, y: 16 } }) - .makeMove({ player: 'white', pos: { x: 5, y: 17 } }) - .makeMove({ player: 'black', pos: { x: 5, y: 16 } }) - .success.should.eql(false); - done(); - }) - - const captureGame = () => Game({ gameData: { handicap: 2 } }).initGame() - .makeMove({ player: 'white', pos: { x: 4, y: 15 } }) // 3 4 5 - .makeMove({ player: 'black', pos: { x: 4, y: 4 } }) // 15 -1 - .makeMove({ player: 'white', pos: { x: 3, y: 16 } }) // 16 -1 0 -1 - .makeMove({ player: 'black', pos: { x: 4, y: 10 } }) // 17 -1 - .makeMove({ player: 'white', pos: { x: 5, y: 16 } }) // 4,16 captured - .makeMove({ player: 'black', pos: { x: 10, y: 4 } }) - - it('makeMove capture smoke test', done => { - captureGame().makeMove({ player: 'white', pos: { x: 4, y: 17 } }) - .success.should.eql(true); - done(); - }); - - it('makeMove assesses captures', done => { - captureGame().boardState['4-17'].capturing[-1].size.should.eql(1); - done(); - }) - - it('makeMove capture removes captured stone', done => { - captureGame().makeMove({ player: 'white', pos: { x: 4, y: 17 } }) - .boardState['4-16'].stone.should.eql(0); - done(); - }); - - it('makeMove capture increases capturing players captures', done => { - captureGame().makeMove({ player: 'white', pos: { x: 4, y: 17 } }) - .playerState.wCaptures.should.eql(1); - done(); - }); - - const multiCaptureGame = () => Game().initGame() - .makeMove({ player: 'black', pos: { x: 4, y: 17 } }) - .makeMove({ player: 'white', pos: { x: 3, y: 16 } }) - .makeMove({ player: 'black', pos: { x: 5, y: 16 } }) - .makeMove({ player: 'white', pos: { x: 4, y: 15 } }) - .makeMove({ player: 'black', pos: { x: 4, y: 16 } }) - .makeMove({ player: 'black', pos: { x: 4, y: 10 } }) // 3 4 5 6 - .makeMove({ player: 'white', pos: { x: 3, y: 17 } }) // 15 -1 -1 - .makeMove({ player: 'black', pos: { x: 10, y: 4 } }) // 16 -1 1 1 -1 - .makeMove({ player: 'white', pos: { x: 5, y: 15 } }) // 17 -1 1 -1 - .makeMove({ player: 'black', pos: { x: 10, y: 8 } }) // 18 -1 - .makeMove({ player: 'white', pos: { x: 4, y: 18} }) - .makeMove({ player: 'black', pos: { x: 3, y: 6 } }) - .makeMove({ player: 'white', pos: { x: 5, y: 17} }) - .makeMove({ player: 'black', pos: { x: 6, y: 3 } }); - - it('smoke test multi stone group capture', done => { - multiCaptureGame().makeMove({ player: 'white', pos: { x: 6, y: 16} }) - .success.should.eql(true); - done(); - }); - - it('multi stone group full group is in capturing', done => { - const game = multiCaptureGame() - const group = game.boardState['4-16'].group; - game.boardState['6-16'].capturing[-1].has(group).should.eql(true); - done(); - }); - - it('multi stone group capture all points are 0', done => { - const game = multiCaptureGame(); - game.makeMove({ player: 'white', pos: { x: 6, y: 16} }); - game.boardState['5-16'].stone.should.eql(0) - game.boardState['4-16'].stone.should.eql(0) - game.boardState['4-17'].stone.should.eql(0) - done(); - }); - - it('multi stone group capture scores points properly', done => { - const game = multiCaptureGame(); - game.makeMove({ player: 'white', pos: { x: 6, y: 16} }); - game.playerState.wCaptures.should.eql(3); - done(); - }) -}); - -describe('capture logic: snapback, ko and playing in eyes', () => { - it('playing in an eye formed by capture yields success: true', done => { - Game().initGame() - .makeMove({ player: 'black', pos: { x: 4, y: 4 } }) // 3 4 5 - .makeMove({ player: 'white', pos: { x: 5, y: 4 } }) // 4 1 - .makeMove({ player: 'black', pos: { x: 5, y: 5 } }) // 5 1 -1 1 - .makeMove({ player: 'white', pos: { x: 16, y: 16 } }) // 6 1 - .makeMove({ player: 'black', pos: { x: 5, y: 3 } }) // (9) at {5, 4} - .makeMove({ player: 'white', pos: { x: 16, y: 4 } }) - .makeMove({ player: 'black', pos: { x: 6, y: 4 } }) - .makeMove({ player: 'white', pos: { x: 4, y: 16 } }) - .makeMove({ player: 'black', pos: { x: 5, y: 4 } }) - .success.should.eql(true); - done(); - }); - - const snapbackGame = () => Game().initGame() - .makeMove({ player: 'black', pos: { x: 4, y: 4 } }) // 3 4 5 6 7 - .makeMove({ player: 'white', pos: { x: 5, y: 4 } }) // 4 1 1 -1 - .makeMove({ player: 'black', pos: { x: 5, y: 6 } }) // 5 1 -1 -1 1 -1 - .makeMove({ player: 'white', pos: { x: 5, y: 7 } }) // 6 1 1 -1 - .makeMove({ player: 'black', pos: { x: 4, y: 5 } }) // (13) at {5,6} - .makeMove({ player: 'white', pos: { x: 4, y: 6 } }) - .makeMove({ player: 'black', pos: { x: 5, y: 3 } }) - .makeMove({ player: 'white', pos: { x: 6, y: 6 } }) - .makeMove({ player: 'black', pos: { x: 6, y: 5 } }) - .makeMove({ player: 'white', pos: { x: 16, y: 16 } }) - .makeMove({ player: 'black', pos: { x: 6, y: 4 } }) - .makeMove({ player: 'white', pos: { x: 5, y: 5 } }) - .makeMove({ player: 'black', pos: { x: 5, y: 6 } }); - - it('snapback functions properly', done => { - snapbackGame() - .success.should.eql(true); - done(); - }); - - const koGame = () => Game().initGame() - .makeMove({ player: 'black', pos: { x: 4, y: 4 } }) // 3 4 5 6 - .makeMove({ player: 'white', pos: { x: 4, y: 5 } }) // 4 1 -1 - .makeMove({ player: 'black', pos: { x: 5, y: 3 } }) // 5 1 -1 1 -1 - .makeMove({ player: 'white', pos: { x: 5, y: 6 } }) // 6 1 -1 - .makeMove({ player: 'black', pos: { x: 6, y: 4 } }) - .makeMove({ player: 'white', pos: { x: 6, y: 5 } }) - .makeMove({ player: 'black', pos: { x: 5, y: 5 } }) - .makeMove({ player: 'white', pos: { x: 5, y: 4 } }) - - it('ko recognized properly on Point', done => { - koGame() - .boardState['5-5'].ko.should.eql(true); - done(); - }) - - it('ko marked on Game object', done => { - koGame().kos.should.eql(['5-5']); - done(); - }); - - it('ko marked in legalMoves', done => { - koGame().legalMoves['5-5'].should.eql('k'); - done(); - }) - - it('ko cleared on Point after move', done => { - koGame().makeMove({ player: 'black', pos: { x: 16, y: 16 } }) - .makeMove({ player: 'white', pos: { x: 4, y: 16 } }) - .boardState['5-5'].ko.should.eql(false); - done(); - }); - - it('ko cleared on Game after move', done => { - koGame().makeMove({ player: 'black', pos: { x: 16, y: 16 } }) - .makeMove({ player: 'white', pos: { x: 4, y: 16 } }) - .kos.should.eql([]) - done(); - }); - - it('ko cleared on legalMoves after move', done => { - koGame().makeMove({ player: 'black', pos: { x: 16, y: 16 } }) - .makeMove({ player: 'white', pos: { x: 4, y: 16 } }) - .legalMoves['5-5'].should.eql('l'); - done(); - }); -}); - -describe('Game history functionality', () => { - const firstMove = { player: 'black', pos: { x: 4, y: 4 }}; - const secondMove = { player: 'white', pos: { x: 16, y: 16 }}; - const thirdMove = { player: 'black', pos: { x: 16, y: 4 } }; - const fourthMove = { player: 'white', pos: { x: 4, y: 16 }}; - const fifthMove = { player: 'black', pos: { x: 10, y: 4 } }; - const sixthMove = { player: 'white', pos: { x: 4, y: 10 }}; - const seventhMove = { player: 'black', pos: { x: 10, y: 16 } }; - const eighthMove = { player: 'white', pos: { x: 16, y: 10 }}; - - it('makeMove creates gameRecord item', done => { - Game().initGame() - .makeMove(firstMove).gameRecord[0].should.eql(firstMove); - done(); - }); - - it('makeMove holds history', done => { - const game = Game().initGame() - .makeMove(firstMove).makeMove(secondMove); - game.gameRecord[0].should.eql(firstMove); - game.gameRecord[1].should.eql(secondMove) - done(); - }); - - const rewoundGame = () => Game().initGame() - .makeMove(firstMove) - .makeMove(secondMove) - .makeMove(thirdMove) - .returnToMove(-1); - - it('Game.returnToMove returns new Game with gameRecord', done => { - rewoundGame() - .gameRecord.should.eql([ firstMove, secondMove ]) - done(); - }); - - it('Game.returnToMove returns new Game with new board state', done => { - rewoundGame() - .boardState['16-4'].stone.should.eql(0); - rewoundGame() - .boardState['4-4'].stone.should.eql(1); - rewoundGame() - .boardState['16-16'].stone.should.eql(-1); - done(); - }); - - const resetGame = () => [ - firstMove, secondMove, thirdMove, fourthMove, fifthMove, sixthMove, seventhMove, eighthMove - ].reduce((game, move) => game.makeMove(move), Game().initGame()); - - it('Game.returnToMove(0) returns to init board state', done => { - const erasedGame = resetGame() - .returnToMove(0) - erasedGame.gameRecord.should.eql([]) - erasedGame.boardState['4-4'].stone.should.eql(0) - done(); - }); - - it('Game.returnToMove(5) returns to state after 5th move', done => { - const fifthMoveGame = resetGame() - .returnToMove(5); - fifthMoveGame.gameRecord.should.eql([firstMove, secondMove, thirdMove, fourthMove, fifthMove]); - fifthMoveGame.boardState['10-4'].stone.should.eql(1) - fifthMoveGame.boardState['4-10'].stone.should.eql(0) - done(); - }) -}) - - -const initialMeta = { - winner: null, - turn: 0, - pass: 0, - playerState: { - bCaptures: 0, - wCaptures: 0, - bScore: 0, - wScore: 0 - }, - gameRecord: [] -} - -const emptyBoard9 = { - '1-1': 'l','1-2': 'l','1-3': 'l','1-4': 'l','1-5': 'l','1-6': 'l','1-7': 'l','1-8': 'l','1-9': 'l', - '2-1': 'l','2-2': 'l','2-3': 'l','2-4': 'l','2-5': 'l','2-6': 'l','2-7': 'l','2-8': 'l','2-9': 'l', - '3-1': 'l','3-2': 'l','3-3': 'l','3-4': 'l','3-5': 'l','3-6': 'l','3-7': 'l','3-8': 'l','3-9': 'l', - '4-1': 'l','4-2': 'l','4-3': 'l','4-4': 'l','4-5': 'l','4-6': 'l','4-7': 'l','4-8': 'l','4-9': 'l', - '5-1': 'l','5-2': 'l','5-3': 'l','5-4': 'l','5-5': 'l','5-6': 'l','5-7': 'l','5-8': 'l','5-9': 'l', - '6-1': 'l','6-2': 'l','6-3': 'l','6-4': 'l','6-5': 'l','6-6': 'l','6-7': 'l','6-8': 'l','6-9': 'l', - '7-1': 'l','7-2': 'l','7-3': 'l','7-4': 'l','7-5': 'l','7-6': 'l','7-7': 'l','7-8': 'l','7-9': 'l', - '8-1': 'l','8-2': 'l','8-3': 'l','8-4': 'l','8-5': 'l','8-6': 'l','8-7': 'l','8-8': 'l','8-9': 'l', - '9-1': 'l','9-2': 'l','9-3': 'l','9-4': 'l','9-5': 'l','9-6': 'l','9-7': 'l','9-8': 'l','9-9': 'l' -} - -const emptyBoard13 = { - '1-1': 'l','1-2': 'l','1-3': 'l','1-4': 'l','1-5': 'l','1-6': 'l','1-7': 'l','1-8': 'l','1-9': 'l','1-10': 'l','1-11': 'l','1-12': 'l','1-13': 'l', - '2-1': 'l','2-2': 'l','2-3': 'l','2-4': 'l','2-5': 'l','2-6': 'l','2-7': 'l','2-8': 'l','2-9': 'l','2-10': 'l','2-11': 'l','2-12': 'l','2-13': 'l', - '3-1': 'l','3-2': 'l','3-3': 'l','3-4': 'l','3-5': 'l','3-6': 'l','3-7': 'l','3-8': 'l','3-9': 'l','3-10': 'l','3-11': 'l','3-12': 'l','3-13': 'l', - '4-1': 'l','4-2': 'l','4-3': 'l','4-4': 'l','4-5': 'l','4-6': 'l','4-7': 'l','4-8': 'l','4-9': 'l','4-10': 'l','4-11': 'l','4-12': 'l','4-13': 'l', - '5-1': 'l','5-2': 'l','5-3': 'l','5-4': 'l','5-5': 'l','5-6': 'l','5-7': 'l','5-8': 'l','5-9': 'l','5-10': 'l','5-11': 'l','5-12': 'l','5-13': 'l', - '6-1': 'l','6-2': 'l','6-3': 'l','6-4': 'l','6-5': 'l','6-6': 'l','6-7': 'l','6-8': 'l','6-9': 'l','6-10': 'l','6-11': 'l','6-12': 'l','6-13': 'l', - '7-1': 'l','7-2': 'l','7-3': 'l','7-4': 'l','7-5': 'l','7-6': 'l','7-7': 'l','7-8': 'l','7-9': 'l','7-10': 'l','7-11': 'l','7-12': 'l','7-13': 'l', - '8-1': 'l','8-2': 'l','8-3': 'l','8-4': 'l','8-5': 'l','8-6': 'l','8-7': 'l','8-8': 'l','8-9': 'l','8-10': 'l','8-11': 'l','8-12': 'l','8-13': 'l', - '9-1': 'l','9-2': 'l','9-3': 'l','9-4': 'l','9-5': 'l','9-6': 'l','9-7': 'l','9-8': 'l','9-9': 'l','9-10': 'l','9-11': 'l','9-12': 'l','9-13': 'l', - '10-1': 'l','10-2': 'l','10-3': 'l','10-4': 'l','10-5': 'l','10-6': 'l','10-7': 'l','10-8': 'l','10-9': 'l','10-10': 'l','10-11': 'l','10-12': 'l','10-13': 'l', - '11-1': 'l','11-2': 'l','11-3': 'l','11-4': 'l','11-5': 'l','11-6': 'l','11-7': 'l','11-8': 'l','11-9': 'l','11-10': 'l','11-11': 'l','11-12': 'l','11-13': 'l', - '12-1': 'l','12-2': 'l','12-3': 'l','12-4': 'l','12-5': 'l','12-6': 'l','12-7': 'l','12-8': 'l','12-9': 'l','12-10': 'l','12-11': 'l','12-12': 'l','12-13': 'l', - '13-1': 'l','13-2': 'l','13-3': 'l','13-4': 'l','13-5': 'l','13-6': 'l','13-7': 'l','13-8': 'l','13-9': 'l','13-10': 'l','13-11': 'l','13-12': 'l','13-13': 'l' -} - -const emptyBoard = { - '1-1': 'l','1-2': 'l','1-3': 'l','1-4': 'l','1-5': 'l','1-6': 'l','1-7': 'l','1-8': 'l','1-9': 'l','1-10': 'l','1-11': 'l','1-12': 'l','1-13': 'l','1-14': 'l','1-15': 'l','1-16': 'l','1-17': 'l','1-18': 'l','1-19': 'l', - '2-1': 'l','2-2': 'l','2-3': 'l','2-4': 'l','2-5': 'l','2-6': 'l','2-7': 'l','2-8': 'l','2-9': 'l','2-10': 'l','2-11': 'l','2-12': 'l','2-13': 'l','2-14': 'l','2-15': 'l','2-16': 'l','2-17': 'l','2-18': 'l','2-19': 'l', - '3-1': 'l','3-2': 'l','3-3': 'l','3-4': 'l','3-5': 'l','3-6': 'l','3-7': 'l','3-8': 'l','3-9': 'l','3-10': 'l','3-11': 'l','3-12': 'l','3-13': 'l','3-14': 'l','3-15': 'l','3-16': 'l','3-17': 'l','3-18': 'l','3-19': 'l', - '4-1': 'l','4-2': 'l','4-3': 'l','4-4': 'l','4-5': 'l','4-6': 'l','4-7': 'l','4-8': 'l','4-9': 'l','4-10': 'l','4-11': 'l','4-12': 'l','4-13': 'l','4-14': 'l','4-15': 'l','4-16': 'l','4-17': 'l','4-18': 'l','4-19': 'l', - '5-1': 'l','5-2': 'l','5-3': 'l','5-4': 'l','5-5': 'l','5-6': 'l','5-7': 'l','5-8': 'l','5-9': 'l','5-10': 'l','5-11': 'l','5-12': 'l','5-13': 'l','5-14': 'l','5-15': 'l','5-16': 'l','5-17': 'l','5-18': 'l','5-19': 'l', - '6-1': 'l','6-2': 'l','6-3': 'l','6-4': 'l','6-5': 'l','6-6': 'l','6-7': 'l','6-8': 'l','6-9': 'l','6-10': 'l','6-11': 'l','6-12': 'l','6-13': 'l','6-14': 'l','6-15': 'l','6-16': 'l','6-17': 'l','6-18': 'l','6-19': 'l', - '7-1': 'l','7-2': 'l','7-3': 'l','7-4': 'l','7-5': 'l','7-6': 'l','7-7': 'l','7-8': 'l','7-9': 'l','7-10': 'l','7-11': 'l','7-12': 'l','7-13': 'l','7-14': 'l','7-15': 'l','7-16': 'l','7-17': 'l','7-18': 'l','7-19': 'l', - '8-1': 'l','8-2': 'l','8-3': 'l','8-4': 'l','8-5': 'l','8-6': 'l','8-7': 'l','8-8': 'l','8-9': 'l','8-10': 'l','8-11': 'l','8-12': 'l','8-13': 'l','8-14': 'l','8-15': 'l','8-16': 'l','8-17': 'l','8-18': 'l','8-19': 'l', - '9-1': 'l','9-2': 'l','9-3': 'l','9-4': 'l','9-5': 'l','9-6': 'l','9-7': 'l','9-8': 'l','9-9': 'l','9-10': 'l','9-11': 'l','9-12': 'l','9-13': 'l','9-14': 'l','9-15': 'l','9-16': 'l','9-17': 'l','9-18': 'l','9-19': 'l', - '10-1': 'l','10-2': 'l','10-3': 'l','10-4': 'l','10-5': 'l','10-6': 'l','10-7': 'l','10-8': 'l','10-9': 'l','10-10': 'l','10-11': 'l','10-12': 'l','10-13': 'l','10-14': 'l','10-15': 'l','10-16': 'l','10-17': 'l','10-18': 'l','10-19': 'l', - '11-1': 'l','11-2': 'l','11-3': 'l','11-4': 'l','11-5': 'l','11-6': 'l','11-7': 'l','11-8': 'l','11-9': 'l','11-10': 'l','11-11': 'l','11-12': 'l','11-13': 'l','11-14': 'l','11-15': 'l','11-16': 'l','11-17': 'l','11-18': 'l','11-19': 'l', - '12-1': 'l','12-2': 'l','12-3': 'l','12-4': 'l','12-5': 'l','12-6': 'l','12-7': 'l','12-8': 'l','12-9': 'l','12-10': 'l','12-11': 'l','12-12': 'l','12-13': 'l','12-14': 'l','12-15': 'l','12-16': 'l','12-17': 'l','12-18': 'l','12-19': 'l', - '13-1': 'l','13-2': 'l','13-3': 'l','13-4': 'l','13-5': 'l','13-6': 'l','13-7': 'l','13-8': 'l','13-9': 'l','13-10': 'l','13-11': 'l','13-12': 'l','13-13': 'l','13-14': 'l','13-15': 'l','13-16': 'l','13-17': 'l','13-18': 'l','13-19': 'l', - '14-1': 'l','14-2': 'l','14-3': 'l','14-4': 'l','14-5': 'l','14-6': 'l','14-7': 'l','14-8': 'l','14-9': 'l','14-10': 'l','14-11': 'l','14-12': 'l','14-13': 'l','14-14': 'l','14-15': 'l','14-16': 'l','14-17': 'l','14-18': 'l','14-19': 'l', - '15-1': 'l','15-2': 'l','15-3': 'l','15-4': 'l','15-5': 'l','15-6': 'l','15-7': 'l','15-8': 'l','15-9': 'l','15-10': 'l','15-11': 'l','15-12': 'l','15-13': 'l','15-14': 'l','15-15': 'l','15-16': 'l','15-17': 'l','15-18': 'l','15-19': 'l', - '16-1': 'l','16-2': 'l','16-3': 'l','16-4': 'l','16-5': 'l','16-6': 'l','16-7': 'l','16-8': 'l','16-9': 'l','16-10': 'l','16-11': 'l','16-12': 'l','16-13': 'l','16-14': 'l','16-15': 'l','16-16': 'l','16-17': 'l','16-18': 'l','16-19': 'l', - '17-1': 'l','17-2': 'l','17-3': 'l','17-4': 'l','17-5': 'l','17-6': 'l','17-7': 'l','17-8': 'l','17-9': 'l','17-10': 'l','17-11': 'l','17-12': 'l','17-13': 'l','17-14': 'l','17-15': 'l','17-16': 'l','17-17': 'l','17-18': 'l','17-19': 'l', - '18-1': 'l','18-2': 'l','18-3': 'l','18-4': 'l','18-5': 'l','18-6': 'l','18-7': 'l','18-8': 'l','18-9': 'l','18-10': 'l','18-11': 'l','18-12': 'l','18-13': 'l','18-14': 'l','18-15': 'l','18-16': 'l','18-17': 'l','18-18': 'l','18-19': 'l', - '19-1': 'l','19-2': 'l','19-3': 'l','19-4': 'l','19-5': 'l','19-6': 'l','19-7': 'l','19-8': 'l','19-9': 'l','19-10': 'l','19-11': 'l','19-12': 'l','19-13': 'l','19-14': 'l','19-15': 'l','19-16': 'l','19-17': 'l','19-18': 'l','19-19': 'l' -}; diff --git a/packages/server/test/gameServices.spec.js b/packages/server/test/gameServices.spec.js index 680758a..bd83e41 100644 --- a/packages/server/test/gameServices.spec.js +++ b/packages/server/test/gameServices.spec.js @@ -4,34 +4,37 @@ const gameServices = require('../services/gameServices'); describe('game services', () => { it('init game returns game board', done => { - gameServices.initGame({id: 1, handicap: 4}) - gameServices.getBoard(1).should.eql(fourHandicapBoard) + gameServices.initGame({ id: 1, handicap: 4 }) + .board.should.eql(fourHandicapBoard) done(); }); + + it('init game returns game metadata', done => { + const { board, ...game } = gameServices.initGame({ id: 1, handicap: 4 }) + game.should.eql({...initialMeta, handicap: 4, turn: -1}); + done(); + }) it('games services places move', done => { - gameServices.initGame({id: 1, handicap: 4}) - const afterMoveOne = gameServices.makeMove({id: 1}, {player: 'white', pos: { x:6, y:3 }}); - const afterMoveOneShould = { board:{ ...fourHandicapBoard, '6-3': -1}, meta: moveOneMeta }; - afterMoveOne.should.eql(afterMoveOneShould); + gameServices.initGame({ id: 1, handicap: 4 }) + const move = { player: 'white', pos: { x: 6, y: 3 } } + const afterMove = gameServices.makeMove({ id: 1, move }); + const afterMoveShould = { board: { ...fourHandicapBoard, '6-3': -1}, ...initialMeta, handicap: 4, turn: 1, gameRecord: [ move ] }; + afterMove.should.eql(afterMoveShould); done(); }); - it('illegal move throws error', done => { - try { - gameServices.initGame({id: 1, handicap: 4}) - const afterIllegalMove = gameServices.makeMove({id: 1}, {player: 'white', pos: { x:4, y:4 }}); - } - catch (err) { - err.message.should.equal('illegal move') - done(); - } - }) + it('illegal move returns error message', done => { + gameServices.initGame({ id: 1, handicap: 4 }); + gameServices.makeMove({ id: 1, move: { player: 'white', pos: { x:4, y:4 } } }) + .message.should.equal('illegal move'); + done(); + }); it('game services places move next to stone', done => { gameServices.initGame({ id: 1, handicap:4 }); - const afterMoveOne = gameServices.makeMove({ id: 1 }, { player: 'white', pos: { x: 4, y: 3 } }); - afterMoveOne.should.not.eql(fourHandicapBoard); + gameServices.makeMove({ id: 1, move: { player: 'white', pos: { x: 4, y: 3 } } }) + .board.should.eql({ ...fourHandicapBoard, '4-3': -1 }); done(); }) }) @@ -59,17 +62,18 @@ const fourHandicapBoard = { '19-1': 'l','19-2': 'l','19-3': 'l','19-4': 'l','19-5': 'l','19-6': 'l','19-7': 'l','19-8': 'l','19-9': 'l','19-10': 'l','19-11': 'l','19-12': 'l','19-13': 'l','19-14': 'l','19-15': 'l','19-16': 'l','19-17': 'l','19-18': 'l','19-19': 'l' }; -const moveOneMeta = { - gameRecord: [ - {player: 'white', pos: { x:6, y:3 }} - ], +const initialMeta = { + winner: null, + turn: 0, pass: 0, + komi: 6.5, + handicap: 0, + boardSize: 19, playerState: { bCaptures: 0, - bScore: 0, wCaptures: 0, + bScore: 0, wScore: 0 - }, - turn: 1, - winner: null + }, + gameRecord: [] } \ No newline at end of file