deprecate Game.v1
This commit is contained in:
parent
a99426e03f
commit
b8eb3770d9
7 changed files with 1356 additions and 1344 deletions
|
@ -1,12 +1,3 @@
|
||||||
/*----- constants -----*/
|
|
||||||
const STONES_DATA = {
|
|
||||||
'-1': 'white',
|
|
||||||
'0': 'none',
|
|
||||||
'1': 'black',
|
|
||||||
'k': 'ko'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// index corresponds to difference in player rank
|
// index corresponds to difference in player rank
|
||||||
const KOMI_REC = {
|
const KOMI_REC = {
|
||||||
'9': [
|
'9': [
|
||||||
|
@ -35,388 +26,384 @@ const HANDI_REC = {
|
||||||
// index represents handicap placement for different board-sizes, eg handiPlace['9][1] = { (3, 3), (7, 7) }
|
// 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
|
// last array in each property also used for hoshi rendering
|
||||||
const HANDI_PLACE = {
|
const HANDI_PLACE = {
|
||||||
'9' : [
|
9 : [
|
||||||
0, 0,
|
0, 0,
|
||||||
[[ 7, 3 ], [ 3, 7 ] ],
|
[ '7-3', '3-7' ], // 2
|
||||||
[ [ 7, 7 ], [ 7, 3 ], [ 3, 7 ] ],
|
[ '7-7', '7-3', '3-7' ],
|
||||||
[ [ 3, 3 ], [ 7, 7 ], [ 3, 7 ], [ 7, 3 ] ]
|
[ '3-3', '7-7', '3-7', '7-3' ]
|
||||||
],
|
],
|
||||||
'13' : [
|
13 : [
|
||||||
0, 0,
|
0, 0,
|
||||||
[ [ 4, 10 ], [ 10, 4 ] ],
|
[ '4-10', '10-4' ], // 2
|
||||||
[ [ 10, 10 ], [ 4, 10 ], [ 10, 4] ],
|
[ '10-10', '4-10', '10-4' ],
|
||||||
[ [ 4, 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-7', '4-4', '10-10', '4-10', '10-4' ],
|
||||||
[ [ 7, 4 ], [ 4, 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] ],
|
[ '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] ],
|
[ '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] ],
|
[ '7-7', '10-7', '7-4', '7-10', '4-7', '4-4', '10-10', '4-10', '10-4' ],
|
||||||
],
|
],
|
||||||
'19' : [
|
19 : [
|
||||||
0, 0,
|
0, 0,
|
||||||
[ [ 4, 16 ], [ 16, 4 ] ],
|
[ '4-16', '16-4' ], // 2
|
||||||
[ [ 16, 16 ], [ 4, 16 ], [ 16, 4] ],
|
[ '16-16', '4-16', '16-4' ],
|
||||||
[ [ 4, 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-10', '4-4', '16-16', '4-16', '16-4' ],
|
||||||
[ [ 10, 4 ], [ 4, 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] ],
|
[ '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] ],
|
[ '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] ],
|
[ '10-10', '16-10', '10-4', '10-16', '4-10', '4-4', '16-16', '4-16', '16-4' ],
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
class Game {
|
const getSingleItemFromSet = set => {
|
||||||
constructor(gameData, gameRecord) {
|
let entry;
|
||||||
this.winner = gameData.winner || null,
|
for (entry of set.entries()) {
|
||||||
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
|
return entry[0];
|
||||||
this.komi = gameData.komi || 6.5, // komi depends on handicap stones + player rank
|
}
|
||||||
this.handicap = gameData.handicap || 0,
|
|
||||||
this.boardSize = gameData.boardSize || 19,
|
const pipeMap = (...funcs) => obj => {
|
||||||
this.groups = {},
|
const arr = Object.entries(obj).reduce((acc, [key, value], i, arr) => {
|
||||||
this.boardState = [],
|
funcs.forEach(func => value = func(value, i, arr));
|
||||||
this.gameRecord = gameRecord || [],
|
return [...acc, [key, value]];
|
||||||
this.playerState = gameData.playerState || {
|
},[]);
|
||||||
|
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,
|
bCaptures: 0,
|
||||||
wCaptures: 0,
|
wCaptures: 0,
|
||||||
bScore: 0,
|
bScore: 0,
|
||||||
wScore: 0
|
wScore: 0
|
||||||
}
|
},
|
||||||
}
|
|
||||||
|
|
||||||
initGame = () => {
|
initGame: function() {
|
||||||
this.winner = null;
|
this.winner = null;
|
||||||
this.pass = null;
|
this.pass = 0;
|
||||||
this.turn = this.handicap ? -1 : 1;
|
this.turn = 1;
|
||||||
|
this.boardState = initBoard(this);
|
||||||
|
this.boardState = getBoardState(this);
|
||||||
|
this.legalMoves = getLegalMoves(this)
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
this.initBoard();
|
addToRecord: function(moveObject) {
|
||||||
return this.getBoardState();
|
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,
|
||||||
|
boardSize: this.boardSize,
|
||||||
|
handicap: this.handicap,
|
||||||
|
komi: this.komi
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
initBoard = () => {
|
// add current point to global group and override current group
|
||||||
let i = 0;
|
Game.groups[point.group].stones.add(this);
|
||||||
while (i < this.boardSize * this.boardSize) {
|
if (this.group !== point.group) {
|
||||||
let point = new Point( Math.floor(i / this.boardSize) + 1, i % this.boardSize + 1, this)
|
this.group = point.group;
|
||||||
this.boardState.push(point);
|
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
this.initHandi();
|
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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return Game;
|
||||||
|
},
|
||||||
|
|
||||||
getBoardState = () => {
|
setLiberties: function(Game) {
|
||||||
this.boardState.forEach(point => point.legal = checkLegal(point, this))
|
const neighbors = getNeighbors({ point: this, Game });
|
||||||
return this.boardState.reduce((boardState, point) => {
|
const liberties = Game.groups[this.group].liberties;
|
||||||
boardState[`${point.pos[0]}-${point.pos[1]}`] = point.legal || point.stone;
|
// if point is occupied remove it from liberties set of point group, else add it
|
||||||
return boardState;
|
neighbors.forEach(neighbor => {
|
||||||
}, {})
|
if (neighbor.stone !== 0) {
|
||||||
|
liberties.delete(neighbor);
|
||||||
|
Game.groups[neighbor.group].liberties.delete(this);
|
||||||
}
|
}
|
||||||
|
if (neighbor.stone === 0) {
|
||||||
getMeta = () => {
|
liberties.add(neighbor)
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
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
|
||||||
function clearKo(Game) {
|
const neighbors = getNeighbors({point: this, Game: game}).filter(neighbor => neighbor.stone === -1 * this.stone)
|
||||||
for (let point in Game.boardState) {
|
neighbors.forEach( neighbor => {
|
||||||
point = Game.boardState[point];
|
const liberties = game.groups[neighbor.group] && game.groups[neighbor.group].liberties;
|
||||||
point.stone = point.stone === 'k' ? 0 : point.stone;
|
if (liberties && liberties.size === 1) {
|
||||||
}
|
const lastLiberty = getSingleItemFromSet(liberties);
|
||||||
}
|
lastLiberty.capturing[neighbor.stone * -1].add(neighbor.group);
|
||||||
|
|
||||||
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 => {
|
return game;
|
||||||
pt.territory = pt.stone * -1;
|
},
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function emptyPointSetTerritory(emptyPoints) {
|
makeCaptures: function(game) {
|
||||||
emptyPoints.filter(pt => !pt.territory && pt.checkNeighbors().filter(nbr => nbr.stone !== 0))
|
// for each group
|
||||||
.forEach(pt => {
|
for (let [captureGroup, _] of this.capturing[this.stone].entries()) {
|
||||||
let b = pt.groupMembers.reduce((acc, grpMem) => {
|
|
||||||
let bNbr = grpMem.checkNeighbors().filter(nbr => nbr.stone === 1).length;
|
const capturesSet = game.groups[captureGroup].stones;
|
||||||
return acc + bNbr;
|
for (let [capture, _] of capturesSet.entries()) {
|
||||||
}, 0);
|
game = capture.removeStone(game);
|
||||||
let w = pt.groupMembers.reduce((acc, grpMem) => {
|
if (capturesSet.size === 1) {
|
||||||
let wNbr = grpMem.checkNeighbors().filter(nbr => nbr.stone === -1).length;
|
const neighbors = getNeighbors({ point: this, Game: game })
|
||||||
return acc + wNbr;
|
const liberties = neighbors.filter(neighbor => neighbor.stone === 0);
|
||||||
}, 0);
|
const groupStones = neighbors.filter(neighbor => neighbor.stone === this.stone);
|
||||||
pt.groupMembers.forEach(grp => {
|
if (liberties.length === 1 && groupStones.length === 0) {
|
||||||
if (Math.abs(b - w) < 4 && b && w) grp.territory = 'd'
|
capture.ko = true;
|
||||||
else grp.territory = b > w ? 1 : -1;
|
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 = {
|
module.exports = {
|
||||||
Game
|
Game,
|
||||||
|
Point
|
||||||
}
|
}
|
422
packages/server/services/Game.v1.js
Normal file
422
packages/server/services/Game.v1.js
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -3,22 +3,27 @@ const Game = require('./Game').Game;
|
||||||
const gamesInProgress = { }
|
const gamesInProgress = { }
|
||||||
|
|
||||||
const storeGame = (game) => {
|
const storeGame = (game) => {
|
||||||
gamesInProgress[game.id] = new Game(game);
|
gamesInProgress[game.id] = Game(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
const initGame = (game) => {
|
const initGame = ({id, gameRecord = [], ...gameData}) => {
|
||||||
gamesInProgress[game.id] = new Game(game)
|
gamesInProgress[id] = Game({ gameData, gameRecord })
|
||||||
return gamesInProgress[game.id].initGame();
|
gamesInProgress[id].initGame();
|
||||||
|
return getDataForUI(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeMove = (game, move) => {
|
const makeMove = ({id, move}) => {
|
||||||
if (!gamesInProgress[game.id]) initGame(game);
|
if (!gamesInProgress[id]) return { message: 'no game'};
|
||||||
const newState = gamesInProgress[game.id].makeMove(move);
|
gamesInProgress[id] = gamesInProgress[id].makeMove(move)
|
||||||
return {...newState}
|
if (gamesInProgress[id].success === false) return { message: 'illegal move' };
|
||||||
|
return getDataForUI(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getBoard = (gameId) => {
|
const getDataForUI = (id) => {
|
||||||
return gamesInProgress[gameId].getBoardState();
|
return {
|
||||||
|
board: gamesInProgress[id].legalMoves,
|
||||||
|
...gamesInProgress[id].getMeta()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAllGames = () => {
|
const getAllGames = () => {
|
||||||
|
@ -28,6 +33,6 @@ const getAllGames = () => {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
makeMove,
|
makeMove,
|
||||||
getAllGames,
|
getAllGames,
|
||||||
getBoard,
|
getDataForUI,
|
||||||
initGame
|
initGame
|
||||||
}
|
}
|
|
@ -1,9 +1,540 @@
|
||||||
const chai = require('chai');
|
const chai = require('chai');
|
||||||
const should = chai.should();
|
const should = chai.should();
|
||||||
const Game = require('../services/Game');
|
const { Game, Point } = require('../services/Game');
|
||||||
|
|
||||||
describe('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();
|
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'
|
||||||
|
};
|
||||||
|
|
|
@ -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'
|
|
||||||
};
|
|
|
@ -4,34 +4,37 @@ const gameServices = require('../services/gameServices');
|
||||||
|
|
||||||
describe('game services', () => {
|
describe('game services', () => {
|
||||||
it('init game returns game board', done => {
|
it('init game returns game board', done => {
|
||||||
gameServices.initGame({id: 1, handicap: 4})
|
gameServices.initGame({ id: 1, handicap: 4 })
|
||||||
gameServices.getBoard(1).should.eql(fourHandicapBoard)
|
.board.should.eql(fourHandicapBoard)
|
||||||
done();
|
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 => {
|
it('games services places move', done => {
|
||||||
gameServices.initGame({id: 1, handicap: 4})
|
gameServices.initGame({ id: 1, handicap: 4 })
|
||||||
const afterMoveOne = gameServices.makeMove({id: 1}, {player: 'white', pos: { x:6, y:3 }});
|
const move = { player: 'white', pos: { x: 6, y: 3 } }
|
||||||
const afterMoveOneShould = { board:{ ...fourHandicapBoard, '6-3': -1}, meta: moveOneMeta };
|
const afterMove = gameServices.makeMove({ id: 1, move });
|
||||||
afterMoveOne.should.eql(afterMoveOneShould);
|
const afterMoveShould = { board: { ...fourHandicapBoard, '6-3': -1}, ...initialMeta, handicap: 4, turn: 1, gameRecord: [ move ] };
|
||||||
|
afterMove.should.eql(afterMoveShould);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('illegal move throws error', done => {
|
it('illegal move returns error message', done => {
|
||||||
try {
|
gameServices.initGame({ id: 1, handicap: 4 });
|
||||||
gameServices.initGame({id: 1, handicap: 4})
|
gameServices.makeMove({ id: 1, move: { player: 'white', pos: { x:4, y:4 } } })
|
||||||
const afterIllegalMove = gameServices.makeMove({id: 1}, {player: 'white', pos: { x:4, y:4 }});
|
.message.should.equal('illegal move');
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
err.message.should.equal('illegal move')
|
|
||||||
done();
|
done();
|
||||||
}
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('game services places move next to stone', done => {
|
it('game services places move next to stone', done => {
|
||||||
gameServices.initGame({ id: 1, handicap:4 });
|
gameServices.initGame({ id: 1, handicap:4 });
|
||||||
const afterMoveOne = gameServices.makeMove({ id: 1 }, { player: 'white', pos: { x: 4, y: 3 } });
|
gameServices.makeMove({ id: 1, move: { player: 'white', pos: { x: 4, y: 3 } } })
|
||||||
afterMoveOne.should.not.eql(fourHandicapBoard);
|
.board.should.eql({ ...fourHandicapBoard, '4-3': -1 });
|
||||||
done();
|
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'
|
'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 = {
|
const initialMeta = {
|
||||||
gameRecord: [
|
winner: null,
|
||||||
{player: 'white', pos: { x:6, y:3 }}
|
turn: 0,
|
||||||
],
|
|
||||||
pass: 0,
|
pass: 0,
|
||||||
|
komi: 6.5,
|
||||||
|
handicap: 0,
|
||||||
|
boardSize: 19,
|
||||||
playerState: {
|
playerState: {
|
||||||
bCaptures: 0,
|
bCaptures: 0,
|
||||||
bScore: 0,
|
|
||||||
wCaptures: 0,
|
wCaptures: 0,
|
||||||
|
bScore: 0,
|
||||||
wScore: 0
|
wScore: 0
|
||||||
},
|
},
|
||||||
turn: 1,
|
gameRecord: []
|
||||||
winner: null
|
|
||||||
}
|
}
|
Loading…
Reference in a new issue