@gobstones/gobstones-lang
Version:
415 lines (377 loc) • 12.1 kB
text/typescript
/* 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');
}