UNPKG

othello.js

Version:

An simple easy-to-use othello game implementation with TypeScript.

345 lines (344 loc) 12.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OthelloBoardManager = void 0; const cellstate_1 = require("./cellstate"); const stonestate_1 = require("./stonestate"); /* 初期状態 0 1 2 3 4 5 6 7 0 1 2 3 o x 4 x o 5 6 7 */ /** * Represents the othello board and a manager of it. */ class OthelloBoardManager { /** * Initialize the board manager. * @param _config the game config of the parent game. */ constructor(_config) { this._config = _config; this._data = null; this._log = []; this.init(); } /** * The log of turns */ get putLog() { return this._log; } init() { this._data = [...Array(8)].map((_, x) => [...Array(8)].map((_, y) => new cellstate_1.CellStatus("none", x, y))); this .setCell("black", { x: 3, y: 3 }, { x: 4, y: 4 }) .setCell("white", { x: 3, y: 4 }, { x: 4, y: 3 }); } /** * Returns the status of the specific stone. * @param type * @returns */ getInfo(type) { return new stonestate_1.StoneStatus(this.sumCell(cell => cell.type === type)); } /** * Represents the stone type of the next turn. */ get nextStone() { return this.getLastMover() === "black" ? "white" : "black"; } get log() { return this._log; } /** * Puts a stone * DO NOT use this method directly. You should call put method of the game class. * If not in dry-run and failed, will throw an error. * @param config the config of this action. * @param dryrun If you only check the result of put, true, otherwise false. * @returns the result of the action. If in dry-run and failed, false, otherwise, the result object. * @internal This method cannot use directly by user. */ put(config, dryrun = false) { if (this.log[this.log.length - 1] && this.log[this.log.length - 1].winner) { if (dryrun) { return false; } throw new Error("the game has already finished"); } let winner = null; const modified = []; if (config.type === "put") { const target = this.getCell(config.x, config.y); if (!dryrun && this.getLastMover() === config.current) { // 2ユーザー連続 throw new Error("two consecutive mover"); } else if (target.type !== "none") { // すでに置かれているセル if (dryrun) return false; throw new Error("the cell has already been put a stone on"); } const arrounds = this.getAroundCells(config.x, config.y); if (arrounds.length === 0) { // まわりに石がないセル if (dryrun) return false; throw new Error("the cell surrounded by no stone"); } else if (arrounds.filter(c => c.type !== target.type).length === 0) { // まわりに種類の異なる(裏されうる)石がないセル if (dryrun) return false; throw new Error("the cell surrounded by no stone of the other type"); } const turnCells = { row: false, column: false, plusdiagonal: false, minusdiagonal: false }; const cellsCache = { row: null, column: null, plusdiagonal: null, minusdiagonal: null }; // 横方向 const row = cellsCache.row = this.getRow(config.y); turnCells.row = this.replaceCells(config, config.x, row, "plus") || this.replaceCells(config, config.x, row, "minus"); // 縦方向 const column = cellsCache.column = this.getColumn(config.x); turnCells.column = this.replaceCells(config, config.y, column, "plus") || this.replaceCells(config, config.y, column, "minus"); // +斜め方向 const plusdiagonal = cellsCache.plusdiagonal = this.getPlusDiagonals(config); turnCells.plusdiagonal = this.replaceCells(config, plusdiagonal.findIndex(cell => config.x === cell.x), plusdiagonal, "plus") || this.replaceCells(config, plusdiagonal.findIndex(cell => config.x === cell.x), plusdiagonal, "minus"); // -斜め方向 const minusdiagonal = cellsCache.minusdiagonal = this.getMinusDiagnals(config); turnCells.minusdiagonal = this.replaceCells(config, minusdiagonal.findIndex(cell => config.x === cell.x), minusdiagonal, "plus") || this.replaceCells(config, minusdiagonal.findIndex(cell => config.x === cell.x), minusdiagonal, "minus"); // 全体で裏返す部分あるか const existsTurnCell = turnCells.row || turnCells.column || turnCells.plusdiagonal || turnCells.minusdiagonal; // なければ if (!existsTurnCell) { if (dryrun) return false; throw new Error("no cell to turn"); } // 裏返しを反映 if (!dryrun) { ["row", "column", "plusdiagonal", "minusdiagonal"].forEach(direction => { if (turnCells[direction]) { cellsCache[direction].forEach(cell => { if (this.getCell(cell.x, cell.y) !== cell) { this._data[cell.x][cell.y] = cell; modified.push(cell); } }); } }); this.setCell(config.current, config); } } if (this.sumCell(cell => cell.type === "none") === 0 || (!dryrun && this.getAbleToPut("black").length + this.getAbleToPut("white").length === 0)) { const white = this.sumCell(cell => cell.type === "white"); const black = this.sumCell(cell => cell.type === "black"); winner = white > black ? "white" : white === black ? "draw" : white < black ? "black" : null; } const result = Object.assign(Object.assign({}, config), { winner, modified }); if (!dryrun) { this._log.push(result); } return result; } /** * Recover the board from the log. */ reset(log) { this.init(); log.forEach(l => { if (l.type === "pass") { this.put({ type: l.type, current: l.current, }); } else { this.put({ type: l.type, current: l.current, x: l.x, y: l.y }); } }); } setCell(type, ...positions) { positions.forEach(p => { this._data[p.x][p.y] = new cellstate_1.CellStatus(type, p.x, p.y); }); return this; } /** * Returns the cell status of the specified coordinate. * @param x x-coordinate of the cell you'd like to know. * @param y y-coordinate of the cell you'd like to know. * @returns the cell status of the specified coordinate. */ getCell(x, y) { return Object.assign(new cellstate_1.CellStatus(), this._data[x][y]); } getAroundCells(x, y) { const cells = []; const columns = [this._data[x - 1], this._data[x], this._data[x + 1]].filter(col => Boolean(col)); columns.forEach(col => { cells.push(...([col[y - 1], col[y], col[y + 1]].filter(cel => Boolean(cel)))); }); return cells; } /** * Returns the all cell coordinates you can put on. * @param current the current turn. * @returns the array of the complete list of the cells you can put on. */ getAbleToPut(current) { const points = []; for (let x = 0; x < 8; x++) { for (let y = 0; y < 8; y++) { if (this.put({ type: "put", current, x, y }, /* dryrun */ true)) { points.push({ x, y }); } } } return points; } /** * Returns a array of the complete list of the cell on the column the specified x-coordinate. * @param x x-coordinate. * @returns a array of the complete list of the cell on the column the specified x-coordinate. */ getColumn(x) { return [...this._data[x]]; } /** * Returns a array of the complete list of the cell on the column the specified y-coordinate. * @param y y-coordinate. * @returns a array of the complete list of the cell on the column the specified y-coordinate. */ getRow(y) { return [...Array(8)].map((_, i) => this._data[i][y]); } getPlusDiagonals(center) { const intercept = center.x + center.y; const result = []; for (let i = 0; i <= 7; i++) { const column = this.getColumn(i); if (column) { result.push(column[intercept - i]); } } return result.filter(cell => Boolean(cell)); } getMinusDiagnals(center) { const intercept = center.x - center.y; const result = []; for (let i = 0; i <= 7; i++) { const column = this.getColumn(i); if (column) { result.push(column[i - intercept]); } } return result.filter(cell => Boolean(cell)); } getLastMover() { if (this._log.length > 0) { return this._log[this._log.length - 1].current; } else { return this._config.firstMove === "black" ? "white" : "black"; } } sumCell(validator) { let result = 0; this._data.forEach(column => { column.forEach(row => { if (validator(row)) result++; }); }); return result; } replaceCells(config, center, target, direction) { if (target.length === center + 1 && direction === "plus" || center === 0 && direction === "minus") { return false; } if (direction === "plus") { let start = -1; let end = -1; for (let i = center + 1; i < target.length; i++) { if (target[i].type === "none") break; if (target[i].type === config.current) break; if (start === -1) { start = i; end = i; } else if (end + 1 === i) { end = i; } else { break; } } if (start === -1 || !target[end + 1] || target[end + 1].type !== config.current) { return false; } for (let i = start; i <= end; i++) { target[i] = new cellstate_1.CellStatus(config.current, target[i].x, target[i].y); } return true; } else { let start = -1; let end = -1; for (let i = center - 1; i >= 0; i--) { if (target[i].type === "none") break; if (target[i].type === config.current) break; if (start === -1) { start = i; end = i; } else if (end - 1 === i) { end = i; } else { break; } } if (start === -1 || !target[end - 1] || target[end - 1].type !== config.current) { return false; } for (let i = start; i >= end; i--) { target[i] = new cellstate_1.CellStatus(config.current, target[i].x, target[i].y); } return true; } } } exports.OthelloBoardManager = OthelloBoardManager;