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