UNPKG

@gamepark/rules-api

Version:

API to implement the rules of a board game

288 lines 13.8 kB
"use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MaterialMoney = void 0; var cloneDeep_1 = __importDefault(require("lodash/cloneDeep")); var isEqual_1 = __importDefault(require("lodash/isEqual")); var keyBy_1 = __importDefault(require("lodash/keyBy")); var mapValues_1 = __importDefault(require("lodash/mapValues")); var sumBy_1 = __importDefault(require("lodash/sumBy")); var index_1 = require("./index"); var MaterialMutator_1 = require("./MaterialMutator"); /** * This subclass of {@link Material} is design to handle counting and moving Money with different units: coins of 5 and 1 for instance. * It also keeps track of how much money is left after spending moves are create so that it is easy to spend money multiple time at once, * without risking spending the same coins twice because the moves are no immediately executed. */ var MaterialMoney = /** @class */ (function (_super) { __extends(MaterialMoney, _super); /** * Construct a new Material Money helper instance * @param {number} type Type of items this instance will work on * @param {MaterialItem[]} items The complete list of items of this type in current game state. * @param {number[]} units The different units that exists in stock to count this money * @param {ItemEntry[]} entries The list of items to work on. Each entry consists of an array with the index of the item, and the item * @param {function} processMove if provided, this function will be executed on every move created with this instance */ function MaterialMoney(type, units, items, processMove, entries) { if (items === void 0) { items = []; } if (entries === void 0) { entries = Array.from(items.entries()).filter(function (entry) { return entry[1].quantity !== 0; }); } var _this = _super.call(this, type, items, processMove, entries) || this; _this.type = type; _this.units = units; _this.items = items; _this.processMove = processMove; _this.entries = entries; _this.pendingMoves = []; if (_this.units[0] === 1) { _this.units = __spreadArray([], units, true); _this.units.sort(function (a, b) { return b - a; }); // Sort units from highest to 1 } if (_this.units[_this.units.length - 1] !== 1) console.warn('Money without 1 in the possible values will produce unexpected outcomes'); return _this; } /** * Helper function to return a new instance of the same class (works also for children class) * @param {ItemEntry[]} entries Filtered entries for the new class * @returns {this} the new Material instance * @protected */ MaterialMoney.prototype.new = function (entries) { var Class = this.constructor; return new Class(this.type, this.units, this.items, this.processMove, entries); }; /** * We need to apply the pending moves before any filtering is done to get the right count for instance. */ MaterialMoney.prototype.filter = function (predicate) { this.applyPendingMoves(); return _super.prototype.filter.call(this, predicate); }; Object.defineProperty(MaterialMoney.prototype, "count", { /** * Count the total value of a material instance * @returns the sum of each item id multiplied by its quantity */ get: function () { this.applyPendingMoves(); return (0, sumBy_1.default)(this.getItems(), function (item) { var _a, _b; return ((_a = item.id) !== null && _a !== void 0 ? _a : 1) * ((_b = item.quantity) !== null && _b !== void 0 ? _b : 1); }); }, enumerable: false, configurable: true }); /** * Create an amount of Money and put it in given location * @param amount Amount to gain * @param location The location to filter material onto, and to create new items in * @returns the moves that need to be played to perform the operation */ MaterialMoney.prototype.addMoney = function (amount, location) { var _a; if (amount === 0) return []; if (amount < 0) return this.removeMoney(-amount, location); var moves = []; var gainMap = this.getGainMap(amount); for (var _i = 0, _b = this.units; _i < _b.length; _i++) { var unit = _b[_i]; if (gainMap[unit] > 0) { moves.push(this.createItem({ id: unit, location: location, quantity: gainMap[unit] })); } } (_a = this.pendingMoves).push.apply(_a, moves); return moves; }; /** * Remove an amount of Money from given location * @param amount Amount to spend * @param location The location to filter material onto, and to create new items in * @returns the moves that need to be played to perform the operation */ MaterialMoney.prototype.removeMoney = function (amount, location) { var _a; if (amount === 0) return []; if (amount < 0) return this.addMoney(-amount, location); this.applyPendingMoves(); if (this.entries.some(function (_a) { var item = _a[1]; return !(0, isEqual_1.default)(item.location, location); })) { return this.location(function (l) { return (0, isEqual_1.default)(l, location); }).removeMoney(amount, location); } var moves = []; var spendMap = this.getSpendMap(amount); for (var _i = 0, _b = this.units; _i < _b.length; _i++) { var unit = _b[_i]; if (spendMap[unit] < 0) { moves.push(this.id(unit).deleteItem(-spendMap[unit])); } else if (spendMap[unit] > 0) { moves.push(this.createItem({ id: unit, location: location, quantity: spendMap[unit] })); } } (_a = this.pendingMoves).push.apply(_a, moves); return moves; }; /** * Move an amount of money from a place to another place. It searches after the easiest way to do it, making money with the bank only if necessary. * @param origin Location to remove money from * @param target Location to move money to * @param amount Amount of money to transfer * @returns the moves that need to be played to perform the operation */ MaterialMoney.prototype.moveMoney = function (origin, target, amount) { var _a; if (amount === 0) return []; if (amount < 0) return this.moveMoney(target, origin, -amount); this.applyPendingMoves(); var moves = []; var originMoney = this.location(function (l) { return (0, isEqual_1.default)(l, origin); }); var targetMoney = this.location(function (l) { return (0, isEqual_1.default)(l, target); }); var originDelta = originMoney.getSpendMap(amount); var targetDelta = targetMoney.getGainMap(amount); for (var _i = 0, _b = this.units; _i < _b.length; _i++) { var unit = _b[_i]; if (originDelta[unit] < 0) { var _loop_1 = function () { var lowerUnits = this_1.units.slice(this_1.units.indexOf(unit) + 1); var targetResultDelta = targetMoney.getSpendMap(unit); var valueSpent = (0, sumBy_1.default)(lowerUnits, function (unit) { return -targetResultDelta[unit] * unit; }); if (valueSpent === unit && lowerUnits.every(function (lowerUnit) { return targetResultDelta[lowerUnit] < 0; })) { targetDelta[unit]++; for (var _c = 0, lowerUnits_1 = lowerUnits; _c < lowerUnits_1.length; _c++) { var lowerUnit = lowerUnits_1[_c]; targetDelta[lowerUnit] += targetResultDelta[lowerUnit]; } } else return "break"; }; var this_1 = this; while (targetDelta[unit] < -originDelta[unit]) { var state_1 = _loop_1(); if (state_1 === "break") break; } var moveAmount = Math.min(-originDelta[unit], targetDelta[unit]); targetDelta[unit] -= moveAmount; var originMaterialUnit = originMoney.id(unit); if (moveAmount > 0) { moves.push(originMaterialUnit.moveItem(target, moveAmount)); } if (moveAmount < -originDelta[unit]) { moves.push(originMaterialUnit.deleteItem(-originDelta[unit] - moveAmount)); } } else if (originDelta[unit] > 0) { if (targetDelta[unit] < 0) { moves.push(targetMoney.id(unit).moveItem(origin, -targetDelta[unit])); } else { moves.push(originMoney.createItem({ id: unit, location: origin, quantity: originDelta[unit] })); } } if (targetDelta[unit] > 0) { moves.push(targetMoney.createItem({ id: unit, location: target, quantity: targetDelta[unit] })); } } (_a = this.pendingMoves).push.apply(_a, moves); return moves; }; /** * Return the best way to gain an amount, prioritizing the highest unit values * @param amount Amount to gain, default 1 * @returns the record of coins to earn (only positive values) */ MaterialMoney.prototype.getGainMap = function (amount) { var map = (0, mapValues_1.default)((0, keyBy_1.default)(this.units), function (_) { return 0; }); for (var _i = 0, _a = this.units; _i < _a.length; _i++) { var unit = _a[_i]; map[unit] = Math.floor(amount / unit); amount %= unit; } return map; }; /** * Return the best way to spend an amount of owned units, prioritizing the smallest unit values * @param amount Amount to gain, default 1 * @returns the record of coins to give away and eventually take (positive and negative values) */ MaterialMoney.prototype.getSpendMap = function (amount) { var _this = this; var owned = (0, mapValues_1.default)((0, keyBy_1.default)(this.units), function (unit) { return _this.id(unit).getQuantity(); }); var map = (0, mapValues_1.default)((0, keyBy_1.default)(this.units), function (_) { return 0; }); for (var _1 = 0; _1 < amount; _1++) { for (var i = this.units.length - 1; i >= 0; i--) { var unit = this.units[i]; if (owned[unit] + map[unit] > 0) { map[unit]--; if (unit > 1) { var rest = unit - 1; for (var _i = 0, _a = this.units.slice(i + 1); _i < _a.length; _i++) { var lowerUnit = _a[_i]; if (lowerUnit <= rest) { map[lowerUnit] += Math.floor(rest / lowerUnit); rest -= (rest % lowerUnit) * lowerUnit; } } } break; } } } return map; }; /** * Mutate the entries to get the state after * @private */ MaterialMoney.prototype.applyPendingMoves = function () { var _this = this; if (!this.pendingMoves.length) return; if (this.items.some(function (item, index) { return item.quantity !== 0 && !_this.entries.some(function (entry) { return entry[0] === index; }); })) { console.warn('MaterialMoney cannot track the state of the items on filtered instances, the filter will be cancelled'); } this.items = (0, cloneDeep_1.default)(this.items); var mutator = new MaterialMutator_1.MaterialMutator(this.type, this.items); while (this.pendingMoves.length > 0) { mutator.applyMove(this.pendingMoves.shift()); } this.entries = Array.from(this.items.entries()).filter(function (entry) { return entry[1].quantity !== 0; }); }; return MaterialMoney; }(index_1.Material)); exports.MaterialMoney = MaterialMoney; //# sourceMappingURL=MaterialMoney.js.map