diff --git a/packages/server/services/Game.v2.js b/packages/server/services/Game.v2.js index 9416c10..0ad5770 100644 --- a/packages/server/services/Game.v2.js +++ b/packages/server/services/Game.v2.js @@ -63,7 +63,6 @@ const getSingleItemFromSet = set => { 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)); @@ -80,9 +79,10 @@ const checkLegal = ({ point, Game }) => { point.legal = false; return point; } - + const neighbors = getNeighbors({Game, point}); + const isEmpty = point => point.stone === 0 && point.legal === true; - const isEmptyAdjacent = Object.values(point.neighbors).filter(isEmpty); + const isEmptyAdjacent = neighbors.filter(isEmpty); // if empty point adjacent return true if (!isEmptyAdjacent.length) { @@ -92,7 +92,7 @@ const checkLegal = ({ point, Game }) => { 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 = Object.values(point.neighbors).filter(isTurnStone).filter(isInGroupWithLiberties).length; + const isInLiveGroup = neighbors.filter(isTurnStone).filter(isInGroupWithLiberties).length; if (isInLiveGroup) { point.legal = true; @@ -123,19 +123,15 @@ const getLegalMoves = (Game) => { return pipeMap(mapLegal)(Game.boardState); } -const getNeighbors = boardSize => (point, i, boardState) => { - const { top, btm, lft, rgt} = point.neighbors; +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}) ] - point.neighbors.top = top ? boardState[i - boardSize][1] : top; - point.neighbors.btm = btm ? boardState[i + boardSize][1] : btm; - point.neighbors.lft = lft ? boardState[i - 1][1] : lft; - point.neighbors.rgt = rgt ? boardState[i + 1][1] : rgt; - for (let [direction, neighbor] of Object.entries(point.neighbors)) { - if (!neighbor) { - delete point.neighbors[direction]; - } - } - return point; + 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) => { @@ -149,14 +145,14 @@ const initBoard = (game) => { }); boardState[`${point.pos.x}-${point.pos.y}`] = point; } - const boardStateWithNeighbors = pipeMap(getNeighbors(boardSize))(boardState) + if (handicap) { HANDI_PLACE[boardSize][handicap].forEach(pt => { - boardStateWithNeighbors[pt].makeMove(game); + boardState[pt].makeMove({...game, boardState}); }); game.turn *= -1; } - return boardStateWithNeighbors; + return boardState; } // returns Game object @@ -183,136 +179,160 @@ const Game = ({gameData = {}, gameRecord = []} = {}) => ({ this.turn = 1; this.boardState = initBoard(this); this.boardState = getBoardState(this); - return { ...this, legalMoves: getLegalMoves(this)}; + this.legalMoves = getLegalMoves(this) + return this; }, 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 } }, makeMove: function({ player, pos: {x, y}}) { + let game = this; let success = false; - const point = this.boardState[`${x}-${y}`]; - const isTurn = ( this.turn === 1 && player === 'black' ) - || ( this.turn === -1 && player === 'white' ); + const point = game.boardState[`${x}-${y}`]; + const isTurn = ( game.turn === 1 && player === 'black' ) + || ( game.turn === -1 && player === 'white' ); if (isTurn) { if (point.legal) { - point.makeMove(this); - this.turn *= -1; + game = point.makeMove(game); + game.turn *= -1; success = true; } } - this.boardState = getBoardState(this); - return {...this, legalMoves: getLegalMoves(this), success }; + game.boardState = getBoardState(game); + return {...game, legalMoves: getLegalMoves(game), success }; }, initGroup: function(point) { - const newSymbol = Symbol(`${point.pos.x}-${point.pos.y}`); - this.groups[newSymbol] = { stones: new Set(), liberties: new Set()}; - return newSymbol; + const group = Symbol(`${point.pos.x}-${point.pos.y}`); + this.groups[group] = { stones: new Set(), liberties: new Set()}; + return { game: this, group }; } }); -const Point = ({x, y, boardSize = 19}) => ({ - pos: {x, y}, - stone: 0, // can be 1, -1, 0, or 'k' for ko - legal: true, - territory: 0, - capturing: { - '1': [], - '-1': [] - }, - 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 - }, +const Point = ({x, y, boardSize = 19}) => { + let point = { + pos: {x, y}, + key: `${x}-${y}`, + stone: 0, // can be 1, -1, 0, or 'k' for ko + legal: true, + territory: 0, + capturing: { + '1': [], + '-1': [] + }, + 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].length) { - this.makeCaptures(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) { - point.group = game.initGroup(point); + makeMove: function(Game) { + this.stone = Game.turn; + this.legal = false; + if (this.capturing[this.stone].length) { + Game = this.makeCaptures(Game); } - - // add current point to global group and override current group - game.groups[point.group].stones.add(this); - this.group = point.group; - this.setLiberties(game); - for (let neighbor of Object.values(this.neighbors)) { - if ( neighbor.stone === this.stone - // this check prevents infinite call chains - && neighbor.group !== this.group - ) { - neighbor.joinGroup({ point: this, 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 = Object.values(this.neighbors); - const liberties = game.groups[this.group].liberties; - // if point is occupied remove it from liberties set of point group, else add it - neighbors.forEach( pt => { - if (pt.stone) { - liberties.delete(pt); - game.groups[pt.group].liberties.delete(this); - } else { - liberties.add(pt) - } - }); - }, + 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].push(this.group); - } - - // if neighbors have one liberty - const neighbors = Object.values(this.neighbors).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) { + 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[neighbor.stone * -1].push(neighbor.group); + lastLiberty.capturing[this.stone * -1].push(this.group); } - }) - }, - makeCaptures: function(game) { - // for each group - this.capturing[this.stone].forEach(captureGroup => { - const capturesSet = game.groups[captureGroup].stones; - for (let [capture] of capturesSet.entries()) { - capture.removeStone(game); - } - }) - }, + // 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].push(neighbor.group); + } + }); + return game; + }, - removeStone: function(game) { - // reset point - this.stone = 0; - this.group = null; - this.capturing[game.turn] = []; - // add captures + makeCaptures: function(game) { + // for each group + this.capturing[this.stone].forEach(captureGroup => { + const capturesSet = game.groups[captureGroup].stones; + for (let [capture] of capturesSet.entries()) { + game = capture.removeStone(game); + } + }); + return game; + }, + + removeStone: function(game) { + // reset point + this.stone = 0; + this.group = null; + this.capturing[game.turn] = []; + // add captures + const player = game.turn > 0 ? 'b' : 'w'; + game.playerState[`${player}Captures`] += 1; + return {...game, boardState: {...this.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, diff --git a/packages/server/test/Game.v2.spec.js b/packages/server/test/Game.v2.spec.js index e26ea4c..1af00df 100644 --- a/packages/server/test/Game.v2.spec.js +++ b/packages/server/test/Game.v2.spec.js @@ -30,7 +30,7 @@ describe('Game', () => { }); }); -describe('Game().initGame() returns boardState', () => { +describe('Game().initGame() returns legalMoves', () => { it('initGame() returns default 19x19', done => { Game().initGame() .legalMoves.should.eql(emptyBoard); @@ -43,24 +43,10 @@ describe('Game().initGame() returns boardState', () => { done(); }); - it('initGame() returns Game with all Points having neighbors', done => { - const boardState = Game().initGame().boardState; - const oneOneNeighbors = boardState['1-1'].neighbors; - const fiveSevenNeighbors = boardState['5-7'].neighbors; - const nineteenTenNeighbors = boardState['19-10'].neighbors; - - oneOneNeighbors.rgt.pos.should.eql({x: 1, y: 2}); - oneOneNeighbors.btm.pos.should.eql({x: 2, y: 1}); - - fiveSevenNeighbors.top.pos.should.eql({x: 4, y: 7}); - fiveSevenNeighbors.btm.pos.should.eql({x: 6, y: 7}); - fiveSevenNeighbors.lft.pos.should.eql({x: 5, y: 6}); - fiveSevenNeighbors.rgt.pos.should.eql({x: 5, y: 8}); - - nineteenTenNeighbors.top.pos.should.eql({x: 18, y: 10}); - nineteenTenNeighbors.lft.pos.should.eql({x: 19, y: 9}); - nineteenTenNeighbors.rgt.pos.should.eql({x: 19, y: 11}); - + 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(); }); @@ -171,19 +157,29 @@ describe('Game.makeMove({ player: str, pos: { x: int, y: int } })', () => { 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 game = 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 }}) - - const hoshiGroupKey = game.boardState['4-16'].group; - const hoshiGroup = game.groups[hoshiGroupKey].stones; - hoshiGroup.has(game.boardState['4-16']).should.eql(true); - hoshiGroup.has(game.boardState['4-15']).should.eql(false); - hoshiGroup.has(game.boardState['3-14']).should.eql(false); - hoshiGroup.has(game.boardState['3-15']).should.eql(false); + 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(); }) @@ -211,8 +207,25 @@ describe('makeMove group join and capture logic', () => { 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 => { - const point = Game({ gameData: { handicap: 2 } }).initGame() + 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 @@ -224,8 +237,6 @@ describe('makeMove group join and capture logic', () => { .makeMove({ player: 'white', pos: { x: 4, y: 17 } }) .makeMove({ player: 'black', pos: { x: 10, y: 16 } }) .makeMove({ player: 'white', pos: { x: 5, y: 17 } }) - console.log(point.boardState['5-16']); - point .makeMove({ player: 'black', pos: { x: 5, y: 16 } }) .success.should.eql(false); done(); @@ -256,9 +267,47 @@ describe('makeMove group join and capture logic', () => { done(); }); - // it('makeMove capture increases capturing players captures', done => { - // captureGame.makeMove({ player: 'white', pos: { x: 4, y: 17 } }) - // .playerState.wCaptures.should.eql(1); + 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 group = multiCaptureGame.boardState['5-16'].group; + console.log(multiCaptureGame.groups[group].liberties) + console.log(multiCaptureGame.boardState['4-16'].capturing) + multiCaptureGame.boardState['6-16'].capturing[-1][0].should.eql(group); + done(); + }) + + // it('multi stone group capture all points are 0', done => { + // const boardState = multiCaptureGame.makeMove({ player: 'white', pos: { x: 6, y: 16} }).boardState; + // boardState['5-16'].stone.should.eql(0) + // // boardState['4-16'].stone.should.eql(0) + // // boardState['4-17'].stone.should.eql(0) // done(); // }) })