UNPKG

godash

Version:

Data structures and utilities to represent the game of Go

1,032 lines (965 loc) 25.8 kB
/** * Utilities for an immutable Board object. * * @module Board */ /** * @external Record * @see https://immutable-js.github.io/immutable-js/docs/#/Record */ /** * @external Set */ import { Set, Map, List, Record, } from 'immutable'; import concat from 'lodash/concat'; import flatMap from 'lodash/flatMap'; import hasIn from 'lodash/hasIn'; import inRange from 'lodash/inRange'; import isInteger from 'lodash/isInteger'; import take from 'lodash/take'; /** * The color black. * * @type {string} */ export const BLACK = 'black'; /** * The color white. * * @type {string} */ export const WHITE = 'white'; /** An empty space. */ export const EMPTY = null; /** * ```javascript * godash.Board(dimensions = 19, ...moves) * ``` * * Representation of a board position. * * Extends [`Immutable.Record`][imm-record]. * * [imm-record]: https://immutable-js.github.io/immutable-js/docs/#/Record * * !!! tldr "Constructor Arguments" * * `dimensions` `(number)` - Size of the board, defaulted to 19. * * `...moves` `(Move)` - Moves to be placed on the board. * * !!! tldr "Properties" * * `dimensions` `(number)` - Size of the board. * * `moves` [`(Immutable.Map)`][imm-map] - Stones present on this board. * [`Coordinate`](#coordinate) keys with either [`BLACK`](#black) or * [`WHITE`](#white) values. * * [imm-map]: https://immutable-js.github.io/immutable-js/docs/#/Map * * ??? example "Examples" * ```javascript * var board = Board(); * * board.toString(); * // => Board { "dimensions": 19, "moves": Map {} } * ``` * * ```javascript * var smallBoard = Board(5, Move(Coordinate(2, 2), BLACK)); * * smallBoard.toString(); * // => Board { "dimensions": 5, "moves": Map { {"x":2,"y":2}: "black" } } * ``` * * ```javascript * var smallBoard = Board(5, Move(Coordinate(2, 2), BLACK)); * * smallBoard.toString(); * // => Board { "dimensions": 5, "moves": Map { {"x":2,"y":2}: "black" } } * ``` * * @extends Record */ class _Board extends Record({dimensions: 19, moves: Map()}, 'Board') { constructor(dimensions = 19, ...moves) { super({ dimensions, moves: Map.of(...flatMap( moves.map(move => [move.coordinate, move.color]) )), }); } toMap(overrides = null) { const defaultMap = { [BLACK]: BLACK, [WHITE]: WHITE, [EMPTY]: EMPTY, }; const stoneMap = overrides || defaultMap; return Array(this.dimensions).fill(EMPTY).map( (_, row) => Array(this.dimensions).fill(EMPTY).map( (_, col) => { const color = this.moves.get(Coordinate(col, row), EMPTY); return stoneMap[color] || defaultMap[color]; } ) ); } } export function Board(...args) { return Reflect.construct(_Board, args); } Board.prototype = _Board.prototype; /** * ```javascript * godash.Coordinate(x, y) * ``` * * A zero-based tuple representing a single location on a Go board. * * Extends [`Immutable.Record`][imm-record]. * * [imm-record]: https://immutable-js.github.io/immutable-js/docs/#/Record * * !!! tldr "Constructor Arguments" * * `x` `(number)` - Location on the X-axis. * * `y` `(number)` - Location on the Y-axis. * * !!! tldr "Properties" * * `x` `(number)` - Location on the X-axis. * * `y` `(number)` - Location on the Y-axis. * * ??? example "Examples" * ```javascript * var tengen = Coordinate(9, 9); * * tengen.toString(); * // => Coordinate { "x": 9, "y": 9 } * * tengen.x; * // => 9 * ``` * * @extends Record */ class _Coordinate extends Record({x: 0, y: 0}, 'Coordinate') { constructor(x, y) { super({x, y}); } } export function Coordinate(...args) { return Reflect.construct(_Coordinate, args); } Coordinate.prototype = _Coordinate.prototype; /** * ```javascript * godash.Move(coordinate, color) * ``` * * Representation of a move, composed of a [`Coordinate`](#coordinate) and a * color. * * Extends [`Immutable.Record`][imm-record]. * * [imm-record]: https://immutable-js.github.io/immutable-js/docs/#/Record * * !!! tldr "Constructor Arguments" * * `coordinate` ([`Coordinate`](#coordinate)) - Location of the move. * * `color` `(string)` - [`BLACK`](#black), [`WHITE`](#white), or * [`EMPTY`](#empty). * * !!! tldr "Properties" * * `coordinate` ([`Coordinate`](#coordinate)) - Location of the move. * * `color` `(string)` - [`BLACK`](#black), [`WHITE`](#white), or * [`EMPTY`](#empty). * * ??? example "Examples" * ```javascript * var tengen = Move(Coordinate(9, 9), BLACK); * ``` * * @extends Record */ class _Move extends Record({coordinate: Coordinate(), color: EMPTY}, 'Move') { constructor(coordinate, color) { super({coordinate, color}); } } export function Move(...args) { return Reflect.construct(_Move, args); } Move.prototype = _Move.prototype; /** * Center point on a 9x9 board. * * @type {Coordinate} */ export const TENGEN_9 = Coordinate(4, 4); /** * Center point on a 13x13 board. * * @type {Coordinate} */ export const TENGEN_13 = Coordinate(6, 6); /** * Center point on a 19x19 board. * * @type {Coordinate} */ export const TENGEN_19 = Coordinate(9, 9); export function adjacentCoordinates(board, coordinate) { const {x, y} = coordinate; const validRange = n => inRange(n, board.dimensions); return Set.of( Coordinate(x, y + 1), Coordinate(x, y - 1), Coordinate(x + 1, y), Coordinate(x - 1, y), ).filter(c => validRange(c.x) && validRange(c.y)); } /** * Finds the moves on the first board that are not on the second board. * * @example * var atari = Board(3, * Move(Coordinate(1, 0), BLACK), * Move(Coordinate(0, 1), BLACK), * Move(Coordinate(1, 2), BLACK), * Move(Coordinate(1, 1), WHITE), * ); * * toAsciiBoard(atari); * // => +O+ * // OXO * // +++ * * var captured = difference( * atari, addMove(atari, Move(Coordinate(2, 1), BLACK)) * ); * * captured.toString(); * // 'Set { List [ Coordinate { "x": 1, "y": 1 }, "white" ] }' * * @param {Board} board1 - First board. * @param {Board} board2 - Board with moves to subtract from first board. * @returns {Set} Set containing pairs of `Coordinate` and color remaining in * the difference. */ export function difference(board1, board2) { if (board1.dimensions !== board2.dimensions) { throw new Error('board sizes do not match'); } const board1Set = Set(board1.moves.entrySeq().map(a => List(a))); const board2Set = Set(board2.moves.entrySeq().map(a => List(a))); return board1Set.subtract(board2Set); } /** * Determines move that would be illegal under the [ko * rule](https://en.wikipedia.org/wiki/Rules_of_go#Ko_and_Superko) * * @example * const koPosition = Board(4, * Move(Coordinate(1, 0), BLACK), * Move(Coordinate(0, 1), BLACK), * Move(Coordinate(1, 2), BLACK), * Move(Coordinate(1, 1), WHITE), * Move(Coordinate(2, 0), WHITE), * Move(Coordinate(2, 2), WHITE), * Move(Coordinate(3, 1), WHITE), * ); * * toAsciiBoard(koPosition); * // => +O++ * // OXO+ * // X+X+ * // +X++ * * const koStart = Coordinate(2, 1); * * followupKo(koPosition, koStart, BLACK).toString(); * // => 'Coordinate { "x": 1, "y": 1 }' * * @param {Board} board - Starting board. * @param {Move} move - Intended `Move`. * @returns {Coordinate} Position of illegal followup or `null` if * none exists. */ export function followupKo(board, move) { if (!isLegalMove(board, move)) { return null; } const postMoveBoard = addMove(board, move); const capturedStones = difference(board, postMoveBoard); if (capturedStones.size === 0 || capturedStones.size > 1) { return null; } const capturedMove = capturedStones.first(); const capturedCoordinate = capturedMove.get(0); const capturedColor = capturedMove.get(1); // The situation is ko if the board returns to the original state. if (isLegalMove(postMoveBoard, Move(capturedCoordinate, capturedColor))) { if (board.equals( addMove(postMoveBoard, Move(capturedCoordinate, capturedColor)) )) { return capturedCoordinate; } } return null; } export function matchingAdjacentCoordinates(board, coordinate, color) { const colorToMatch = color === undefined ? board.moves.get(coordinate, EMPTY) : color; return adjacentCoordinates(board, coordinate) .filter(c => board.moves.get(c, EMPTY) === colorToMatch); } /** * Finds the set of coordinates which identifies the fully connected group for * the given location. * * @example * var board = Board(3, * Move(Coordinate(1, 0), WHITE), * Move(Coordinate(0, 2), WHITE), * Move(Coordinate(1, 2), BLACK), * Move(Coordinate(2, 2), BLACK), * Move(Coordinate(2, 1), BLACK), * ); * * toAsciiBoard(board); * // => ++X * // X+O * // +OO * * group(board, Coordinate(2, 1)).toString(); * // => Set { * // Coordinate { "x": 2, "y": 1 }, * // Coordinate { "x": 2, "y": 2 }, * // Coordinate { "x": 1, "y": 2 } * // } * * @param {Board} board - Board to inspect. * @param {Coordinate} coordinate - Location to inspect. * @returns {Set} Containing `Coordinate` for the members of the group. */ export function group(board, coordinate) { let found = Set(); let queue = Set.of(coordinate); while (!queue.isEmpty()) { const current = queue.first(); const more_matching = matchingAdjacentCoordinates(board, current); found = found.add(current); queue = queue.rest().union(more_matching.subtract(found)); } return found; } /** * Toggles the passed color. * * @example * oppositeColor(BLACK) === WHITE * // => true * * oppositeColor(WHITE) === BLACK * // => true * * @param {string} color - `godash.BLACK` or `godash.WHITE` * @return {string} Color opposite of the one provided. */ export function oppositeColor(color) { if (color === BLACK) { return WHITE; } else if (color === WHITE) { return BLACK; } else { return EMPTY; } } /** * Finds the set of all liberties for the given coordinate. If the coordinate * is part of a group, the set of liberties are the liberties for that group. * * @example * var board = Board(3, Move(Coordinate(1, 1), BLACK)); * var collectedLiberties = liberties(board, Coordinate(1, 1)); * * Immutable.Set.of( * Coordinate(1, 0), * Coordinate(0, 1), * Coordinate(1, 2), * Coordinate(2, 1) * ).equals(collectedLiberties); * // => true * * @param {Board} board - Board to inspect. * @param {Coordinate} coordinate - Coordinate to inspect. * @return {Set} Containing `Coordinate` members for the liberties of the * passed coordinate. */ export function liberties(board, coordinate) { return group(board, coordinate).reduce( (acc, coord) => acc.union( matchingAdjacentCoordinates(board, coord, EMPTY), ), Set(), ); } /** * Counts the liberties for the given coordinate. If the coordinate is part of * a group, liberties for the entire group is counted. * * @example * var board = Board(3, Coordinate(1, 1), BLACK); * * libertyCount(board, Coordinate(1, 1)) === 4; * // => true * * @param {Board} board - Board to inspect. * @param {Coordinate} coordinate - Coordinate to inspect. * @return {number} Count of liberties for the coordinate on the board. */ export function libertyCount(board, coordinate) { return liberties(board, coordinate).size; } /** * Determine whether the coordinate-color combination provided is a legal move * for the board. [Ko][ko-rule] is not considered. Use `followupKo` if you * want to do [ko][ko-rule]-related things. * * [ko-rule]: https://en.wikipedia.org/wiki/Rules_of_go#Ko_and_Superko * * @example * var ponnuki = Board(3, * Move(Coordinate(1, 0), BLACK), * Move(Coordinate(0, 1), BLACK), * Move(Coordinate(1, 2), BLACK), * Move(Coordinate(2, 1), BLACK), * ); * * toAsciiBoard(ponnuki); * // => +O+ * // O+O * // +O+ * * isLegalMove(ponnuki, Move(Coordinate(1, 1), BLACK)) * // => true * * isLegalMove(ponnuki, Move(Coordinate(1, 1), WHITE)) * // => false * * @param {Board} board - Board to inspect. * @param {Move} move - Move to check. * @return {boolean} Whether the move is legal. */ export function isLegalMove(board, move) { const willHaveLiberties = libertyCount( board.setIn(['moves', move.coordinate], move.color), move.coordinate, ) > 0; const willKillSomething = matchingAdjacentCoordinates( board, move.coordinate, oppositeColor(move.color) ).some(coord => libertyCount(board, coord) === 1); return willHaveLiberties || willKillSomething; } /** * Partial application of `isLegalMove`, fixing the color to `BLACK`. * * @param {Board} board - Board to inspect. * @param {Coordinate} coordinate - Location to check. * @return {boolean} Whether the move is legal. */ export function isLegalBlackMove(board, coordinate) { return isLegalMove(board, Move(coordinate, BLACK)); } /** * Partial application of `isLegalMove`, fixing the color to `WHITE`. * * @param {Board} board - Board to inspect. * @param {Coordinate} coordinate - Location to check. * @return {boolean} Whether the move is legal. */ export function isLegalWhiteMove(board, coordinate) { return isLegalMove(board, Move(coordinate, WHITE)); } /** * Make a given coordinate empty on the board. * * @example * var board = Board(3, Move(Coordinate(1, 1), WHITE)); * * toAsciiBoard(board); * // => +++ * // +X+ * // +++ * * toAsciiBoard( * removeStone(board, Coordinate(1, 1)) * ); * // => +++ * // +++ * // +++ * * @param {Board} board - Board from which to remove the stone. * @param {Coordinate} coordinate - Location of the stone. * @return {Board} New board with the stone removed. */ export function removeStone(board, coordinate) { return board.set('moves', board.moves.delete(coordinate)); } /** * Makes several coordinates empty on the board. * * @example * var board = Board(3, * Move(Coordinate(1, 0), WHITE), * Move(Coordinate(1, 1), WHITE), * Move(Coordinate(1, 2), BLACK), * ); * * toAsciiBoard(board); * // => +++ * // XXO * // +++ * * toAsciiBoard( * removeStones(board, [ * Coordinate(1, 1), * Coordinate(1, 2) * ]) * ); * // => +++ * // X++ * // +++ * * @param {Board} board - Board from which to remove the stone. * @param {Coordinate} coordinates - Location of the stones. * @return {Board} New board with the stones removed. */ export function removeStones(board, coordinates) { return board.setIn(['moves'], coordinates.reduce( (acc, coordinate) => acc.delete(coordinate), board.moves, ) ); } /** * Function to add a move onto a board while respecting the rules. Since no * sequence information is available, this function does not respect * [ko][ko-rule]. Use `followupKo` if you want to do [ko][ko-rule]-related * things. * * [ko-rule]: https://en.wikipedia.org/wiki/Rules_of_go#Ko_and_Superko * * @example * var atari = Board(3, * Move(Coordinate(1, 0), BLACK), * Move(Coordinate(0, 1), BLACK), * Move(Coordinate(1, 2), BLACK), * Move(Coordinate(1, 1), WHITE), * ); * * toAsciiBoard(atari); * // => +O+ * // OXO * // +++ * * var killed = addMove( * atari, * Move(Coordinate(2, 1), BLACK) * ); * * toAsciiBoard(killed); * // => +O+ * // O+O * // +O+ * * @param {Board} board - Board from which to add the move. * @param {Move} move - Move or location of the move. * @return {Board} New board with the move played. */ export function addMove(board, move) { if (!isLegalMove(board, move)) { throw new Error('Not a valid position'); } if (board.moves.has(move.coordinate)) { throw new Error('There is already a stone there'); } const killed = matchingAdjacentCoordinates( board, move.coordinate, oppositeColor(move.color) ).reduce( (acc, coord) => acc.union( libertyCount(board, coord) === 1 ? group(board, coord) : Set() ), Set(), ); return removeStones(board, killed).setIn( ['moves', move.coordinate], move.color, ); } /** * Places a stone on the board, ignoring the rules of Go. * * @example * var ponnuki = Board(3, * Move(Coordinate(1, 0), BLACK), * Move(Coordinate(0, 1), BLACK), * Move(Coordinate(1, 2), BLACK), * Move(Coordinate(2, 1), BLACK), * ); * * toAsciiBoard(ponnuki); * // => +O+ * // O+O * // +O+ * * toAsciiBoard( * placeStone(ponnuki, Coordinate(1, 1), WHITE) * ); * // => +O+ * // OXO * // +O+ * * @param {Board} board - Board to add stone. * @param {Coordinate} coordinate - Location to add stone. * @param {string} color - Stone color - `BLACK` or `WHITE`. * @param {boolean} force - Optionally allow placement over existing stones. * @return {Board} New board with the move placed. */ export function placeStone(board, coordinate, color, force = false) { const currentColor = board.moves.get(coordinate, EMPTY); if ((!force) && oppositeColor(currentColor) === color) { throw new Error( 'There is already a stone there. Pass force=true to override.' ); } else { return board.setIn(['moves', coordinate], color); } } /** * Places a set of stones onto the board, ignoring the rules of Go. * * @example * var board = Board(3, Coordinate(1, 1), WHITE); * * toAsciiBoard(board); * // => +++ * // +X+ * // +++ * * toAsciiBoard( * placeStones(board, [ * Coordinate(1, 0), * Coordinate(0, 1), * Coordinate(1, 2), * Coordinate(2, 1) * ], BLACK) * ); * // => +O+ * // OXO * // +O+ * * @param {Board} board - Board to add stones. * @param {Array} coordinates - Stones to place. * @param {string} color - Stone color - `BLACK` or `WHITE`. * @param {boolean} force - Optionally allow placement over existing stones. * @return {Board} New board with the moves placed. */ export function placeStones(board, coordinates, color, force = false) { return coordinates.reduce( (acc, coordinate) => placeStone(acc, coordinate, color, force), board, ); } /** * Constructs an ASCII representation of the board. * * @example * var board = Board(3, * Coordinate(1, 0), BLACK, * Coordinate(0, 1), BLACK, * Coordinate(1, 2), BLACK, * Coordinate(1, 1), WHITE * ); * * toAsciiBoard(board); * // => +O+ * // OXO * // +++ * * @param {Board} board - Board to represent. * @return {string} ASCII representation of the board. */ export function toAsciiBoard(board) { return toString(board); } /** * Constructs a string representation of the board. * * @example * var board = Board(3, * Coordinate(1, 0), BLACK, * Coordinate(0, 1), BLACK, * Coordinate(1, 2), BLACK, * Coordinate(1, 1), WHITE * ); * * toString(board, {[BLACK]: 'B', [WHITE]: 'W"}); * // => +B+ * // BWB * // +++ * * @param {Board} board - Board to represent. * @param {Object} overrides - Overrides print characters, indexed by color * constants. * @return {string} String representation of the board. */ export function toString(board, overrides = null) { const dimensions = board.dimensions; const defaultMap = { [BLACK]: 'O', [WHITE]: 'X', [EMPTY]: '+', }; const colorMap = overrides || defaultMap; let prettyString = ''; for (var i = 0; i < dimensions; i++) { for (var j = 0; j < dimensions; j++) { const color = board.moves.get(Coordinate(i, j), EMPTY); prettyString += colorMap[color] || defaultMap[color]; } prettyString += '\n'; } return prettyString; } const A1_PREFIXES = [ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ]; /** * Construct an A1-style coordinate string from a Coordinate. * * An [A1-style coordinate][a1-style] is given in the form A1 to T19 with I * omitted to avoid confusion with J. * * This function accepts coordinates in the range 0-24, mapping from A-Z. * * [a1-style]: https://senseis.xmp.net/?Coordinates#toc2 * * @param {Coordinate} coordinate - Coordinate to convert. * @return {string} String A1-style coordinate. */ export function toA1Coordinate(coordinate) { if ( coordinate.x < 0 || coordinate.x > 24 || coordinate.y < 0 || coordinate.y > 24 ) { throw new Error('Coordinates must be in [0-24] range'); } return A1_PREFIXES[coordinate.x] + (coordinate.y + 1); } /** * Construct a Coordinate from an A1-style coordinate string. * * An [A1-style coordinate][a1-style] is given in the form A1 to T19 with I * omitted to avoid confusion with J. * * This function accepts raw coordinates from A-Z. * * [a1-style]: https://senseis.xmp.net/?Coordinates#toc2 * * @param {string} raw - A1-style coordinate to convert. * @return {Coordinate} Coordinate representation of A1-style input. */ export function fromA1Coordinate(raw) { raw = raw.toUpperCase(); if (raw.length >= 2) { const xValue = A1_PREFIXES.indexOf(raw.charAt(0)); const y = raw.slice(1); const validY = /^\d{1,2}$/; if (validY.test(y) && xValue >= 0) { const yValue = parseInt(y); if (yValue > 0 && yValue <= 25) { return Coordinate(xValue, yValue - 1); } } } throw new Error('Invalid A1 coordinate'); } /** * Constructs a board for an array of coordinates. This function iteratively * calls `addMove` while alternating colors. * * @example * var tigersMouth = Board(3, * Move(Coordinate(1, 0), BLACK), * Move(Coordinate(0, 1), BLACK), * Move(Coordinate(1, 2), BLACK), * ); * * toAsciiBoard(tigersMouth); * // => +O+ * // O+O * // +++ * * var selfAtari = Coordinate(1, 1); * var killingMove = Coordinate(2, 1); * * var ponnuki = constructBoard( * [selfAtari, killingMove], * tigersMouth, * WHITE * ); * * toAsciiBoard(ponnuki); * // => +O+ * // O+O * // +O+ * * @param {Array} coordinates - Ordered `Coordinate` moves. * @param {Board} board - Optional starting board. Empty 19x19, if omitted. * @param {string} startColor - Optional starting color, defaulted to `BLACK`. * @return {Board} New board constructed from the coordinates. */ export function constructBoard(coordinates, board = null, startColor = BLACK) { if (!board) { board = Board(); } const opposite = oppositeColor(startColor); return coordinates.reduce( (acc, coordinate, index) => { const isCoordinate = coordinate instanceof Coordinate; const hasXY = hasIn(coordinate, 'x') && hasIn(coordinate, 'y'); if (!(isCoordinate || hasXY)) { throw new Error( 'You must pass coordinates or coordinate-like objects.' ); } const checkedCoordinate = isCoordinate ? coordinate : Coordinate(coordinate.x, coordinate.y); return addMove( acc, Move(checkedCoordinate, index % 2 === 0 ? startColor : opposite), ); }, board, ); } /** * Creates a `Board` with the correct number of handicap stones placed. * Only standard board sizes (9, 13, 19) are allowed. * * @example * var board = handicapBoard(9, 4); * * toAsciiBoard(board); * // => +++++++++ * // +++++++++ * // ++O+++O++ * // +++++++++ * // +++++++++ * // +++++++++ * // ++O+++O++ * // +++++++++ * // +++++++++ * * @param {number} size - Size of board, must be 9, 13, or 19. * @param {number} handicap - Number of handicaps, must be 0-9. * @return {Board} New board with handicaps placed. */ export function handicapBoard(size, handicap) { if (size !== 9 && size !== 13 && size !== 19) { throw new Error( 'Only 9, 13, 19 allowed - use placeStone for non standard sizes' ); } if (!inRange(handicap, 0, 10) || !isInteger(handicap)) { throw new Error('Handicap must be an integer between 0 and 9'); } const nonTengenHandicap = { 9: [ Coordinate(2, 2), Coordinate(6, 6), Coordinate(2, 6), Coordinate(6, 2), Coordinate(6, 4), Coordinate(2, 4), Coordinate(4, 2), Coordinate(4, 6), ], 13: [ Coordinate(3, 3), Coordinate(9, 9), Coordinate(3, 9), Coordinate(9, 3), Coordinate(9, 6), Coordinate(3, 6), Coordinate(6, 3), Coordinate(6, 9), ], 19: [ Coordinate(3, 3), Coordinate(15, 15), Coordinate(15, 3), Coordinate(3, 15), Coordinate(15, 9), Coordinate(3, 9), Coordinate(9, 3), Coordinate(9, 15), ], }[size]; const tengen = { 9: TENGEN_9, 13: TENGEN_13, 19: TENGEN_19, }[size]; const board = Board(size); if (handicap < 5) { return placeStones(board, take(nonTengenHandicap, handicap), BLACK); } else if (handicap === 5) { return placeStones( board, concat(take(nonTengenHandicap, 4), tengen), BLACK, ); } else if (handicap === 6) { return placeStones(board, take(nonTengenHandicap, 6), BLACK); } else if (handicap === 8) { return placeStones(board, nonTengenHandicap, BLACK); } else { return placeStones( board, concat(take(nonTengenHandicap, handicap - 1), tengen), BLACK, ); } } export default { BLACK, Board, Coordinate, EMPTY, Move, WHITE, TENGEN_9, TENGEN_13, TENGEN_19, addMove, constructBoard, difference, followupKo, fromA1Coordinate, group, handicapBoard, isLegalMove, liberties, libertyCount, oppositeColor, placeStone, placeStones, removeStone, removeStones, toA1Coordinate, toAsciiBoard, toString, };