@gamepark/rules-api
Version:
API to implement the rules of a board game
270 lines • 10.8 kB
JavaScript
import { isEqual, merge } from 'es-toolkit';
import { isSameLocationArea } from '../location';
import { isShuffleRandomized, ItemMoveType } from '../moves';
import { Material } from './index';
/**
* Helper class to change the state of any {@link MaterialItem} in a game implemented with {@link MaterialRules}.
*
* @typeparam P - identifier of a player. Either a number or a numeric enum (eg: PlayerColor)
* @typeparam M - Numeric enum of the types of material manipulated in the game
* @typeparam L - Numeric enum of the types of location in the game where the material can be located
*/
export class MaterialMutator {
type;
items;
locationsStrategies;
canMerge;
rulesClassName;
/**
* @param type Type of items this mutator will work on
* @param items Items to work with
* @param locationsStrategies The strategies that these items must follow
* @param canMerge Whether to items at the exact same location can merge into one item with a quantity
* @param rulesClassName Constructor name of the main rules class for logging
*/
constructor(type, items, locationsStrategies = {}, canMerge = true, rulesClassName = '') {
this.type = type;
this.items = items;
this.locationsStrategies = locationsStrategies;
this.canMerge = canMerge;
this.rulesClassName = rulesClassName;
}
/**
* Executes a move on the game items
* @param move
*/
applyMove(move) {
switch (move.type) {
case ItemMoveType.Create:
this.create(move);
break;
case ItemMoveType.CreateAtOnce:
for (const item of move.items) {
this.create({ ...move, type: ItemMoveType.Create, item });
}
break;
case ItemMoveType.Move:
this.move(move);
break;
case ItemMoveType.MoveAtOnce:
this.moveItemsAtOnce(move);
break;
case ItemMoveType.Roll:
this.roll(move);
break;
case ItemMoveType.Delete:
this.delete(move);
break;
case ItemMoveType.DeleteAtOnce:
for (const index of move.indexes) {
this.removeItem(this.items[index], Infinity);
}
break;
case ItemMoveType.Shuffle:
this.shuffle(move);
break;
case ItemMoveType.Select:
this.select(move);
break;
}
}
/**
* Find the index of an existing item we could merge a new item with (create a single item with a quantity)
*
* @param newItem An item to compare with existing items
* @returns {number} Index of the existing item we can merge with, or -1 if there is no possible merge
*/
findMergeIndex(newItem) {
if (!this.canMerge)
return -1;
const { quantity: q1, ...data1 } = newItem;
return this.items.findIndex(({ quantity: q2, ...data2 }) => q1 !== 0 && q2 !== 0 && isEqual(data1, data2));
}
addItem(item) {
this.applyAddItemStrategy(item);
const availableIndex = this.items.findIndex(item => item.quantity === 0);
if (availableIndex !== -1) {
this.items[availableIndex] = item;
}
else {
this.items.push(item);
}
}
/**
* Provides the index that the new item will have
* @param newItem An item that is going to be created
* @returns {number} the future index of that item
*/
getItemCreationIndex(newItem) {
const mergeIndex = this.findMergeIndex(newItem);
if (mergeIndex !== -1)
return mergeIndex;
const availableIndex = this.items.findIndex(item => item.quantity === 0);
if (availableIndex !== -1)
return availableIndex;
return this.items.length;
}
applyAddItemStrategy(item) {
if (item.location.type in this.locationsStrategies) {
const strategy = this.locationsStrategies[item.location.type];
if (strategy.addItem) {
const material = new Material(this.type, this.items)
.location(item.location.type).player(item.location.player).locationId(item.location.id).parent(item.location.parent);
strategy.addItem(material, item);
}
}
}
applyMoveItemStrategy(item, index) {
if (item.location.type in this.locationsStrategies) {
const strategy = this.locationsStrategies[item.location.type];
if (strategy.moveItem) {
const material = new Material(this.type, this.items)
.location(item.location.type).player(item.location.player).locationId(item.location.id).parent(item.location.parent);
strategy.moveItem(material, item, index);
}
}
}
removeItem(item, quantity = 1) {
item.quantity = Math.max(0, (item.quantity ?? 1) - quantity);
if (item.quantity === 0) {
this.applyRemoveItemStrategy(item);
}
}
applyRemoveItemStrategy(item) {
if (item.location.type in this.locationsStrategies) {
const strategy = this.locationsStrategies[item.location.type];
if (strategy.removeItem) {
const material = new Material(this.type, this.items)
.location(item.location.type).player(item.location.player).locationId(item.location.id).parent(item.location.parent);
strategy.removeItem(material, item);
}
}
}
create(move) {
const mergeIndex = this.findMergeIndex(move.item);
if (mergeIndex !== -1) {
const mergeItem = this.items[mergeIndex];
mergeItem.quantity = (mergeItem.quantity ?? 1) + (move.item.quantity ?? 1);
}
else {
if (move.item.quantity && !this.canMerge) {
console.error(`${this.rulesClassName}: do not use quantity on items that cannot merge. Items that can be hidden cannot merge.`);
}
this.addItem(JSON.parse(JSON.stringify(move.item)));
}
}
move(move) {
const quantity = move.quantity ?? 1;
const sourceItem = this.items[move.itemIndex];
const itemAfterMove = this.getItemAfterMove(move);
const mergeIndex = this.findMergeIndex(itemAfterMove);
if (mergeIndex !== -1) {
if (mergeIndex === move.itemIndex) {
console.warn(`${this.rulesClassName}: item is moved to the location he already has - ${JSON.stringify(move)}`);
}
else {
const mergeItem = this.items[mergeIndex];
mergeItem.quantity = (mergeItem.quantity ?? 1) + quantity;
this.removeItem(sourceItem, quantity);
}
}
else if (sourceItem.quantity && sourceItem.quantity > quantity) {
sourceItem.quantity -= quantity;
this.addItem(itemAfterMove);
}
else {
this.moveItem(sourceItem, itemAfterMove, move.itemIndex);
}
}
roll(move) {
const sourceItem = this.items[move.itemIndex];
const itemAfterMove = { ...sourceItem, location: JSON.parse(JSON.stringify(move.location)) };
this.moveItem(sourceItem, itemAfterMove, move.itemIndex);
}
moveItem(item, newItem, index) {
if (!isSameLocationArea(newItem.location, item.location)) {
this.applyAddItemStrategy(newItem);
}
else {
this.applyMoveItemStrategy(newItem, index);
}
this.items[index] = newItem;
if (!isSameLocationArea(newItem.location, item.location)) {
this.applyRemoveItemStrategy(item);
}
}
moveItemsAtOnce(move) {
for (const index of move.indexes) {
const sourceItem = this.items[index];
const itemAfterMove = this.getItemAfterMoveAtOnce(move, index);
if (!isSameLocationArea(itemAfterMove.location, sourceItem.location)) {
this.applyAddItemStrategy(itemAfterMove);
}
else {
this.applyMoveItemStrategy(itemAfterMove, index);
}
this.items[index] = itemAfterMove;
if (!isSameLocationArea(itemAfterMove.location, sourceItem.location)) {
this.applyRemoveItemStrategy(sourceItem);
}
}
}
/**
* Provides the state of an item after it is moved
* @param move The move that is going to happen
* @return {MaterialItem} state of the item after the move is executed
*/
getItemAfterMove(move) {
const item = JSON.parse(JSON.stringify(this.getItemWithLocation(move.location, move.itemIndex)));
if (move.reveal) {
merge(item, move.reveal);
}
if (move.quantity) {
item.quantity = move.quantity;
}
else {
delete item.quantity;
}
return item;
}
/**
* Provides the state of an item after it is moved
* @param move The move that is going to happen
* @param index Index of the item to consider
* @return {MaterialItem} state of the item after the move is executed
*/
getItemAfterMoveAtOnce(move, index) {
const item = JSON.parse(JSON.stringify(this.getItemWithLocation(move.location, index)));
if (move.reveal && move.reveal[index]) {
merge(item, move.reveal[index]);
}
return item;
}
getItemWithLocation(location, index) {
const moveLocation = JSON.parse(JSON.stringify(location));
const actualItem = this.items[index];
const newLocation = location.type === undefined ? { ...actualItem.location, ...moveLocation } : moveLocation;
return { ...actualItem, location: newLocation };
}
delete(move) {
return this.removeItem(this.items[move.itemIndex], move.quantity);
}
shuffle(move) {
if (!isShuffleRandomized(move))
return; // Nothing to do on front-end side for a shuffle. The index swap is only required on the backend.
const shuffledItems = move.indexes.map((index) => this.items[index]);
move.newIndexes.forEach((newIndex, i) => {
this.items[newIndex] = { ...shuffledItems[i], location: this.items[newIndex].location };
});
}
select(move) {
const item = this.items[move.itemIndex];
if (move.selected === false) {
delete item.selected;
}
else {
item.selected = move.quantity ?? true;
}
}
}
//# sourceMappingURL=MaterialMutator.js.map