@gamepark/rules-api
Version:
API to implement the rules of a board game
288 lines • 13.8 kB
JavaScript
"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