UNPKG

@gamepark/rules-api

Version:

API to implement the rules of a board game

305 lines 16 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 __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.hideFront = exports.hideItemId = exports.HiddenMaterialRules = void 0; var difference_1 = __importDefault(require("lodash/difference")); var get_1 = __importDefault(require("lodash/get")); var isEqual_1 = __importDefault(require("lodash/isEqual")); var keys_1 = __importDefault(require("lodash/keys")); var mapValues_1 = __importDefault(require("lodash/mapValues")); var set_1 = __importDefault(require("lodash/set")); var unset_1 = __importDefault(require("lodash/unset")); var MaterialRules_1 = require("./MaterialRules"); var moves_1 = require("./moves"); /** * Implement HiddenMaterialRules when you want to use the {@link MaterialRules} approach with {@link HiddenInformation}. * Using some {@link HidingStrategy} allows to enforce the security of a game with hidden information easily. * If the game has secret information (some players have information not available to others, link cards in their hand), then you * must implement {@link SecretMaterialRules} instead. */ var HiddenMaterialRules = /** @class */ (function (_super) { __extends(HiddenMaterialRules, _super); function HiddenMaterialRules(game, client) { var _this = _super.call(this, game) || this; _this.client = client; return _this; } HiddenMaterialRules.prototype.randomize = function (move, player) { if (player !== undefined && this.isRevealingItemMove(move, player)) { // We need to know if a MoveItem has revealed something to the player to prevent the undo in that case. // To know that, we need the position of the item before the move. // To prevent having to recalculate the game state before the move, we flag the move in the database with "reveal: {}". // This flag indicate that something was revealed to someone. // We use the "randomize" function because is the where we can "preprocess" the move and transform it after checking it is legal and before it is saved. return __assign(__assign({}, move), { reveal: {} }); } return _super.prototype.randomize.call(this, move); }; HiddenMaterialRules.prototype.isRevealingItemMove = function (move, player) { return ((0, moves_1.isMoveItem)(move) && this.moveItemWillRevealSomething(move, player)) || ((0, moves_1.isMoveItemsAtOnce)(move) && this.moveAtOnceWillRevealSomething(move, player)); }; /** * Items that can be hidden cannot merge by default, to prevent hidden items to merge only because they have no id. */ HiddenMaterialRules.prototype.itemsCanMerge = function (type) { return !this.hidingStrategies[type]; }; /** * Moves that reveal some information (like drawing a card) cannot be predicted by the player. */ HiddenMaterialRules.prototype.isUnpredictableMove = function (move, player) { var _this = this; if ((0, moves_1.isMoveItem)(move)) { return this.moveItemWillRevealSomething(move, player); } else if ((0, moves_1.isMoveItemsAtOnce)(move)) { return this.moveAtOnceWillRevealSomething(move, player); } else if ((0, moves_1.isCreateItem)(move)) { return this.itemHasHiddenInformation(move.itemType, move.item, player); } else if ((0, moves_1.isCreateItemsAtOnce)(move)) { return move.items.some(function (item) { return _this.itemHasHiddenInformation(move.itemType, item, player); }); } else if ((0, moves_1.isShuffle)(move)) { return true; } else { return _super.prototype.isUnpredictableMove.call(this, move, player); } }; /** * Moves than reveals an information to someone cannot be undone by default */ HiddenMaterialRules.prototype.moveBlocksUndo = function (move, player) { return _super.prototype.moveBlocksUndo.call(this, move, player) || this.moveRevealedSomething(move); }; /** * @param move A move to test * @returns true if the move revealed something to some player */ HiddenMaterialRules.prototype.moveRevealedSomething = function (move) { return ((0, moves_1.isMoveItem)(move) || (0, moves_1.isMoveItemsAtOnce)(move)) && !!move.reveal; }; /** * With the material approach, we can offer a default working implementation for {@link HiddenInformation.getView} */ HiddenMaterialRules.prototype.getView = function (player) { var _this = this; return __assign(__assign({}, this.game), { items: (0, mapValues_1.default)(this.game.items, function (items, stringType) { var itemsType = parseInt(stringType); var hidingStrategies = _this.hidingStrategies[itemsType]; if (!hidingStrategies || !items) return items; return items.map(function (item) { return _this.hideItem(itemsType, item, player); }); }) }); }; HiddenMaterialRules.prototype.hideItem = function (type, item, player) { var paths = this.getItemHiddenPaths(type, item, player); if (!paths.length) return item; var hiddenItem = JSON.parse(JSON.stringify(item)); for (var _i = 0, paths_1 = paths; _i < paths_1.length; _i++) { var path = paths_1[_i]; (0, unset_1.default)(hiddenItem, path); } return hiddenItem; }; HiddenMaterialRules.prototype.getItemHiddenPaths = function (type, item, player) { var _a; var hidingStrategy = (_a = this.hidingStrategies[type]) === null || _a === void 0 ? void 0 : _a[item.location.type]; var hiddenPaths = hidingStrategy ? hidingStrategy(item, player) : []; return hiddenPaths.flatMap(function (path) { if (!path) console.error('Empty paths are not allowed in hiding strategies'); var itemAtPath = (0, get_1.default)(item, path); if (typeof itemAtPath === 'object') { return (0, keys_1.default)(itemAtPath).map(function (key) { return path + '.' + key; }); } else { return [path]; } }); }; HiddenMaterialRules.prototype.itemHasHiddenInformation = function (type, item, player) { return this.getItemHiddenPaths(type, item, player).length > 0; }; /** * To be able to know if a MoveItem cannot be undone, the server flags the moves with a "reveal" property. * This difference must be integrated without error during the callback. */ HiddenMaterialRules.prototype.canIgnoreServerDifference = function (clientMove, serverMove) { if ((0, moves_1.isMoveItem)(clientMove) && (0, moves_1.isMoveItem)(serverMove)) { var reveal = serverMove.reveal, serverMoveWithoutReveal = __rest(serverMove, ["reveal"]); return (0, isEqual_1.default)(clientMove, serverMoveWithoutReveal); } return false; }; /** * With the material approach, we can offer a default working implementation for {@link HiddenInformation.getMoveView} */ HiddenMaterialRules.prototype.getMoveView = function (move, player) { var _this = this; if (move.kind === moves_1.MoveKind.ItemMove && move.itemType in this.hidingStrategies) { switch (move.type) { case moves_1.ItemMoveType.Move: return this.getMoveItemView(move, player); case moves_1.ItemMoveType.MoveAtOnce: return this.getMoveAtOnceView(move, player); case moves_1.ItemMoveType.Create: return __assign(__assign({}, move), { item: this.hideItem(move.itemType, move.item, player) }); case moves_1.ItemMoveType.CreateAtOnce: return __assign(__assign({}, move), { items: move.items.map(function (item) { return _this.hideItem(move.itemType, item, player); }) }); case moves_1.ItemMoveType.Shuffle: return this.getShuffleItemsView(move, player); } } return move; }; HiddenMaterialRules.prototype.getMoveItemView = function (move, player) { var revealedPaths = this.getMoveItemRevealedPath(move, player); if (!revealedPaths.length) return move; var item = this.material(move.itemType).getItem(move.itemIndex); var moveView = __assign(__assign({}, move), { reveal: {} }); for (var _i = 0, revealedPaths_1 = revealedPaths; _i < revealedPaths_1.length; _i++) { var path = revealedPaths_1[_i]; (0, set_1.default)(moveView.reveal, path, (0, get_1.default)(item, path)); } return moveView; }; HiddenMaterialRules.prototype.getMoveAtOnceView = function (move, player) { var moveView = __assign({}, move); for (var _i = 0, _a = move.indexes; _i < _a.length; _i++) { var index = _a[_i]; var revealedPaths = this.getMoveAtOnceRevealedPath(move, index, player); if (!revealedPaths.length) continue; if (!moveView.reveal) moveView.reveal = {}; moveView.reveal[index] = {}; var item = this.material(move.itemType).getItem(index); for (var _b = 0, revealedPaths_2 = revealedPaths; _b < revealedPaths_2.length; _b++) { var path = revealedPaths_2[_b]; (0, set_1.default)(moveView.reveal[index], path, (0, get_1.default)(item, path)); } } return moveView; }; HiddenMaterialRules.prototype.getMoveItemRevealedPath = function (move, player) { var item = this.material(move.itemType).getItem(move.itemIndex); var hiddenPathsBefore = this.getItemHiddenPaths(move.itemType, item, player); var hiddenPathsAfter = this.getItemHiddenPaths(move.itemType, this.mutator(move.itemType).getItemAfterMove(move), player); return (0, difference_1.default)(hiddenPathsBefore, hiddenPathsAfter); }; HiddenMaterialRules.prototype.getMoveAtOnceRevealedPath = function (move, itemIndex, player) { var item = this.material(move.itemType).getItem(itemIndex); var hiddenPathsBefore = this.getItemHiddenPaths(move.itemType, item, player); var hiddenPathsAfter = this.getItemHiddenPaths(move.itemType, this.mutator(move.itemType).getItemAfterMoveAtOnce(move, itemIndex), player); return (0, difference_1.default)(hiddenPathsBefore, hiddenPathsAfter); }; HiddenMaterialRules.prototype.moveItemWillRevealSomething = function (move, player) { return this.getMoveItemRevealedPath(move, player).length > 0; }; HiddenMaterialRules.prototype.moveAtOnceWillRevealSomething = function (move, player) { var _this = this; return move.indexes.some(function (index) { return _this.getMoveAtOnceRevealedPath(move, index, player).length; }); }; HiddenMaterialRules.prototype.getShuffleItemsView = function (move, player) { if (this.canSeeShuffleResult(move, player)) return move; var newIndexes = move.newIndexes, moveView = __rest(move, ["newIndexes"]); return moveView; }; HiddenMaterialRules.prototype.canSeeShuffleResult = function (move, player) { var _this = this; if (!this.hidingStrategies[move.itemType]) return true; var material = this.material(move.itemType); var hiddenPaths = this.getItemHiddenPaths(move.itemType, material.getItem(move.indexes[0]), player); if (process.env.NODE_ENV === 'development' && move.indexes.some(function (index) { return !(0, isEqual_1.default)(hiddenPaths, _this.getItemHiddenPaths(move.itemType, material.getItem(index), player)); })) { throw new RangeError("You cannot shuffle items with different hiding strategies: ".concat(JSON.stringify(move.indexes.map(function (index) { return _this.getItemHiddenPaths(move.itemType, material.getItem(index), player); })))); } // TODO: if we shuffle a hand of items partially hidden, we should send the partially visible information to the client. // Example: It's a Wonderful World with the Extension: the back face of the player's hand are different // => when the hand is shuffled we should see where the expansion cards land. return !hiddenPaths.length; }; /** * Override of {@link MaterialRules.play} that also removes the hidden information from items, for example when a card is flipped face down */ HiddenMaterialRules.prototype.play = function (move, context) { var result = _super.prototype.play.call(this, move, context); if (this.client && (0, moves_1.isMoveItem)(move) && this.hidingStrategies[move.itemType]) { var item = this.material(move.itemType).getItem(move.itemIndex); if (item) { this.game.items[move.itemType][move.itemIndex] = this.hideItem(move.itemType, item, this.client.player); } } if (this.client && (0, moves_1.isMoveItemsAtOnce)(move) && this.hidingStrategies[move.itemType]) { for (var _i = 0, _a = move.indexes; _i < _a.length; _i++) { var index = _a[_i]; var item = this.material(move.itemType).getItem(index); if (item) { this.game.items[move.itemType][index] = this.hideItem(move.itemType, item, this.client.player); } } } return result; }; return HiddenMaterialRules; }(MaterialRules_1.MaterialRules)); exports.HiddenMaterialRules = HiddenMaterialRules; /** * Hiding strategy that removes the item id */ var hideItemId = function () { return ['id']; }; exports.hideItemId = hideItemId; /** * Hiding strategy that removes "id.front" from the item (when we have cards with composite ids, back & front) */ var hideFront = function () { return ['id.front']; }; exports.hideFront = hideFront; //# sourceMappingURL=HiddenMaterialRules.js.map