@gamepark/rules-api
Version:
API to implement the rules of a board game
487 lines • 23.2 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 __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