@gobstones/gobstones-gbb-parser
Version:
A Parser/Stringifier for GBB (Gobstones Board) file format
1,345 lines (1,333 loc) • 193 kB
JavaScript
import require$$0 from 'events';
/**
* The base class of the error hierarchy that is thrown when
* an invalid operation is performed in the [[Board/Board | Board]]
* class and it's associated [[Board/Cell | Cell]].
*/
class BoardError extends Error {
constructor(name, message) {
super(message);
this.name = name;
this.isError = true;
Object.setPrototypeOf(this, BoardError.prototype);
}
}
/**
* This error is thrown when attempting to create a board
* with invalid data.
*/
class InvalidBoardDescription extends BoardError {
constructor(height, width, cellLocation) {
super('InvalidBoardDescription', `The values used to create the board are invalid. ` +
` height: ${height}, width: ${width}, cell location: ${cellLocation}`);
this.height = height;
this.width = width;
this.cellLocation = cellLocation;
Object.setPrototypeOf(this, InvalidBoardDescription.prototype);
}
}
/**
* This error is thrown when attempting to read a cell, a
* column or a row but an invalid location is given.
*/
class InvalidCellReading extends BoardError {
constructor(attempt, failingCoordinate) {
super('InvalidCellReading', `The attempt of ${attempt} failed for coordinate ` +
`[${failingCoordinate[0]}, ${failingCoordinate[1]}].`);
this.attempt = attempt;
this.failingCoordinate = failingCoordinate;
Object.setPrototypeOf(this, InvalidCellReading.prototype);
}
}
/**
* This error is thrown when attempting to move the head, but an
* invalid location is given.
*/
class LocationFallsOutsideBoard extends BoardError {
constructor(attempt, failingCoordinate, previousCoordinate) {
super('LocationFallsOutsideBoard', `The attempt of ${attempt} from [${previousCoordinate[0]}, ` +
`${previousCoordinate[1]}] falls outside the board on ` +
`coordinate [${failingCoordinate[0]}, ${failingCoordinate[1]}].`);
this.attempt = attempt;
this.failingCoordinate = failingCoordinate;
this.previousCoordinate = previousCoordinate;
Object.setPrototypeOf(this, LocationFallsOutsideBoard.prototype);
}
}
/**
* This error is thrown when attempting to change the size of the board,
* but an invalid size is given
*/
class InvalidSizeChange extends BoardError {
constructor(attempt, previousWidth, previousHeight, newWidth, newHeight) {
super('InvalidSizeChange', `The attempt of changing size by ${attempt} from width ${previousWidth}, ` +
`and height ${previousHeight} ends in an invalid board of width ` +
`${newWidth} and height ${newHeight}.`);
this.attempt = attempt;
this.previousWidth = previousWidth;
this.previousHeight = previousHeight;
this.newWidth = newWidth;
this.newHeight = newHeight;
Object.setPrototypeOf(this, InvalidSizeChange.prototype);
}
}
/**
* This error is thrown when attempting to change the stones amount
* with an invalid amount of stone (negative amount).
*/
class InvalidStonesAmount extends BoardError {
constructor(attempt, color, amount, previousCellState) {
super('InvalidStonesAmount', `The attempt of ${attempt} failed for color ${color} given ${amount}.`);
this.attempt = attempt;
this.color = color;
this.amount = amount;
this.previousCellState = previousCellState;
Object.setPrototypeOf(this, InvalidStonesAmount.prototype);
}
}
/**
* This enum represent the valid Gobstones Colors.
* It's accompanied by a namespace with the same name, that provides additional
* functionality, such as asking for the first or the last color, or iterate over
* the elements of this enum.
*
* Note that directions are sorted in the following order, from first to last.
* * Color.Blue
* * Color.Black
* * Color.Red
* * Color.Green
*
* Always prefer using the enum over the string values it represents,
* even as object keys.
*/
var Color;
(function (Color) {
Color["Blue"] = "a";
Color["Black"] = "n";
Color["Red"] = "r";
Color["Green"] = "v";
})(Color || (Color = {}));
/**
* This namespace provides additional functionality that extends the simple
* Color enum, by providing some helper functions.
*/
(function (Color) {
/**
* The smallest Color possible, currently [[Color.Blue]]
*
* @returns The smallest color.
*/
Color.min = () => Color.Blue;
/**
* The biggest Color possible, currently [[Color.Green]]
*
* @returns The biggest color.
*/
Color.max = () => Color.Green;
/**
* The next Color of a given Color. Colors are sorted
* in the following way, from first to last:
* * Color.Blue
* * Color.Black
* * Color.Red
* * Color.Green
*
* And they are cyclic, that is, the next color of Green is Blue.
*
* @param color The color to obtain the next value from.
*
* @returns The next color of the given one.
*/
Color.next = (color) => {
switch (color) {
case Color.Blue:
return Color.Black;
case Color.Black:
return Color.Red;
case Color.Red:
return Color.Green;
case Color.Green:
return Color.Blue;
/* istanbul ignore next */
default:
return undefined;
}
};
/**
* The next Color of a given Color. Color are sorted
* in the following way, from last to first:
* * Color.Green
* * Color.Red
* * Color.Black
* * Color.Blue
*
* And they are cyclic, that is, the previous color of Blue is Green.
*
* @param color The color to obtain the previous value from.
*
* @returns The previous color of the given one.
*/
Color.previous = (color) => {
switch (color) {
case Color.Blue:
return Color.Green;
case Color.Black:
return Color.Blue;
case Color.Red:
return Color.Black;
case Color.Green:
return Color.Red;
/* istanbul ignore next */
default:
return undefined;
}
};
/**
* Iterate over all the colors, in their defined order, from the smallest,
* to the biggest, performing the callback over each color. A function that
* expects a color and returns void is expected as an argument.
*
* @param f The callback to call on each iteration.
*/
function foreach(f) {
let current = Color.min();
while (current !== Color.max()) {
f(current);
current = Color.next(current);
}
f(current);
}
Color.foreach = foreach;
})(Color || (Color = {}));
/**
* This enum represent the valid Gobstones Directions.
* It's accompanied by a namespace with the same name, that provides additional
* functionality, such as asking for the first or the last direction, or iterate over
* the elements of this enum.
*
* Note that directions are sorted in the following order, from first to last.
* * Direction.North
* * Direction.East
* * Direction.South
* * Direction.West
*
* Always prefer using the enum over the string values it represents,
* even as object keys.
*/
var Direction;
(function (Direction) {
Direction["North"] = "n";
Direction["East"] = "e";
Direction["South"] = "s";
Direction["West"] = "w";
})(Direction || (Direction = {}));
/**
* This namespace provides additional functionality that extends the simple
* Direction enum, by providing some helper functions.
*/
(function (Direction) {
/**
* The smallest Direction possible, currently [[Direction.North]]
*
* @returns The smallest direction.
*/
Direction.min = () => Direction.North;
/**
* The biggest Direction possible, currently [[Direction.West]]
*
* @returns The biggest direction.
*/
Direction.max = () => Direction.West;
/**
* The next Direction of a given Direction. Directions are sorted
* in the following way, from first to last:
* * Direction.North
* * Direction.East
* * Direction.South
* * Direction.West
*
* And they are cyclic, that is, the next direction of West is North.
*
* @param dir The direction to obtain the next value from.
*
* @returns The next direction of the given one.
*/
Direction.next = (dir) => {
switch (dir) {
case Direction.North:
return Direction.East;
case Direction.East:
return Direction.South;
case Direction.South:
return Direction.West;
case Direction.West:
return Direction.North;
/* istanbul ignore next */
default:
return undefined;
}
};
/**
* The next Direction of a given Direction. Directions are sorted
* in the following way, from last to first:
* * Direction.West
* * Direction.South
* * Direction.East
* * Direction.North
*
* And they are cyclic, that is, the previous direction of North is West.
*
* @param dir The direction to obtain the previous value from.
*
* @returns The previous direction of the given one.
*/
Direction.previous = (color) => {
switch (color) {
case Direction.North:
return Direction.West;
case Direction.East:
return Direction.North;
case Direction.South:
return Direction.East;
case Direction.West:
return Direction.South;
/* istanbul ignore next */
default:
return undefined;
}
};
/**
* The opposite Direction of a given Direction. Directions are opposed
* to each other in pairs, those being:
* * Direction.West is opposite to Direction.East and vice versa
* * Direction.North is opposite to Direction.South and vice versa
*
* @param dir The direction to obtain the opposite value from.
*
* @returns The opposite direction of the given one.
*/
Direction.opposite = (color) => {
switch (color) {
case Direction.North:
return Direction.South;
case Direction.East:
return Direction.West;
case Direction.South:
return Direction.North;
case Direction.West:
return Direction.East;
/* istanbul ignore next */
default:
return undefined;
}
};
/**
* Answer wether or not the given direction is vertical,
* that is, one of Direction.North or Direction.South.
*
* @param dir The direction to find out if it's vertical.
*
* @returns `true` if it's vertical, `false` otherwise.
*/
Direction.isVertical = (dir) => dir === Direction.North || dir === Direction.South;
/**
* Answer wether or not the given direction is horizontal,
* that is, one of Direction.East or Direction.West.
*
* @param dir The direction to find out if it's horizontal.
*
* @returns `true` if it's horizontal, `false` otherwise.
*/
Direction.isHorizontal = (dir) => !Direction.isVertical(dir);
/**
* Iterate over all the directions, in their defined order, from the smallest,
* to the biggest, performing the callback over each direction. A function that
* expects a direction and returns void is expected as an argument.
*
* @param f The callback to call on each iteration.
*/
Direction.foreach = (f) => {
let current = Direction.min();
while (current !== Direction.max()) {
f(current);
current = Direction.next(current);
}
f(current);
};
})(Direction || (Direction = {}));
var TypedEmitter = require$$0.EventEmitter;
/**
* This is just a helper module that re-export the useful
* [binier/tiny-typed-emitter](https://github.com/binier/tiny-typed-emitter).
* You can check out information about this module at their README.
*
* @see https://github.com/binier/tiny-typed-emitter
*
* @author Alan Rodas Bonjour <alanrodas@gmail.com>
*
* @packageDocumentation
*/
/**
* This is a rename of EventEmitter that allows for type checking
* of the event's emitting in a class. Just extend your event
* throwing classes with TypeEmitter with the events signature as a
* generic type, and expect that calling emit throws errors when not
* typechecking. The on event over instances of the class will also
* throws errors when invalid event names are used.
*
* @see [binier/tiny-typed-emitter](https://github.com/binier/tiny-typed-emitter)
* to read more information about how all this works.
*/
const TypedEmitter$1 = TypedEmitter;
/**
* This module provides the function [[deepEquals]] that allows to test
* if two object are semantically equal. The module is loosely based on
* [inspect-js/node-deep-equal](https://github.com/inspect-js/node-deep-equal)
* but removing all dependencies.
*
* The function is intended for comparison of basic types, simple non classed
* objects, arrays, and built-in basic classed objects such as Set, Map, RegExp,
* Date, Buffer and Error.
*
* Note that deep equality is costly, and should be avoided whenever possible. Yet
* is some scenarios, it may be useful to count with such a function. In that sense,
* we provide a 'cheap' (in terms of dependency overhead) alternative to most
* third-party implementations, that can be used through the whole project.
*
* Note that the implementation is kind of ugly and heavily procedural. The idea
* behind the code is to return a result as fast as possible. Also note that it might
* not consider the most edgy cases. If you have trouble with a specific case,
* please consider sending a Pull Request or raising an Issue in this project's
* repository.
*
* @author Alan Rodas Bonjour <alanrodas@gmail.com>
*
* @packageDocumentation
*/
/**
* Answer wether or not two elements are equal, considering them
* equal when they have the same type and all their internal elements are
* the same, or when they represent the same concept (two regular expressions
* that match the same string, to date for the same moment, to sets with same
* elements in it, and so on).
*
* Most simple cases should return true as expected, such as:
* * `deepEquals(1, 1.0)`
* * `deepEquals({a: 1, b: {c: 3, d: 4}}, {b: {c: 3, d: 4}, a: 1})`
* * `deepEquals([1,2,3], [1, 1+1, 2+1])`
* * `deepEquals(new Set([1,2,3]), new Set([3,2,1]))`
*
* There is one special case, that we support and that might not be expected
* in standard TS/JS behavior, which is `NaN` comparison. Here you might find
* that `deepEquals(NaN, NaN)` is `true`, even though in JS NaN is not equal
* to anything, even itself.
*
* Note that parameters are statically typed when running in TypeScript,
* thus not allowing for things such as `deepEquals(4, '4.0')` to be typed,
* unless explicitly casted away. In that case even, the comparison is performed
* not considering type coercion, thus, returning false.
*
* If you want to see all supported and unsupported cases, we recommend you to check
* out the test cases.
*
*
* @param first The element to compare to.
* @param second The element to compare against.
*
* @return `true` if both elements are equal, `false` otherwise.
*/
const deepEquals = (first, second) => {
const compare = (a, b) => {
// Return true if they are the same object
if (a === b)
return true;
// and false if they don't have the same type
else if (typeof a !== typeof b)
return false;
// Check for types and call a specific comparer
// depending on the type
if (typeof a === 'number' && typeof b === 'number')
return numberEquals(a, b);
// Cases where they are both objects start here, many
// different things are considered object in JS, so
// we need to disambiguate.
if (typeof a === 'object' && typeof b === 'object') {
// If they belong to different classes, then they are not equal,
// one of them might not have a class, so consider that case too.
if (a.constructor && b.constructor && a.constructor !== b.constructor)
return false;
// Use array comparison if both are arrays
if (Array.isArray(a) && Array.isArray(b))
return arrayEquals(a, b, compare);
// If both are Sets
if (a instanceof Set && b instanceof Set)
return setEquals(a, b);
// If both are Maps
if (a instanceof Map && b instanceof Map)
return mapEquals(a, b, compare);
// If both are Errors
if (a instanceof Error && b instanceof Error)
return errorEquals(a, b);
// If both are RegExp
if (a instanceof RegExp && b instanceof RegExp)
return regexpEquals(a, b);
// If both are Dates
if (a instanceof Date && b instanceof Date)
return dateEquals(a, b);
// If both are Buffers
if (isBuffer(a) && isBuffer(b))
return bufferEquals(a, b);
// Reached this case we consider a plain object (or class
// with plain properties that can be accessed)
return objectEquals(a, b, compare);
}
return false;
};
return compare(first, second);
};
/**
* Answer if two numbers are equal. Two numbers are equal
* if they happen to be the same number, or, if both are NaN.
*
* @param a The first number
* @param b The second number
*
* @returns true when both numbers are the same, or both are NaN.
*/
const numberEquals = (a, b) => {
if (Number.isNaN(a) && Number.isNaN(b))
return true;
else
return a === b;
};
/**
* Answer if two arrays are equal. Two arrays are equal when they both
* have the exact same number of elements, and they have the same element
* in each position. To consider if two elements inside the array are equal
* the [[innerComparer]] is used. The expected value for [[innerComparer]]
* is the recursive comparer function in deepEquals.
*
* @param a The first array
* @param b The second array
* @param innerComparer The function for testing if two inner elements are equal
*
* @returns `true` if both arrays are equal, `false` otherwise.
*/
const arrayEquals = (aArr, bArr, innerComparer) => {
// Two arrays should have the same length
if (aArr.length !== bArr.length)
return false;
// And the same element in each position, which is
// compared by deep equality
for (let i = 0; i < aArr.length; i++) {
// In case the value in a position is not equal,
// they are not equal
if (!innerComparer(aArr[i], bArr[i]))
return false;
}
// They are only equal after full comparison
return true;
};
/**
* Answer if two objects are equal. Two objects are equal when they both
* have the exact same number of properties, with same names, and they
* have the same value in each property. To consider if two values of a property
* are equal the [[innerComparer]] is used. The expected value for [[innerComparer]]
* is the recursive comparer function in deepEquals.
*
* @param a The first object
* @param b The second object
* @param innerComparer The function for testing if two inner elements are equal
*
* @returns `true` if both object are equal, `false` otherwise.
*/
const objectEquals = (aArr, bArr, innerComparer) => {
// Obtain the object keys, sorted
const aKeys = Object.keys(aArr).sort();
const bKeys = Object.keys(bArr).sort();
// They should have the same amount of keys
if (aKeys.length !== bKeys.length)
return false;
// And perform a cheap key test (they should both have same keys)
for (let i = 0; i < aKeys.length; i++) {
if (aKeys[i] !== bKeys[i])
return false;
}
// If they do, perform a more expensive deep equal test in all values
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < aKeys.length; i++) {
const aValue = aArr[aKeys[i]];
const bValue = bArr[aKeys[i]];
if (!innerComparer(aValue, bValue))
return false;
}
// They must be equal when this is reached
return true;
};
/**
* Answer if two Sets are equal. Two Sets are equal when they both
* have the exact same number of elements, and they have the same
* elements.
*
* @param a The first object
* @param b The second object
*
* @returns `true` if both object are equal, `false` otherwise.
*/
const setEquals = (a, b) => {
if (a.size !== b.size)
return false;
const aIterator = a.entries();
let aNext = aIterator.next();
while (aNext && !aNext.done) {
if (!b.has(aNext.value[1]))
return false;
aNext = aIterator.next();
}
return true;
};
/**
* Answer if two Maps are equal. Two Maps are equal when they both
* have the exact same number of keys, with same key names, and they
* have the same value in each key. To consider if two values of a key
* are equal the [[innerComparer]] is used. The expected value for [[innerComparer]]
* is the recursive comparer function in deepEquals.
*
* @param a The first map
* @param b The second map
* @param innerComparer The function for testing if two inner elements are equal
*
* @returns `true` if both Maps are equal, `false` otherwise.
*/
const mapEquals = (a, b, innerComparer) => {
if (a.size !== b.size)
return false;
const aEntries = a.entries();
let aNext = aEntries.next();
while (!aNext.done) {
// Should have a key with same name or value
if (!b.has(aNext.value[0]))
return false;
if (!innerComparer(aNext.value[1], b.get(aNext.value[0])))
return false;
aNext = aEntries.next();
}
return true;
};
/**
* Answer if two Errors are equal. Two Errors are equal when they both
* have the exact name and message.
*
* @param a The first Error
* @param b The second Error
*
* @returns `true` if both Errors are equal, `false` otherwise.
*/
const errorEquals = (a, b) => a.name === b.name && a.message === b.message;
/**
* Answer if two RegExps are equal. Two RegExps are equal when they both
* have the exact source and flags.
*
* @param a The first RegExp
* @param b The second RegExp
*
* @returns `true` if both RegExp are equal, `false` otherwise.
*/
const regexpEquals = (a, b) => a.source === b.source && a.flags === b.flags;
/**
* Answer if two Dates are equal. Two Date are equal when they both
* have the exact time.
*
* @param a The first Date
* @param b The second Date
*
* @returns `true` if both Date are equal, `false` otherwise.
*/
const dateEquals = (a, b) => a.getTime() === b.getTime();
/**
* Answer if two Buffers are equal. Two Buffers are equal when they both
* have the exact element at each position.
*
* @param a The first Buffer
* @param b The second Buffer
*
* @returns `true` if both Buffers are equal, `false` otherwise.
*/
const bufferEquals = (a, b) => {
if (a.length !== b.length)
return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i])
return false;
}
return true;
};
/**
* Answer if an element is a Buffer.
*
* @param x The element to test if it's a buffer
*
* @returns `true` if the element is a Buffer, `false` otherwise.
*/
const isBuffer = (x) => !!(x.constructor && x.constructor.isBuffer && x.constructor.isBuffer(x));
/**
* This module provides the [[Matchers]] class, that contains all the matchers
* for the expectations. All matchers are centralized in this module for
* bigger extensibility.
*
* Additionally, it provides the [[MatcherCall]] interface, that allows to
* register the result of a call to a specific matcher.
*
* @author Alan Rodas Bonjour <alanrodas@gmail.com>
*
* @packageDocumentation
*/
/**
* This object contains a series of matchers, that is, a series of functions
* that can be called with the actual value (and in cases a series of arguments)
* and returns a boolean, `true` if the value satisfies the matcher, and `false`
* otherwise.
*
* Having the matchers separated from the instances that use the matchers allow for
* greater extensibility.
*/
class Matchers {
// Generic
/** Answers if the actual value is the same as expected, using strict compare */
static toBe(actual, expected) {
return actual === expected;
}
/** Answers if the actual value is the same as expected, using a deep compare mechanism */
static toBeLike(actual, expected) {
return deepEquals(actual, expected);
}
/** Answers if the actual value is defined (as in not equal to undefined) */
static toBeDefined(actual) {
return actual !== undefined;
}
/** Answers if the actual value is undefined */
static toBeUndefined(actual) {
return actual === undefined;
}
/** Answers if the actual value is null (strict null, not undefined) */
static toBeNull(actual) {
// eslint-disable-next-line no-null/no-null
return actual === null;
}
/** Answers if the actual value is a truthy value */
static toBeTruthy(actual) {
return !!actual;
}
/** Answers if the actual value is a falsy value */
static toBeFalsy(actual) {
return !actual;
}
/**
* Answers if the actual value has a type matching the expected type.
* This comparison is performed using the `typeof` operation over the value,
* with additional logic added to support 'array' as a type.
* @example `toHaveType([1,2,3], 'array')` returns `true` as expected.
*/
static toHaveType(actual, expectedType) {
return ((expectedType !== 'object' && typeof actual === expectedType) ||
(expectedType === 'array' && typeof actual === 'object' && Array.isArray(actual)) ||
(expectedType === 'object' && !Array.isArray(actual) && typeof actual === expectedType));
}
// Numbers
/** Answer if the actual value is greater than the expected value. */
static toBeGreaterThan(actual, expected) {
return typeof actual === 'number' && actual > expected;
}
/** Answer if the actual value is greater than or equal than the expected value. */
static toBeGreaterThanOrEqual(actual, expected) {
return typeof actual === 'number' && actual >= expected;
}
/** Answer if the actual value is lower than the expected value. */
static toBeLowerThan(actual, expected) {
return typeof actual === 'number' && actual < expected;
}
/** Answer if the actual value is lower than or equal than the expected value. */
static toBeLowerThanOrEqual(actual, expected) {
return typeof actual === 'number' && actual <= expected;
}
/** Answer if the actual value is between the from and to values (inclusive). */
static toBeBetween(actual, from, to) {
return typeof actual === 'number' && from <= actual && actual <= to;
}
/** Answer if the actual value is infinity (positive or negative). */
static toBeInfinity(actual) {
return typeof actual === 'number' && (actual === Infinity || actual === -Infinity);
}
/** Answer if the actual value is not a number. */
static toBeNaN(actual) {
return typeof actual === 'number' && Number.isNaN(actual);
}
/**
* Answer if the actual value is close to the expected value, by at least the number
* of digits given.
* @example `toBeCloseTo(4.0005, 4.0009, 3)` returns `true`, as there are 3
* digits that are equal between actual and expected.
* If no amount of digits is given, 5 is taken by default.
*/
static toBeCloseTo(actual, expected, numDigits) {
return (typeof actual === 'number' &&
Math.abs(expected - actual) < Math.pow(10, -numDigits) / 10);
}
// String
/** Answer if the actual value has expected as a substring. */
static toHaveSubstring(actual, expected) {
return typeof actual === 'string' && actual.indexOf(expected) >= 0;
}
/** Answer if the actual value starts with the expected string. */
static toStartWith(actual, expected) {
return typeof actual === 'string' && actual.startsWith(expected);
}
/** Answer if the actual value ends with the expected string. */
static toEndWith(actual, expected) {
return typeof actual === 'string' && actual.endsWith(expected);
}
/** Answer if the actual value matches the given regexp. */
static toMatch(actual, expected) {
return typeof actual === 'string' && expected.test(actual);
}
// Arrays
/** Answer if the actual value has a length of expected number. */
static toHaveLength(actual, expected) {
return typeof actual === 'object' && actual instanceof Array && actual.length === expected;
}
/** Answer if the actual value contains the expected element. */
static toContain(actual, expected) {
return typeof actual === 'object' && Array.isArray(actual) && actual.indexOf(expected) >= 0;
}
/**
* Answer if the actual value has a the expected element at a given position.
* Returns false if the position does not exist.
*/
static toHaveAtPosition(actual, expected, position) {
return (typeof actual === 'object' &&
Array.isArray(actual) &&
actual.length > position &&
position >= 0 &&
actual[position] === expected);
}
/** Answer if all the element of the actual value satisfy a given criteria. */
static allToSatisfy(actual, criteria) {
return (typeof actual === 'object' &&
Array.isArray(actual) &&
actual.reduce((r, a) => criteria(a) && r, true));
}
/** Answer if any of the element of the actual value satisfy a given criteria. */
static anyToSatisfy(actual, criteria) {
return (typeof actual === 'object' &&
Array.isArray(actual) &&
actual.reduce((r, a) => criteria(a) || r, false));
}
/** Answer if a given amount of elements of the actual value satisfy a given criteria. */
static amountToSatisfy(actual, amount, criteria) {
return (typeof actual === 'object' &&
Array.isArray(actual) &&
actual.reduce((r, a) => (criteria(a) ? r + 1 : r), 0) === amount);
}
// Objects
/** Answer if the actual element has the given amount of properties. */
static toHavePropertyCount(actual, amount) {
return (typeof actual === 'object' &&
Object.keys(actual).filter((e) => Object.hasOwnProperty.call(actual, e)).length ===
amount);
}
/** Answer if an object has at least all keys in the least. Combine with
* toHaveNoOtherThan to ensure exact key existence */
static toHaveAtLeast(actual, keys) {
if (typeof actual !== 'object')
return false;
for (const key of keys) {
if (!actual[key])
return false;
}
return true;
}
/** Answer if an object has no other than the given keys (although not all given
* need to be present). Combine with toHaveAtLeast to ensure exact key existence */
static toHaveNoOtherThan(actual, keys) {
if (typeof actual !== 'object')
return false;
for (const key of Object.keys(actual)) {
if (keys.indexOf(key) < 0) {
return false;
}
}
return true;
}
/** Answer if the actual element has a property with the given name. */
static toHaveProperty(actual, propertyName) {
return (typeof actual === 'object' && Object.prototype.hasOwnProperty.call(actual, propertyName));
}
/** Answer if the actual element is an instance of a given class (using instanceof). */
// eslint-disable-next-line @typescript-eslint/ban-types
static toBeInstanceOf(actual, classConstructor) {
return typeof actual === 'object' && actual instanceof classConstructor;
}
}
/**
* This abstract class provides finished expectation behavior for
* all actions based on the fact that it's subclass provides
* an implementation for [[getResult]].
*/
class FinishedExpectation {
/** @inheritdoc [[IFinishedExpectation.orThrow]] */
orThrow(error) {
if (!this.getResult()) {
throw error;
}
}
/** @inheritdoc [[IFinishedExpectation.orYield]] */
orYield(value) {
return !this.getResult() ? value : undefined;
}
/** @inheritdoc [[IFinishedExpectation.andDoOr]] */
andDoOr(actionWhenTrue, actionWhenFalse) {
if (this.getResult()) {
actionWhenTrue();
}
else {
actionWhenFalse();
}
}
/** @inheritdoc [[IFinishedExpectation.andDo]] */
andDo(action) {
// eslint-disable-next-line @typescript-eslint/no-empty-function, no-empty-function
this.andDoOr(action, () => { });
}
/** @inheritdoc [[IFinishedExpectation.orDo]] */
orDo(action) {
// eslint-disable-next-line @typescript-eslint/no-empty-function, no-empty-function
this.andDoOr(() => { }, action);
}
}
/**
* This module provides the [[Expectation]] class that implements
* all interfaces for expectations.
*
* @author Alan Rodas Bonjour <alanrodas@gmail.com>
*
* @packageDocumentation
*/
class Expectation extends FinishedExpectation {
/**
* Create a new expectation for the given element.
*
* @param element The element to query to.
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
constructor(element) {
super();
this.states = [];
this.element = element;
this.isNot = false;
}
/** @inheritdoc [[IGenericExpectation.not]] */
get not() {
this.isNot = !this.isNot;
return this;
}
/** @inheritdoc [[IGenericExpectation.toBe]] */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
toBe(value) {
return this.runMatcher('toBe', [value]);
}
/** @inheritdoc [[IGenericExpectation.toBeLike]] */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
toBeLike(value) {
return this.runMatcher('toBeLike', [value]);
}
/** @inheritdoc [[IGenericExpectation.toBeNull]] */
toBeNull() {
return this.runMatcher('toBeNull', []);
}
/** @inheritdoc [[IGenericExpectation.toBeDefined]] */
toBeDefined() {
return this.runMatcher('toBeDefined', []);
}
/** @inheritdoc [[IGenericExpectation.toBeUndefined]] */
toBeUndefined() {
return this.runMatcher('toBeUndefined', []);
}
/** @inheritdoc [[IGenericExpectation.toBeTruthy]] */
toBeTruthy() {
return this.runMatcher('toBeTruthy', []);
}
/** @inheritdoc [[IGenericExpectation.toBeFalsy]] */
toBeFalsy() {
return this.runMatcher('toBeFalsy', []);
}
/** @inheritdoc [[IGenericExpectation.toHaveType]] */
toHaveType(typeName) {
return this.runMatcher('toHaveType', [typeName]);
}
// INumberExpectation
/** @inheritdoc [[INumberExpectation.toBeGreaterThan]] */
toBeGreaterThan(value) {
return this.runMatcher('toBeGreaterThan', [value]);
}
/** @inheritdoc [[INumberExpectation.toBeGreaterThanOrEqual]] */
toBeGreaterThanOrEqual(value) {
return this.runMatcher('toBeGreaterThanOrEqual', [value]);
}
/** @inheritdoc [[INumberExpectation.toBeLowerThan]] */
toBeLowerThan(value) {
return this.runMatcher('toBeLowerThan', [value]);
}
/** @inheritdoc [[INumberExpectation.toBeLowerThanOrEqual]] */
toBeLowerThanOrEqual(value) {
return this.runMatcher('toBeLowerThanOrEqual', [value]);
}
/** @inheritdoc [[INumberExpectation.toBeBetween]] */
toBeBetween(from, to) {
return this.runMatcher('toBeBetween', [from, to]);
}
/** @inheritdoc [[INumberExpectation.toBeInfinity]] */
toBeInfinity() {
return this.runMatcher('toBeInfinity', []);
}
/** @inheritdoc [[INumberExpectation.toBeNaN]] */
toBeNaN() {
return this.runMatcher('toBeNaN', []);
}
/** @inheritdoc [[INumberExpectation.toBeCloseTo]] */
toBeCloseTo(value, digits = 5) {
return this.runMatcher('toBeCloseTo', [value, digits]);
}
// IStringExpectation
/** @inheritdoc [[IStringExpectation.toHaveSubstring]] */
toHaveSubstring(substring) {
return this.runMatcher('toHaveSubstring', [substring]);
}
/** @inheritdoc [[IStringExpectation.toStartWith]] */
toStartWith(start) {
return this.runMatcher('toStartWith', [start]);
}
/** @inheritdoc [[IStringExpectation.toEndWith]] */
toEndWith(end) {
return this.runMatcher('toEndWith', [end]);
}
/** @inheritdoc [[IStringExpectation.toMatch]] */
toMatch(regexp) {
return this.runMatcher('toMatch', [regexp]);
}
// IArrayExpectation
/** @inheritdoc [[IArrayExpectation.toHaveLength]] */
toHaveLength(count) {
return this.runMatcher('toHaveLength', [count]);
}
/** @inheritdoc [[IArrayExpectation.toContain]] */
toContain(value) {
return this.runMatcher('toContain', [value]);
}
/** @inheritdoc [[IArrayExpectation.toHaveAtPosition]] */
toHaveAtPosition(value, position) {
return this.runMatcher('toHaveAtPosition', [value, position]);
}
/** @inheritdoc [[IArrayExpectation.allToSatisfy]] */
allToSatisfy(criteria) {
return this.runMatcher('allToSatisfy', [criteria]);
}
/** @inheritdoc [[IArrayExpectation.anyToSatisfy]] */
anyToSatisfy(criteria) {
return this.runMatcher('anyToSatisfy', [criteria]);
}
/** @inheritdoc [[IArrayExpectation.amountToSatisfy]] */
amountToSatisfy(count, criteria) {
return this.runMatcher('amountToSatisfy', [count, criteria]);
}
// IObjectExpectation
/** @inheritdoc [[IObjectExpectation.toHavePropertyCount]] */
toHavePropertyCount(count) {
return this.runMatcher('toHavePropertyCount', [count]);
}
/** @inheritdoc [[IObjectExpectation.toHaveAtLeast]] */
toHaveAtLeast(keys) {
return this.runMatcher('toHaveAtLeast', keys, false);
}
/** @inheritdoc [[IObjectExpectation.toHaveNoOtherThan]] */
toHaveNoOtherThan(keys) {
return this.runMatcher('toHaveNoOtherThan', keys, false);
}
/** @inheritdoc [[IObjectExpectation.toHaveProperty]] */
toHaveProperty(propertyName) {
return this.runMatcher('toHaveProperty', [propertyName]);
}
/** @inheritdoc [[IObjectExpectation.toBeInstanceOf]] */
// eslint-disable-next-line @typescript-eslint/ban-types
toBeInstanceOf(classConstructor) {
return this.runMatcher('toBeInstanceOf', [classConstructor]);
}
// IFinishedExpectation
/** @inheritdoc [[IFinishedExpectation.getResult]] */
getResult() {
return this.result;
}
/**
* Set the given value as the result of this
* expectation. The result is directly set, when
* no previous result existed, or joined with a
* logic conjunction with the previous result if
* a value already exists.
*
* @value The value to set.
*/
setResult(value) {
if (this.result === undefined) {
this.result = value;
}
else {
this.result = this.result && value;
}
}
/**
* Run a matcher with the given name, passing the
* querying element as a first argument, and all additional
* given arguments. The result of running the matcher is stores,
* and a new state is pushed to this particular matcher.
*
* @param matcherName The matcher name to run
* @param args The arguments to pass to the matcher
*/
runMatcher(matcherName, args, sparse = true) {
const matcherArgs = sparse ? [this.element, ...args] : [this.element, args];
const matcherResult = Matchers[matcherName].call(this, ...matcherArgs);
const result = this.isNot ? !matcherResult : matcherResult;
this.states.push({
matcher: matcherName,
args,
result
});
this.setResult(result);
return this;
}
}
/**
* This module provides the [[JoinedExpectation]] class that provides
* a way to create an expectation that has a result the result of
* applying the joiner to every expectation in the result.
*
* @author Alan Rodas Bonjour <alanrodas@gmail.com>
*
* @packageDocumentation
*/
/**
* A joined expectation consist of multiple expectations joined by a specific
* joiner function. A JoinedExpectation implements [[FinishedExpectation]],
* where the result is calculated using the given joiner function.
*
* Currently two join forms are provided, [[Expectations/Expectations.expect.and]],
* and [[Expectations/Expectations.expect.or]].
*/
class JoinedExpectation extends FinishedExpectation {
/**
* Create a new instance of a JoinedExpectation for the given set
* of expectations, using the provided joiner.
*
* @param expectations The expectations that ought to be joined.
* @param joiner The joiner to use to calculate the result.
*/
constructor(expectations, joiner) {
super();
this.result = joiner(expectations);
}
/** @inheritdoc [[IFinishedExpectancy.getResult]] */
getResult() {
return this.result;
}
}
// eslint-disable-next-line max-len
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions, @typescript-eslint/explicit-module-boundary-types
function expect(element) {
return new Expectation(element);
}
/**
* This namespace provides additional hany operations for the expect function.
*/
(function (expect) {
/**
* Create a new [[JoinedExpectation]] where all the expectations need to have a `true` result
* in order for the result of the joined one to be also `true`. That is, an expectation
* that joins it's components with a logical and.
* @param expectations A list of expectations that need to be fulfilled in order to
* return `true` as result.
*/
expect.and = (...expectations) => new JoinedExpectation(expectations, (exp) => exp.reduce((r, e) => r && e.getResult(), true));
/**
* Create a new [[JoinedExpectation]] where any of the expectations need to have a `true` result
* in order for the result of the joined one to be also `true`. That is, an expectation
* that joins it's components with a logical or.
* @param expectations A list of expectations where one need to be fulfilled in order to
* return `true` as result.
*/
expect.or = (...expectations) => new JoinedExpectation(expectations, (exp) => exp.reduce((r, e) => r || e.getResult(), false));
})(expect || (expect = {}));
/**
* This object contains the default values for a [[Board]] and it's cells.
* When a specific value is not given, the defaults are used.
*/
const Defaults = {
[Color.Blue]: 0,
[Color.Black]: 0,
[Color.Red]: 0,
[Color.Green]: 0
};
class Cell extends TypedEmitter$1 {
/**
* Create a new instance of a cell with the given cell information.
* A cell should be given the [[Board]] it belongs to, as a cell cannot exist
* without a board. Additionally, at least the [x, y] coordinate of the cell
* within the board should be passed as the cell's information, and optionally,
* the amount of stones of the different colors in case they are not zero.
*
* When creating a cell and passing color information, you should prefer using
* the Color enum as a key, instead of the enum value as a string.
* @example
* ```
* new Cell(board, { x: 3, y: 2, [Color.Red]: 5, [Color.Green]: 1 });
* ```
* This allows for abstracting away the inner representation of the enum, and allow
* for changes in the future without impacting your code.
*
* @param board The board this cell belongs to.
* @param cellInfo The information for this cell, at least the X and Y coordinates,
* and optionally, amount of stones for each color.
*/
constructor(board, cellInfo) {
var _a, _b, _c, _d;
super();
this.board = board;
this.locationX = cellInfo.x;
this.locationY = cellInfo.y;
this.blueStones = (_a = cellInfo[Color.Blue]) !== null && _a !== void 0 ? _a : Defaults[Color.Blue];
this.blackStones = (_b = cellInfo[Color.Black]) !== null && _b !== void 0 ? _b : Defaults[Color.Black];
this.redStones = (_c = cellInfo[Color.Red]) !== null && _c !== void 0 ? _c : Defaults[Color.Red];
this.greenStones = (_d = cellInfo[Color.Green]) !== null && _d !== void 0 ? _d : Defaults[Color.Green];
}
/* ************* Cloning ************** */
/**
* Clone this cell. Pass a board in order to set the
* associated board of the cloned cell to that element.
*
* @param cloneBoard The Board the cloned cell will be associated to.
* @returns A new [[Cell]]
*/
clone(newBoard) {
return new Cell(newBoard, {
x: this.x,
y: this.y,
[Color.Blue]: this.getStonesOf(Color.Blue),
[Color.Black]: this.getStonesOf(Color.Black),
[Color.Red]: this.getStonesOf(Color.Red),
[Color.Green]: this.getStonesOf(Color.Green)
});
}
/* ************* Accessors ************** */
/**
* Get or set the X location of this cell within the board.
*
* @warning The getter can be used always. The setter on the other hand
* although exported, should not be used, as it's usage is reserved
* for internal actions of the board only (It's used exclusively on
* recalculating coordinates when the board resizes). Avoid the
* setter at all cost.
*
* @param value The new value for the X coordinate.
*
* @returns This cells X coordinate within the board
*/
get x() {
return this.locationX;
}
set x(value) {
this.locationX = value;
}
/**
* Get or set the Y location of this cell within the board.
*
* @warning The getter can be used always. The setter on the other hand
* although exported, should not be used, as it's usage is reserved
* for internal actions of the board only (It's used exclusively on
* recalculating coordinates when the board resizes). Avoid the
* setter at all cost.
*
* @param value The new value for the Y coordinate.
*
* @returns This cells Y coordinate within the board
*/
get y() {
return this.locationY;
}
set y(value) {
this.locationY = value;
}
/**
* Get the amount of [[Color.Blue | blue]] stones of this cell.
* Or instead, set the amount of stones.
*
* @deprecated This is retain only for compatibility reasons.
* If you need to access the amount of stones of a color,
* use [[getStonesOf]] instead, passing the color as an argument.
* So the preferred method for blue stones should be
* ```
* cell.getStonesOf(Color.Blue);
* ```
* For setting the stones of a color, use [[setStonesOf]] instead,
* with the color and the new desired value. The preferred way for
* blue stones should be:
* ```
* cell.setStonesOf(Color.Blue, amount);
* ```
*
* @throws [[InvalidStonesAmount]] with the attempt set to `SetStones`
* if the new amount of stones is lower than zero.
*
* @param value The new amount of [[Color.Blue | blue]] stones
*
* @returns The number of stones of [[Color.Blue]].
*/
get a() {
return this.getStonesOf(Color.Blue);
}
/* istanbul ignore next */
set a(value) {
this.setStonesOf(Color.Blue, value);
}
/**
* Get the amount of [[Color.Black | black]] stones of this cell.
* Or instead, set the amount of stones.
*
* @deprecated This is retain only for compatibility reasons.
* If you need to access the amount of stones of a color,
* use [[getStonesOf]] instead, passing the color as an argument.
* So the preferred method for black stones should be
* ```
* cell.getStonesOf(Color.Black);
* ```
* For setting the stones of a color, use [[setStonesOf]] instead,
* with the color and the new desired value. The preferred way for
* black stones should be:
* ```
* cell.setStonesOf(Color.Black, amount);
* ```
*
* @throws [[InvalidStonesAmount]] with the attempt set to `SetStones`
* if the new amount of stones is lower than zero.
*
* @param value The new amount of [[Color.Black | black]] stones
*
* @returns The number of stones of [[Color.Black]].
*/
get n() {
return this.getStonesOf(Color.Black);
}
/* istanbul ignore next */
set n(value) {
this.setStonesOf(Color.Black, value);
}
/**
* Get the amount of [[Color.Red | red]] stones of this cell.
* Or i