@gobstones/gobstones-gbb-parser
Version:
A Parser/Stringifier for GBB (Gobstones Board) file format
328 lines (314 loc) • 13.6 kB
text/typescript
import { Board, Cell, Color, expect } from '@gobstones/gobstones-core';
import {
GBBStringifyingOptions,
defaultGBBStringifyingOptions,
stringFromSeparator
} from './models';
import { GBBStringifyingErrors } from './errors';
import { intl } from '../translations';
/**
* Convert the given JS Object representing a Board into a String representing a GBB board.
*
* @param board - The `Board` to convert to a GBB board string.
* @param options - The options on how to produce the final string
* @returns A String with a GBB Board format representation of the given board.
*
* @throws
* `GBBUnparsingErrors.InvalidSizeDefinition` -
* If the board has cero or negative size in any width or height.
* `GBBUnparsingErrors.HeadBoundaryExceeded` -
* If the head position is negative or exceeds the board size in any of it's coordinates.
* `GBBUnparsingErrors.InvalidBoardDefinition` -
* If the board array has incorrect size (not matching the board defined size)
* in the main array or any of it's elements.
* `GBBUnparsingErrors.InvalidCellDefinition` -
* If any cell of the board array includes more than 4 keys or is missing any of
* 'a', 'n', 'r' or 'v' keys.
*/
export const stringify = (board: Board, options?: Partial<GBBStringifyingOptions>): string => {
// Only for compatibility reasons should use Board directly and not custom objects
const theBoard = convertToBoardIfSimpleJSON(board);
const fullOptions = Object.assign(
{},
defaultGBBStringifyingOptions,
options
) as GBBStringifyingOptions;
intl.setLocale(fullOptions.language);
return stringifyBoardWith(theBoard, fullOptions);
};
/**
* Convert the given JS Object representing a Board into a String representing a GBB board.
* This is called by stringify as an inner implementation, as stringify may present additional
* logic in the future.
*
* @param board - The `Board` to convert to a GBB board string.
* @param options - The options on how to produce the final string
* @returns A String with a GBB Board format representation of the given board.
*
* @throws
* `GBBUnparsingErrors.InvalidSizeDefinition` -
* If the board has cero or negative size in any width or height.
* `GBBUnparsingErrors.HeadBoundaryExceeded` -
* If the head position is negative or exceeds the board size in any of it's coordinates.
* `GBBUnparsingErrors.InvalidBoardDefinition` -
* If the board array has incorrect size (not matching the board defined size)
* in the main array or any of it's elements.
* `GBBUnparsingErrors.InvalidCellDefinition` -
* If any cell of the board array includes more than 4 keys or is missing any of
* 'a', 'n', 'r' or 'v' keys.
*/
const stringifyBoardWith = (board: Board, options: GBBStringifyingOptions): string => {
const separatorBetweenCoordinates = stringFromSeparator(options.separators.betweenCoordinates);
const separatorKeywordToCoordinates = stringFromSeparator(
options.separators.keywordToCoordinates
);
const separatorBetweenKeywords = stringFromSeparator(options.separators.betweenKeywords);
let gbbStrings = [`GBB/1.0`];
gbbStrings.push(
`size${separatorKeywordToCoordinates}${board.width}` +
`${separatorBetweenCoordinates}${board.height}`
);
gbbStrings = gbbStrings.concat(getCellInfoArray(board, options));
gbbStrings.push(
`head${separatorKeywordToCoordinates}${board.head[0]}` +
`${separatorBetweenCoordinates}${board.head[1]}`
);
return gbbStrings.join(separatorBetweenKeywords);
};
/**
* Get the "cells" part of a GBB part format string for a given board object.
*
* @param board - The `Board` to convert to a GBB board string.
* @param options - The options on how to produce the final string
* @returns A String with the cells in GBB Board format representation of the given board.
*
* `GBBUnparsingErrors.InvalidBoardDefinition` -
* If the board array has incorrect size (not matching the board defined size)
* in the main array or any of it's elements.
* `GBBUnparsingErrors.InvalidCellDefinition` -
* If any cell of the board array includes more than 4 keys or is missing any of
* 'a', 'n', 'r' or 'v' keys.
*/
const getCellInfoArray = (board: Board, options: GBBStringifyingOptions): string[] => {
const separatorBetweenCoordinates = stringFromSeparator(options.separators.betweenCoordinates);
const separatorKeywordToCoordinates = stringFromSeparator(
options.separators.keywordToCoordinates
);
return board.foldCells((gbbCells: string[], cell: Cell) => {
const colorInfo = getColorInfo(cell, options);
if (colorInfo !== '') {
gbbCells.push(
`cell${separatorKeywordToCoordinates}${cell.x}` +
`${separatorBetweenCoordinates}${cell.y}` +
`${separatorBetweenCoordinates}${colorInfo}`
);
}
return gbbCells;
}, []);
};
/**
* Obtain the string representation of a cell color information for a given cell
* information object.
*
* @param cell - The `CellInfo` to convert to a string.
* @param options - The options on how to produce the final string
* @returns A String with a cell representation of the given cell.
*
*/
const getColorInfo = (cell: Cell, options: GBBStringifyingOptions): string => {
const separatorColorKeyToNumber = stringFromSeparator(options.separators.colorKeyToNumber);
const separatorBetweenColors = stringFromSeparator(options.separators.betweenColors);
const colorKeySum = cell.getStonesAmount();
const getColorInfoString = (colorKey: Color): string =>
!(
cell.hasStonesOf(colorKey) ||
(options.declareColorsWithZeroStones && colorKeySum > 0) ||
(options.declareColorsWithAllZeroStones && colorKeySum === 0)
)
? ''
: `${nameForColor(
colorKey,
options.useFullColorNames
)}${separatorColorKeyToNumber}${cell.getStonesOf(colorKey)}`;
return [
getColorInfoString(Color.Blue),
getColorInfoString(Color.Black),
getColorInfoString(Color.Red),
getColorInfoString(Color.Green)
]
.join(separatorBetweenColors)
.trim();
};
const nameForColor = (color: Color, fullName: boolean): string => {
switch (color) {
case Color.Blue:
return fullName ? 'Azul' : 'a';
case Color.Black:
return fullName ? 'Negro' : 'n';
case Color.Red:
return fullName ? 'Rojo' : 'r';
case Color.Green:
return fullName ? 'Verde' : 'v';
/* istanbul ignore next */
default:
return undefined;
}
};
/**
* If the given Board is an instance of [[Board]], then the board is returned,
* otherwise, the board is checked for consistency, and it should be an object
* containing at least width, height, and head location. Then, it can supply
* a list of cell data (where each element is an object containing x, y location
* of the cell, and a set of keys that are a [[Color]] with a numeric value), like
* @example
* ```
* { x: 5, y: 3, [Color.Red]: 3 }
* ```
*
* Otherwise, the old method of providing a `board` attribute with the full definition
* for the board, is also accepted, although deprecated and discouraged.
*
* The preference is to always work with an instance of a board, so defining a custom
* object is discouraged, and should be left only for the CLI prompt.
*
* @param board The board, or board definition.
* @throws [GBBStringifyError] or one of it's instances, depending on the
* lacking or not of parts of the board in the definition, if not an instance
* of a board was given.
* @returns an instance of a board
*/
const convertToBoardIfSimpleJSON = (board: any): Board => {
let cellData: any;
if (board.isBoard || (typeof board === 'object' && board instanceof Board)) {
return board;
} else {
expect(board.width as number)
.toBeDefined()
.toBeGreaterThan(0)
.orThrow(new GBBStringifyingErrors.InvalidSizeDefinition('width', board.width));
expect(board.height as number)
.toBeDefined()
.toBeGreaterThan(0)
.orThrow(new GBBStringifyingErrors.InvalidSizeDefinition('height', board.height));
expect(board.head as number[])
.toBeDefined()
.toHaveType('array')
.toHaveLength(2)
.orThrow(new GBBStringifyingErrors.InvalidHeadDefinition());
expect(board.head[0] as number)
.toBeGreaterThanOrEqual(0)
.toBeLowerThan(board.width)
.orThrow(
new GBBStringifyingErrors.HeadBoundaryExceeded(
'xCoordinate',
board.head[0],
0,
board.width
)
);
expect(board.head[1] as number)
.toBeGreaterThanOrEqual(0)
.toBeLowerThan(board.height)
.orThrow(
new GBBStringifyingErrors.HeadBoundaryExceeded(
'yCoordinate',
board.head[1],
0,
board.height
)
);
// New mode of defining
if (board.cellData) {
expect(board.cellData as any[])
.toHaveType('array')
.orThrow(new GBBStringifyingErrors.InvalidBoardDataDefinition());
(board.cellData as any[]).forEach((cell) => {
expect(cell)
.toHaveType('object')
.toHaveNoOtherThan(['a', 'n', 'r', 'v', 'x', 'y'])
.orThrow(new GBBStringifyingErrors.InvalidCellDefinition(cell.x, cell.y));
expect(cell)
.toHaveType('object')
.toHaveProperty('x')
.toHaveProperty('y')
.orThrow(new GBBStringifyingErrors.InvalidBoardDataDefinition());
expect(cell.x as number)
.toHaveType('number')
.toBeGreaterThanOrEqual(0)
.toBeLowerThan(board.width)
.orThrow(new GBBStringifyingErrors.InvalidBoardDataDefinition());
expect(cell.y as number)
.toHaveType('number')
.toBeGreaterThanOrEqual(0)
.toBeLowerThan(board.height)
.orThrow(new GBBStringifyingErrors.InvalidBoardDataDefinition());
expect(cell)
.toHaveNoOtherThan([
'x',
'y',
[Color.Blue].toString(),
[Color.Black].toString(),
[Color.Red].toString(),
[Color.Green].toString()
])
.orThrow(
new GBBStringifyingErrors.InvalidCellDefinition(cell.x, cell.y, 'added')
);
});
cellData = board.cellData;
}
// Board
/* istanbul ignore next */
if (board.board) {
cellData = [];
expect(board.board as any[][])
.toHaveType('array')
.toHaveLength(board.width)
.orThrow(
new GBBStringifyingErrors.InvalidBoardDefinition(
board.board.length,
board.width
)
);
(board.board as any[][]).forEach((column, i) => {
expect(column)
.toHaveType('array')
.toHaveLength(board.height)
.orThrow(
new GBBStringifyingErrors.InvalidBoardDefinition(
column.length,
board.height,
i
)
);
column.forEach((cell, j) => {
expect(cell)
.toHaveType('object')
.toHaveNoOtherThan(['a', 'n', 'r', 'v', 'x', 'y'])
.orThrow(new GBBStringifyingErrors.InvalidCellDefinition(i, j));
expect(cell)
.toHaveProperty('a')
.orThrow(new GBBStringifyingErrors.InvalidCellDefinition(i, j, 'a'));
expect(cell)
.toHaveProperty('n')
.orThrow(new GBBStringifyingErrors.InvalidCellDefinition(i, j, 'n'));
expect(cell)
.toHaveProperty('r')
.orThrow(new GBBStringifyingErrors.InvalidCellDefinition(i, j, 'r'));
expect(cell)
.toHaveProperty('v')
.orThrow(new GBBStringifyingErrors.InvalidCellDefinition(i, j, 'v'));
cellData.push({
x: i,
y: j,
[Color.Blue]: cell['a'],
[Color.Blue]: cell['n'],
[Color.Red]: cell['r'],
[Color.Green]: cell['v']
});
});
});
}
}
return new Board(board.width, board.height, board.head, cellData);
};