UNPKG

tps-ninja

Version:

Generate images from Tak Positional System (TPS) strings

457 lines (417 loc) 12.4 kB
import { Square, atoi } from "./Square.js"; import { Piece } from "./Piece.js"; import { Ply } from "./Ply.js"; import { findRoads } from "./Roads.js"; import { cloneDeep, isNumber, isString, times, zip } from "lodash-es"; const pieceCounts = { 3: { flat: 10, cap: 0 }, 4: { flat: 15, cap: 0 }, 5: { flat: 21, cap: 1 }, 6: { flat: 30, cap: 1 }, 7: { flat: 40, cap: 2 }, 8: { flat: 50, cap: 2 }, }; export const parseTPS = function (tps) { const matchData = tps .toUpperCase() .match(/^([X1-8SC,/-]+)\s+([12])\s+(\d+)$/); const result = {}; if (!matchData) { result.error = "Invalid TPS notation"; return result; } [, result.grid, result.player, result.linenum] = matchData; result.grid = result.grid .replace(/X(\d+)/g, (x, count) => { const spaces = ["X"]; while (spaces.length < count) { spaces.push("X"); } return spaces.join(","); }) .split(/[/-]/) .reverse() .map((row) => row.split(",")); result.size = result.grid.length; result.player = Number(result.player); result.linenum = Number(result.linenum); const validCell = /^(X|[12]+[SC]?)$/; if ( result.grid.find( (row) => row.length !== result.size || row.find((cell) => !validCell.test(cell)) ) ) { result.error = "Invalid TPS notation"; } return result; }; export const transformTPS = function (tps, [rotate, flip]) { if (isString(tps)) { tps = parseTPS(tps); if (tps.error) { throw tps.error; } } rotate = rotate % 4; flip = flip % 2; let grid = cloneDeep(tps.grid); if (rotate === 1) { grid = zip(...grid.map((row) => row.reverse())); } else if (rotate === 2) { grid = grid.map((row) => row.reverse()).reverse(); } else if (rotate === 3) { grid = zip(...grid).map((row) => row.reverse()); } if (flip) { grid = grid.map((row) => row.reverse()); } tps.grid = grid; }; export const Board = class { constructor(options = {}) { this.options = { opening: "swap", ...options }; this.errors = []; if (isString(options.tps) && options.tps.length) { const tps = parseTPS(options.tps); if (tps.error) { this.errors.push(tps.error); return; } if (options.transform) { transformTPS(tps, options.transform); this.transform = options.transform; } this.grid = tps.grid; this.size = tps.size; this.player = tps.player; this.linenum = tps.linenum; } else if (isNumber(options.size || options.tps)) { this.size = options.size || options.tps; this.player = 1; this.linenum = 1; } else { this.errors.push("Missing TPS or board size"); return; } if (!(String(this.size) in pieceCounts)) { this.errors.push("Invalid board size"); return; } // Set up piece counts this.pieceCounts = { 1: { ...pieceCounts[this.size] }, 2: { ...pieceCounts[this.size] }, }; if (options.flats > 2) { this.pieceCounts[1].flat = Number(options.flats); this.pieceCounts[2].flat = Number(options.flats); } if ("caps" in options && options.caps >= 0) { this.pieceCounts[1].cap = Number(options.caps); this.pieceCounts[2].cap = Number(options.caps); } if (options.flats1 > 2) { this.pieceCounts[1].flat = Number(options.flats1); } if ("caps1" in options && options.caps1 >= 0) { this.pieceCounts[1].cap = Number(options.caps1); } if (options.flats2 > 2) { this.pieceCounts[2].flat = Number(options.flats2); } if ("caps2" in options && options.caps2 >= 0) { this.pieceCounts[2].cap = Number(options.caps2); } this.pieceCounts[1].total = this.pieceCounts[1].flat + this.pieceCounts[1].cap; this.pieceCounts[2].total = this.pieceCounts[2].flat + this.pieceCounts[2].cap; if (options.komi) { this.komi = Number(options.komi); if (this.komi % 1) { this.komi = Math.floor(this.komi) + 0.5; } } // Create pieces this.pieces = { all: { 1: { flat: [], cap: [] }, 2: { flat: [], cap: [] }, }, played: { 1: { flat: [], cap: [] }, 2: { flat: [], cap: [] }, }, }; [1, 2].forEach((color) => { ["flat", "cap"].forEach((type) => { for (let index = 0; index < this.pieceCounts[color][type]; index++) { this.pieces.all[color][type][index] = new Piece({ index: index, color: color, type: type, }); } }); }); this.squares = []; for (let y = 0; y < this.size; y++) { this.squares[y] = []; for (let x = 0; x < this.size; x++) { this.squares[y][x] = new Square(x, y, this.size); } } // Create squares this.squares.forEach((row) => { row.forEach((square) => { if (!square.edges.N) { square.neighbors.N = this.squares[square.y + 1][square.x]; } if (!square.edges.S) { square.neighbors.S = this.squares[square.y - 1][square.x]; } if (!square.edges.E) { square.neighbors.E = this.squares[square.y][square.x + 1]; } if (!square.edges.W) { square.neighbors.W = this.squares[square.y][square.x - 1]; } }); }); // Do TPS if (this.grid) { let stack, square, piece, type; this.grid.forEach((row, y) => { row.forEach((col, x) => { if (col[0] !== "X") { stack = col.split(""); square = this.squares[y][x]; while ((piece = stack.shift())) { if (/[SC]/.test(stack[0])) { type = stack.shift(); } else { type = "flat"; } this.playPiece(piece, type, square); } } }); }); } this.afterPly(); } afterPly() { // Count flats this.flats = [0, 0]; this.squares.forEach((row) => { row.forEach((square) => { if (square.color && square.piece.isFlat()) { this.flats[square.color - 1]++; } }); }); if (this.komi) { this.flats[1 * (this.komi > 0)] += Math.abs(this.komi); } // Check for game end this.isGameEnd = false; this.result = ""; const roads = findRoads(this.squares); if (roads.length) { this.roads = roads; // Update road squares roads[1].concat(roads[2]).forEach((road) => { road.squares.forEach((coord) => { coord = atoi(coord); this.squares[coord[1]][coord[0]].setRoad(road); }); }); // Check current player first if (roads[this.player].length) { this.result = this.player === 1 ? "R-0" : "0-R"; } else if (roads[this.player === 1 ? 2 : 1].length) { // Completed opponent's road this.result = this.player === 1 ? "0-R" : "R-0"; } if (this.result) { this.isGameEnd = true; } } else if ( Object.keys(this.pieces.played).some( (player) => this.pieces.played[player].flat.length + this.pieces.played[player].cap.length === this.pieceCounts[player].total ) || !this.squares.find((row) => row.find((square) => !square.pieces.length)) ) { // Last empty square or last piece this.isGameEndFlats = true; if (this.flats[0] === this.flats[1]) { // Draw this.result = "1/2-1/2"; } else if (this.flats[0] > this.flats[1]) { this.result = "F-0"; } else { this.result = "0-F"; } this.isGameEnd = true; } } getTPS() { const grid = this.squares .map((row) => { return row .map((square) => { if (square.pieces.length) { return square.pieces .map((piece) => piece.color + piece.typeCode()) .join(""); } else { return "x"; } }) .join(","); }) .reverse() .join("/") .replace(/x((,x)+)/g, (spaces) => "x" + (1 + spaces.length) / 2); return `${grid} ${this.player} ${this.linenum}`; } doPly(ply) { if (!(ply instanceof Ply)) { ply = new Ply(ply); if (this.transform) { ply = ply.transform(this.size, this.transform); } } if (this.isGameEnd) { throw new Error("The game has ended"); } if (ply.pieceCount > this.size) { throw new Error("Ply violates carry limit"); } if (this.linenum === 1 && (ply.specialPiece || ply.movement)) { throw new Error("Invalid first move"); } const stack = []; const moveset = ply.toMoveset(); if (moveset[0].errors) { throw new Error(...moveset[0].errors); } for (let i = 0; i < moveset.length; i++) { const move = moveset[i]; const action = move.action; const x = move.x; const y = move.y; const count = move.count || 1; const type = move.type; if (!this.squares[y] || !this.squares[y][x]) { throw new Error("Invalid move"); } const square = this.squares[y][x]; if (type) { if (action === "pop") { // Undo placement this.unplayPiece(square); } else { // Do placement if (square.piece) { throw new Error("Invalid move"); } const piece = this.playPiece( this.options.opening === "swap" && this.linenum === 1 ? this.player === 1 ? 2 : 1 : this.player, type, square ); if (!piece) { throw new Error("Invalid move"); } piece.ply = ply; } } else if (action === "pop") { // Begin movement if (i === 0 && square.color !== this.player) { throw new Error("Invalid move"); } times(count, () => { const piece = square.popPiece(); if (!piece) { throw new Error("Invalid move"); } stack.push(piece); }); } else { // Continue movement if (square.pieces.length) { // Check that we can move onto existing piece(s) if (square.piece.isCapstone) { throw new Error("Invalid move"); } else if (square.piece.isStanding) { if ( stack[0].isCapstone && count === 1 && i === moveset.length - 1 ) { // Smash square.piece.isStanding = false; square._setPiece(square.piece); } else { throw new Error("Invalid move"); } } } times(count, () => { const piece = stack.pop(); if (!piece) { throw new Error("Invalid move"); } square.pushPiece(piece); }); } } this.afterPly(); this.player = this.player === 2 ? 1 : 2; this.linenum += Number(this.player === 1); return ply; } playPiece(color, type, square) { const isStanding = /S|wall/.test(type); if (!(type in this.pieceCounts[1])) { type = type === "C" ? "cap" : "flat"; } const piece = this.pieces.all[color][type][this.pieces.played[color][type].length]; if (piece) { piece.isStanding = isStanding; this.pieces.played[color][type].push(piece); square.pushPiece(piece); return piece; } return null; } unplayPiece(square) { const piece = square.popPiece(); if (piece) { piece.isStanding = false; const pieces = this.pieces.played[piece.color][piece.type]; if (piece.index !== pieces.length - 1) { // Swap indices with the top of the stack const lastPiece = pieces.pop(); pieces.splice(piece.index, 1, lastPiece); [piece.index, lastPiece.index] = [lastPiece.index, piece.index]; this.pieces.all[piece.color][piece.type][piece.index] = piece; this.pieces.all[piece.color][piece.type][lastPiece.index] = lastPiece; } else { this.pieces.played[piece.color][piece.type].pop(); } return piece; } return null; } };