UNPKG

@gamepark/rules-api

Version:

API to implement the rules of a board game

487 lines 23.2 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 __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MaterialRules = void 0; var difference_1 = __importDefault(require("lodash/difference")); var random_1 = __importDefault(require("lodash/random")); var shuffle_1 = __importDefault(require("lodash/shuffle")); var union_1 = __importDefault(require("lodash/union")); var Rules_1 = require("../Rules"); var TimeLimit_1 = require("../TimeLimit"); var items_1 = require("./items"); var memory_1 = require("./memory"); var moves_1 = require("./moves"); var rules_1 = require("./rules"); /** * The MaterialRules class is the main class to implement the rules of a board game with the "Material oriented" approach. * With this approach, the game state and the game moves is structured around the game material items and their movements. * The rules are also automatically split into small parts. * Finally, a "memory" util is available to store temporary information which is not part of the material or the rules state. * If you need to implement game with hidden information, see {@link HiddenMaterialRules} or {@link SecretMaterialRules}. * * @typeparam Player - identifier of a player. Either a number or a numeric enum (eg: PlayerColor) * @typeparam MaterialType - Numeric enum of the types of material manipulated in the game * @typeparam LocationType - Numeric enum of the types of location in the game where the material can be located */ var MaterialRules = /** @class */ (function (_super) { __extends(MaterialRules, _super); function MaterialRules() { var _this = _super !== null && _super.apply(this, arguments) || this; /** * The "location strategies" are global rules that always apply in a game when we want to maintain a consistency in the position of the material. * For example, we usually want the cards in a player hand to always go from x=0 to x=n without a gap, so we use a {@link PositiveSequenceStrategy} to enforce * the rule once and for all. If we want to create a "river" of card we can use a {@link FillGapStrategy}. * Games with more complex use cases can implement their own {@link LocationStrategy}. */ _this.locationsStrategies = {}; _this.startPlayerTurn = moves_1.MaterialMoveBuilder.startPlayerTurn; _this.startSimultaneousRule = moves_1.MaterialMoveBuilder.startSimultaneousRule; _this.startRule = moves_1.MaterialMoveBuilder.startRule; _this.customMove = moves_1.MaterialMoveBuilder.customMove; _this.endGame = moves_1.MaterialMoveBuilder.endGame; return _this; } /** * Helper function to manipulate the material items of the game. See {@link Material}. * * @param type The type of Material we want to work on * @returns a Material instance to manipulate all the material of that type in current game state. */ MaterialRules.prototype.material = function (type) { return new items_1.Material(type, this.game.items[type]); }; Object.defineProperty(MaterialRules.prototype, "players", { /** * Shortcut for this.game.players * @returns array of the players identifiers */ get: function () { return this.game.players; }, enumerable: false, configurable: true }); Object.defineProperty(MaterialRules.prototype, "activePlayer", { /** * @return the active player if exactly one player is active */ get: function () { return this.getActivePlayer(); }, enumerable: false, configurable: true }); Object.defineProperty(MaterialRules.prototype, "activePlayers", { /** * @returns all the active players */ get: function () { var _a, _b, _c; return ((_a = this.game.rule) === null || _a === void 0 ? void 0 : _a.player) !== undefined ? [this.game.rule.player] : (_c = (_b = this.game.rule) === null || _b === void 0 ? void 0 : _b.players) !== null && _c !== void 0 ? _c : []; }, enumerable: false, configurable: true }); /** * Utility function to access the memory tool for the game or on player. * this.game.memory can be used to store any data that is not available through the state of the material, or current rule. * * @param player Optional, identifier of the player if we want to manipulate a specific player's memory * @returns {@link GameMemory} or {@link PlayerMemory} utility * @protected */ MaterialRules.prototype.getMemory = function (player) { return player === undefined ? new memory_1.GameMemory(this.game) : new memory_1.PlayerMemory(this.game, player); }; /** * Save a new value inside the memory. * @param key The key to index the memorized value. * @param value Any JSON serializable value to store, or a function that takes previous stored value and returns the new value to store. * @param player optional, if we need to memorize a different value for each player. */ MaterialRules.prototype.memorize = function (key, value, player) { return this.getMemory(player).memorize(key, value); }; /** * Retrieve the value memorized under a given key. * Shortcut for this.game.memory[key] or this.game.memory[key][player] * * @param key Key under which the memory is store. Usually a value of a numeric enum named "Memory". * @param player optional, if we need to memorize a different value for each player. */ MaterialRules.prototype.remind = function (key, player) { return this.getMemory(player).remind(key); }; /** * Delete a value from the memory * @param key Key of the value to delete * @param player optional, if we need to memorize a different value for each player. */ MaterialRules.prototype.forget = function (key, player) { this.getMemory(player).forget(key); }; Object.defineProperty(MaterialRules.prototype, "rulesStep", { /** * Instantiates the class that handled the rules of the game corresponding to the current rule id. * This function reads the current value in this.game.rule.id and instantiate the corresponding class in the {@link rules} property. * * @returns the class that handled the rules of the game, at current specific game state. */ get: function () { if (!this.game.rule) return; var RulesStep = this.rules[this.game.rule.id]; if (!RulesStep) { console.error("The rules class for rules id ".concat(this.game.rule.id, " is missing")); return; } return new RulesStep(this.game); }, enumerable: false, configurable: true }); /** * Returns a utility class to change the state of the items. * Used by the framework to apply the Material moves on the items (should not be manipulated directly in the games). * * @param type MaterialType of the item we want to modify * @returns a MaterialMutator to change the state of the items */ MaterialRules.prototype.mutator = function (type) { if (!this.game.items[type]) this.game.items[type] = []; return new items_1.MaterialMutator(type, this.game.items[type], this.locationsStrategies[type], this.itemsCanMerge(type)); }; /** * Items can sometime be stored with a quantity (for example, coins). * By default, if you create or move similar items to the exact same location, they will merge into one item with a quantity. * However, if you have 2 cards that can be at the same spot for a short time (swapping or replacing), you can override this function to prevent them to merge * * @param _type type of items * @returns true if items can merge into one item with a quantity (default behavior) */ MaterialRules.prototype.itemsCanMerge = function (_type) { return true; }; /** * In the material approach, the rules behavior is delegated to the current {@link rulesStep}. See {@link Rules.delegate} */ MaterialRules.prototype.delegate = function () { return this.rulesStep; }; /** * Randomize Shuffle of Roll moves (see {@link RandomMove.randomize}) * @param move The Material Move to randomize * @returns the randomized move */ MaterialRules.prototype.randomize = function (move) { if ((0, moves_1.isShuffle)(move)) { return __assign(__assign({}, move), { newIndexes: (0, shuffle_1.default)(move.indexes) }); } else if ((0, moves_1.isRoll)(move)) { return __assign(__assign({}, move), { location: __assign(__assign({}, move.location), { rotation: this.roll(move) }) }); } return move; }; /** * When a RollItem move is create, it has to be randomized on the server side before it is saved and shared. * This function provides the random value. By default, it returns a value between 0 and 5 assuming a 6 sided dice is rolled. * If you need to flip a coin or roll non-cubic dice, you need to override this function. * * @param _move The RollItem move to randomize * @returns a random rolled value, by default a value between 0 and 5 (cubic dice result) */ MaterialRules.prototype.roll = function (_move) { return (0, random_1.default)(5); }; /** * Execution of the Material Moves. See {@link Rules.play}. * * @param move Material move to play on the game state * @param context Context in which the move was played * @returns Consequences of the move */ MaterialRules.prototype.play = function (move, context) { var consequences = []; var rulesStep = this.rulesStep; switch (move.kind) { case moves_1.MoveKind.ItemMove: consequences.push.apply(consequences, this.onPlayItemMove(move, context)); break; case moves_1.MoveKind.RulesMove: consequences.push.apply(consequences, this.onPlayRulesMove(move, context)); break; case moves_1.MoveKind.CustomMove: consequences.push.apply(consequences, this.onCustomMove(move, context)); if (rulesStep) { consequences.push.apply(consequences, rulesStep.onCustomMove(move, context)); } break; case moves_1.MoveKind.LocalMove: switch (move.type) { case moves_1.LocalMoveType.DisplayHelp: this.game.helpDisplay = move.helpDisplay; break; case moves_1.LocalMoveType.DropItem: if (!this.game.droppedItems) { this.game.droppedItems = []; } this.game.droppedItems.push(move.item); break; case moves_1.LocalMoveType.SetTutorialStep: this.game.tutorial.step = move.step; this.game.tutorial.stepComplete = false; this.game.tutorial.popupClosed = false; break; case moves_1.LocalMoveType.CloseTutorialPopup: this.game.tutorial.popupClosed = true; } } var endGameIndex = consequences.findIndex(moves_1.isEndGame); if (endGameIndex !== -1) { return consequences.slice(0, endGameIndex + 1); } return consequences; }; MaterialRules.prototype.onPlayItemMove = function (move, context) { var consequences = []; var rulesStep = this.rulesStep; consequences.push.apply(consequences, this.beforeItemMove(move, context)); if (rulesStep && !(context === null || context === void 0 ? void 0 : context.transient)) { consequences.push.apply(consequences, rulesStep.beforeItemMove(move, context)); } if (!this.game.items[move.itemType]) this.game.items[move.itemType] = []; var mutator = this.mutator(move.itemType); mutator.applyMove(move); if (this.game.droppedItems) { this.game.droppedItems = this.game.droppedItems.filter(function (droppedItem) { if (move.itemType !== droppedItem.type) { return true; } switch (move.type) { case moves_1.ItemMoveType.Move: case moves_1.ItemMoveType.Delete: return move.itemIndex !== droppedItem.index; case moves_1.ItemMoveType.MoveAtOnce: return !move.indexes.includes(droppedItem.index); } }); } var indexes = getItemMoveIndexes(move); if (context === null || context === void 0 ? void 0 : context.transient) { if (!this.game.transientItems) this.game.transientItems = {}; this.game.transientItems[move.itemType] = (0, union_1.default)(this.game.transientItems[move.itemType], indexes); } else if (this.game.transientItems) { this.game.transientItems[move.itemType] = (0, difference_1.default)(this.game.transientItems[move.itemType], indexes); } consequences.push.apply(consequences, this.afterItemMove(move, context)); if (rulesStep && !(context === null || context === void 0 ? void 0 : context.transient)) { consequences.push.apply(consequences, rulesStep.afterItemMove(move, context)); } return consequences; }; MaterialRules.prototype.beforeItemMove = function (_move, _context) { return []; }; MaterialRules.prototype.afterItemMove = function (_move, _context) { return []; }; MaterialRules.prototype.onCustomMove = function (_move, _context) { return []; }; MaterialRules.prototype.onPlayRulesMove = function (move, context) { var _a, _b; var consequences = []; var rulesStep = this.rulesStep; if (move.type === moves_1.RuleMoveType.EndPlayerTurn) { if ((_b = (_a = this.game.rule) === null || _a === void 0 ? void 0 : _a.players) === null || _b === void 0 ? void 0 : _b.includes(move.player)) { this.game.rule.players = this.game.rule.players.filter(function (player) { return player !== move.player; }); if ((0, rules_1.isSimultaneousRule)(rulesStep)) { consequences.push.apply(consequences, rulesStep.onPlayerTurnEnd(move, context)); if (this.game.rule.players.length === 0) { consequences.push.apply(consequences, rulesStep.getMovesAfterPlayersDone()); } } } else { console.warn("".concat(this.constructor.name, ": endPlayerTurn was triggered for player ").concat(move.player, " which is already inactive: ").concat(JSON.parse(JSON.stringify(this.game.rule)))); } } else { consequences.push.apply(consequences, this.changeRule(move, context)); } return consequences; }; MaterialRules.prototype.changeRule = function (move, context) { var _a, _b, _c, _d, _e, _f; var moves = (_b = (_a = this.rulesStep) === null || _a === void 0 ? void 0 : _a.onRuleEnd(move, context)) !== null && _b !== void 0 ? _b : []; var rule = this.game.rule; switch (move.type) { case moves_1.RuleMoveType.StartPlayerTurn: this.game.rule = { id: move.id, player: move.player }; break; case moves_1.RuleMoveType.StartSimultaneousRule: this.game.rule = { id: move.id, players: (_c = move.players) !== null && _c !== void 0 ? _c : this.game.players }; break; case moves_1.RuleMoveType.StartRule: this.game.rule = { id: move.id, player: (_d = this.game.rule) === null || _d === void 0 ? void 0 : _d.player }; break; case moves_1.RuleMoveType.EndGame: delete this.game.rule; break; } return moves.concat((_f = (_e = this.rulesStep) === null || _e === void 0 ? void 0 : _e.onRuleStart(move, rule, context)) !== null && _f !== void 0 ? _f : []); }; /** * By default, a Material Move can be undone if no player became active and no dice was rolled. * See {@link Undo.canUndo} and {@link HiddenMaterialRules.canUndo} * * @param action Action to consider * @param consecutiveActions Action played in between * @returns true if the action can be undone by the player that played it */ MaterialRules.prototype.canUndo = function (action, consecutiveActions) { for (var i = consecutiveActions.length - 1; i >= 0; i--) { if (this.consecutiveActionBlocksUndo(action, consecutiveActions[i])) { return false; } } return !this.actionBlocksUndo(action); }; MaterialRules.prototype.consecutiveActionBlocksUndo = function (action, consecutiveAction) { if (this.actionActivatesPlayer(consecutiveAction)) { return true; } if (consecutiveAction.playerId === action.playerId) { if (!(0, moves_1.isSelectItem)(consecutiveAction.move) || !(0, moves_1.isSelectItem)(action.move)) { return true; } } return false; }; MaterialRules.prototype.actionBlocksUndo = function (action) { for (var i = action.consequences.length - 1; i >= 0; i--) { if (this.moveBlocksUndo(action.consequences[i], action.playerId)) { return true; } } return this.moveBlocksUndo(action.move, action.playerId); }; MaterialRules.prototype.actionActivatesPlayer = function (action) { for (var i = action.consequences.length - 1; i >= 0; i--) { if (this.moveActivatesPlayer(action.consequences[i])) { return true; } } return this.moveActivatesPlayer(action.move); }; /** * @protected * If a moves blocks the undo, any action with this move cannot be undone. * By default, a move block the undo if it activates a player or exposes new information (roll result, hidden information revealed...) * * @param move The move to consider * @param player The player that triggered the move * @returns true if the move blocks the undo */ MaterialRules.prototype.moveBlocksUndo = function (move, player) { return this.moveActivatesPlayer(move, player) || (0, moves_1.isRoll)(move); }; MaterialRules.prototype.moveActivatesPlayer = function (move, player) { var _a; return ((0, moves_1.isStartPlayerTurn)(move) && move.player !== player) || ((0, moves_1.isStartSimultaneousRule)(move) && ((_a = move.players) !== null && _a !== void 0 ? _a : this.game.players).some(function (p) { return p !== player; })); }; /** * Restore help display & local item moves */ MaterialRules.prototype.restoreTransientState = function (previousState) { var _this = this; this.game.helpDisplay = previousState.helpDisplay; this.game.transientItems = previousState.transientItems; var _loop_1 = function (type) { previousState.transientItems[type].forEach(function (index) { return _this.game.items[type][index] = previousState.items[type][index]; }); }; for (var type in previousState.transientItems) { _loop_1(type); } }; /** * Random moves, or moves that reveals something to me, are unpredictable. * Unpredictable moves cannot be precomputed on client side, the server side's response is necessary. * See {@link Rules.isUnpredictableMove} * * @param move Material move to consider * @param _player The player playing the move * @returns true if the move outcome cannot be predicted on client side */ MaterialRules.prototype.isUnpredictableMove = function (move, _player) { return (0, moves_1.isRoll)(move); }; /** * A Material Game is over when there is no rule left to execute. This state results of a {@link EndGame} move. * * @returns true if game is over */ MaterialRules.prototype.isOver = function () { return !this.game.rule; }; /** * Amount of time given to a player everytime it is their turn to play. * @param playerId Id of the player, if you want to give different time depending on the id for asymmetric games. * @return number of seconds to add to the player's clock */ MaterialRules.prototype.giveTime = function (playerId) { var rule = this.rulesStep; return rule && (0, TimeLimit_1.hasTimeLimit)(rule) ? rule.giveTime(playerId) : 60; }; return MaterialRules; }(Rules_1.Rules)); exports.MaterialRules = MaterialRules; function getItemMoveIndexes(move) { switch (move.type) { case moves_1.ItemMoveType.Move: case moves_1.ItemMoveType.Delete: case moves_1.ItemMoveType.Roll: case moves_1.ItemMoveType.Select: return [move.itemIndex]; case moves_1.ItemMoveType.MoveAtOnce: case moves_1.ItemMoveType.DeleteAtOnce: case moves_1.ItemMoveType.Shuffle: return move.indexes; default: return []; } } //# sourceMappingURL=MaterialRules.js.map