export const standardMap = [ "--------", "--------", "--------", "---wb---", "---bw---", "--------", "--------", "--------" ]; export class ReversiEngine { map; mapWidth; mapHeight; board; turn = true; prevColor = null; opts; constructor(map, opts){ this.opts = { isLlotheo: opts.isLlotheo ?? false, canPutEverywhere: opts.canPutEverywhere ?? false, loopedBoard: opts.loopedBoard ?? false }; this.mapWidth = map[0]?.length ?? 0; this.mapHeight = map.length; const data = map.join(""); this.board = data.split("").map((d)=>d === "-" ? null : d === "b" ? true : d === "w" ? false : undefined); this.map = data.split("").map((d)=>d === "-" || d === "b" || d === "w" ? "empty" : "null"); if (!this.canPutSomewhere(true)) this.turn = this.canPutSomewhere(false) ? false : null; } get blackCount() { return this.board.filter((x)=>x === true).length; } get whiteCount() { return this.board.filter((x)=>x === false).length; } posToXy(pos) { return [ pos % this.mapWidth, Math.floor(pos / this.mapWidth) ]; } xyToPos(x, y) { return x + y * this.mapWidth; } mapDataGet(pos) { const [x, y] = this.posToXy(pos); return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight ? "null" : this.map[pos]; } getPuttablePlaces(color) { return Array.from(this.board.keys()).filter((i)=>this.canPut(color, i)); } canPutSomewhere(color) { return this.getPuttablePlaces(color).length > 0; } canPut(color, pos) { if (this.board[pos] !== null) return false; if (this.opts.canPutEverywhere) return this.mapDataGet(pos) === "empty"; return this.effects(color, pos).length !== 0; } effects(color, initPos) { const enemyColor = !color; const diffVectors = [ [ 0, -1 ], [ 1, -1 ], [ 1, 0 ], [ 1, 1 ], [ 0, 1 ], [ -1, 1 ], [ -1, 0 ], [ -1, -1 ] ]; return diffVectors.flatMap(([dx, dy])=>{ const found = []; let [x, y] = this.posToXy(initPos); while(true){ x += dx; y += dy; if (this.opts.loopedBoard && this.xyToPos(x = (x % this.mapWidth + this.mapWidth) % this.mapWidth, y = (y % this.mapHeight + this.mapHeight) % this.mapHeight) === initPos) { return found; } if (x === -1 || y === -1 || x === this.mapWidth || y === this.mapHeight) return []; const pos = this.xyToPos(x, y); if (this.mapDataGet(pos) === "null") return []; const stone = this.board[pos]; if (stone === null) return []; if (stone === enemyColor) found.push(pos); if (stone === color) return found; } }); } putStone(pos) { const color = this.turn; if (color == null) return; this.board[pos] = color; for (const effect of this.effects(color, pos)){ this.board[effect] = color; } this.prevColor = color; this.turn = this.canPutSomewhere(!color) ? !color : this.canPutSomewhere(color) ? color : null; } get isEnded() { return this.turn == null; } get winner() { if (!this.isEnded) return null; if (this.blackCount === this.whiteCount) return null; return this.opts.isLlotheo === this.blackCount > this.whiteCount ? false : true; } calcCrc32() { let hash = 0; const text = JSON.stringify({ board: this.board, turn: this.turn }); for(let i = 0; i < text.length; i++){ hash = hash * 31 + text.charCodeAt(i) | 0; } return hash.toString(); } } export function serializeLogs(logs) { const serialized = []; for(let i = 0; i < logs.length; i++){ const log = logs[i]; const timeDelta = i === 0 ? log.time : log.time - logs[i - 1].time; serialized.push([ timeDelta, log.player ? 1 : 0, 0, log.pos ]); } return serialized; } export function deserializeLogs(logs) { const deserialized = []; let time = 0; for (const log of logs){ time += log[0]; if (log[2] === 0) { deserialized.push({ time, player: log[1] === 1, operation: "put", pos: log[3] }); } } return deserialized; } export function restoreGame(env) { const game = new ReversiEngine(env.map, env); for (const log of deserializeLogs(env.logs)){ if (log.operation === "put") game.putStone(log.pos); } return game; }