UNPKG

@gobstones/gobstones-gbb-parser

Version:

A Parser/Stringifier for GBB (Gobstones Board) file format

1,342 lines (1,326 loc) 194 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var require$$0 = require('events'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var require$$0__default = /*#__PURE__*/_interopDefaultLegacy(require$$0); /** * 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__default['default'].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 numbe