UNPKG

@gobstones/gobstones-lang

Version:
415 lines (377 loc) 12.1 kB
/* eslint-disable no-underscore-dangle */ import fs from 'fs'; /* * A board format FMT is a pair of two functions: * * fromJboard : jboard -> string * toJboard : string -> jboard * * where "string" is a string in the given format, and * "jboard" is the internal format produced/consumed by * the RuntimeState class in src/runtime.js. * * Internal format: * jboard.width = width of the board * jboard.height = height of the board * jboard.head = array [x, y] with the position of the head * jboard.board = array of <width> elements, * each of which is an array of <height> elements, * each of which is a cell, of the form * {"a": na, "n": nn, "r": nr, "v": nv} * in such a way that: * jboard.board[x][y].a = number of blue stones at (x, y) * jboard.board[x][y].n = number of black stones at (x, y) * jboard.board[x][y].r = number of red stones at (x, y) * jboard.board[x][y].v = number of green stones at (x, y) */ export interface JBoard { width: number; height: number; head: number[]; board: { a: number; n: number; r: number; v: number }[][]; } class BoardFormat { private _formatName: string; private _description: string; private _extension: string; private _toJboard: (board: string) => JBoard; private _fromJboard: (board: JBoard) => string; public constructor( formatName: string, description: string, extension: string, fromJboard, toJboard ) { this._formatName = formatName; this._description = description; this._extension = extension; this._fromJboard = fromJboard; this._toJboard = toJboard; } public get formatName(): string { return this._formatName; } public get description(): string { return this._description; } public get extension(): string { return this._extension; } public get fromJboard(): (board: JBoard) => string { return this._fromJboard; } public get toJboard(): (board: string) => JBoard { return this._toJboard; } } export interface Cell { blue?: number; black?: number; red?: number; green?: number; } export interface APIJBoard { width: number; height: number; head: { x: number; y: number }; table: Cell[][]; } export function apiboardFromJboard(jboard: JBoard): APIJBoard { const apiboard: APIJBoard = { head: { x: jboard.head[0], y: jboard.head[1] }, width: jboard.width, height: jboard.height, table: [] }; for (let y = 0; y < jboard.height; y++) { const row: Cell[] = []; for (let x = 0; x < jboard.width; x++) { const cellO = jboard.board[x][y]; const cell = { blue: cellO.a > 0 ? cellO.a : undefined, black: cellO.n > 0 ? cellO.n : undefined, red: cellO.r > 0 ? cellO.r : undefined, green: cellO.v > 0 ? cellO.v : undefined }; if (cell.blue === undefined) delete cell.blue; if (cell.black === undefined) delete cell.black; if (cell.red === undefined) delete cell.red; if (cell.green === undefined) delete cell.green; row.push(cell); } apiboard.table.unshift(row); } return apiboard; } export function apiboardToJboard(apiboard: APIJBoard): JBoard { const jboard: JBoard = { head: [apiboard.head.x, apiboard.head.y], width: apiboard.width, height: apiboard.height, board: [] }; for (let x = 0; x < jboard.width; x++) { const column = []; for (let y = 0; y < jboard.height; y++) { const cell = apiboard.table[jboard.height - y - 1][x]; const ca = 'blue' in cell ? cell.blue : 0; const cn = 'black' in cell ? cell.black : 0; const cr = 'red' in cell ? cell.red : 0; const cv = 'green' in cell ? cell.green : 0; column.push({ a: ca, n: cn, r: cr, v: cv }); } jboard.board.push(column); } return jboard; } export interface GSBoard { sizeX: number; sizeY: number; x: number; y: number; table: Cell[][]; } function gsboardFromJboard(jboard: JBoard): string { const gsboard: GSBoard = { x: jboard.head[0], y: jboard.head[1], sizeX: jboard.width, sizeY: jboard.height, table: [] }; for (let y = 0; y < jboard.height; y++) { const row = []; for (let x = 0; x < jboard.width; x++) { const cell = jboard.board[x][y]; row.push({ blue: cell.a, black: cell.n, red: cell.r, green: cell.v }); } gsboard.table.unshift(row); } return JSON.stringify(gsboard); } function gsboardToJboard(gsBoardString: string): JBoard { const gsboard = JSON.parse(gsBoardString); const jboard = { head: [gsboard.x, gsboard.y], width: gsboard.sizeX, height: gsboard.sizeY, board: [] }; for (let x = 0; x < jboard.width; x++) { const column = []; for (let y = 0; y < jboard.height; y++) { const cell = gsboard.table[jboard.height - y - 1][x]; column.push({ a: cell.blue, n: cell.black, r: cell.red, v: cell.green }); } jboard.board.push(column); } return jboard; } export function gbbFromJboard(jboard: JBoard): string { const gbb = []; gbb.push('GBB/1.0'); gbb.push(`size ${jboard.width.toString()} ${jboard.height.toString()}`); for (let y = 0; y < jboard.height; y++) { for (let x = 0; x < jboard.width; x++) { const cell = jboard.board[x][y]; if (cell.a + cell.n + cell.r + cell.v === 0) { continue; } let c = 'cell ' + x.toString() + ' ' + y.toString(); if (cell.a > 0) { c += ' Azul ' + cell.a.toString(); } if (cell.n > 0) { c += ' Negro ' + cell.n.toString(); } if (cell.r > 0) { c += ' Rojo ' + cell.r.toString(); } if (cell.v > 0) { c += ' Verde ' + cell.v.toString(); } gbb.push(c); } } gbb.push(`head ${jboard.head[0].toString()} ${jboard.head[1].toString()}`); return gbb.join('\n') + '\n'; } export function gbbToJboard(gbb: string): JBoard { let i = 0; // DUMMY const jboard: JBoard = { width: 0, height: 0, head: [], board: [] }; const isWhitespace = (x: string): boolean => x === ' ' || x === '\t' || x === '\r' || x === '\n'; function isNumeric(str: string): boolean { for (const char of str) { if ('0123456789'.indexOf(char) === -1) { return false; } } return str.length > 0; } function skipWhitespace(): void { /* Skip whitespace */ while (i < gbb.length && isWhitespace(gbb[i])) { i++; } } function readToken(): string { const t = []; skipWhitespace(); while (i < gbb.length && !isWhitespace(gbb[i])) { t.push(gbb[i]); i++; } return t.join(''); } function readN(errmsg: string): number { const t = readToken(); if (!isNumeric(t)) { throw Error(errmsg); } const ti = parseInt(t, 10); if (ti < 0) { throw Error(errmsg); } return ti; } function readRange(a: number, b: number, errmsg: string): number { const t = readN(errmsg); if (t < a || t >= b) { throw Error(errmsg); } return t; } if (readToken() !== 'GBB/1.0') { throw Error('GBB/1.0: Board not in GBB/1.0 format.'); } if (readToken() !== 'size') { throw Error('GBB/1.0: Board lacks a size declaration.'); } jboard.width = readN('GBB/1.0: Board width is not a number.'); jboard.height = readN('GBB/1.0: Board height is not a number.'); if (jboard.width <= 0 || jboard.height <= 0) { throw Error('GBB/1.0: Board size should be positive.'); } jboard.head = [0, 0]; jboard.board = []; for (let w = 0; w < jboard.width; w++) { const row = []; for (let j = 0; j < jboard.height; j++) { row.push({ a: 0, n: 0, r: 0, v: 0 }); } jboard.board.push(row); } let headDeclared = false; const colores = { Azul: 'a', A: 'a', Negro: 'n', N: 'n', Rojo: 'r', R: 'r', Verde: 'v', V: 'v' }; while (i < gbb.length) { const op = readToken(); if (op === '') { break; } else if (op === 'head') { if (headDeclared) { throw Error('GBB/1.0: Head position cannot be declared twice.'); } headDeclared = true; const hx = readRange(0, jboard.width, 'GBB/1.0: Invalid head position.'); const hy = readRange(0, jboard.height, 'GBB/1.0: Invalid head position.'); jboard.head = [hx, hy]; } else if (op === 'cell') { const cx = readRange(0, jboard.width, 'GBB/1.0: Invalid cell position.'); const cy = readRange(0, jboard.height, 'GBB/1.0: Invalid cell position.'); const colorDeclared = {}; while (i < gbb.length) { const color = readToken(); if (!(color in colores)) { i -= color.length; break; } const colorId = colores[color]; if (colorId in colorDeclared) { throw Error('GBB/1.0: Color cannot be declared twice.'); } const n = readN('GBB/1.0: Invalid amount of stones.'); jboard.board[cx][cy][colorId] = n; } } else { throw Error('GBB/1.0: Malformed board: unknown command "' + op + '".'); } } return jboard; } const BOARD_FORMAT_LIST = [ new BoardFormat( 'jboard', 'Representation of a board as a JavaScript object for internal usage.', 'jboard', JSON.stringify, JSON.parse ), new BoardFormat( 'gs-weblang-cli-json-board', `Representation of a board as a Javascript object used by the gs-weblang-cli tool.`, 'json', gsboardFromJboard, gsboardToJboard ), new BoardFormat('gbb', 'GBB/1.0', 'gbb', gbbFromJboard, gbbToJboard) ]; export const DEFAULT_FORMAT = 'gs-weblang-cli-json-board'; export const BOARD_FORMATS = {}; for (const boardFormat of BOARD_FORMAT_LIST) { BOARD_FORMATS[boardFormat.formatName] = boardFormat; } function fileExtension(filename: string): string { const parts = filename.split('.'); return parts[parts.length - 1]; } function fileBoardFormat(filename: string): BoardFormat { const extension = fileExtension(filename); for (const fmt of BOARD_FORMAT_LIST) { if (extension === fmt.extension) { return fmt; } } return BOARD_FORMATS[DEFAULT_FORMAT]; } export function readJboardFromFile(filename: string): JBoard { const format = fileBoardFormat(filename); const contents = fs.readFileSync(filename, 'utf8'); return format.toJboard(contents); } export function writeJboardToFile(filename: string, jboard: JBoard): void { const format = fileBoardFormat(filename); const contents = format.fromJboard(jboard); fs.writeFileSync(filename, contents, 'utf8'); }