hathora-et-labora-game
Version:
Game logic reducer for Uwe Rosenberg's Ora et Labora
148 lines • 6.01 kB
JavaScript
import { assoc, equals, findIndex, lensProp, map, pipe, reduce, remove, set } from 'ramda';
import { match } from 'ts-pattern';
import { nextFrame4Long } from './frame/nextFrame4Long';
import { nextFrame3Long } from './frame/nextFrame3Long';
import { nextFrameSolo } from './frame/nextFrameSolo';
import { nextFrame3Short } from './frame/nextFrame3Short';
import { nextFrame4Short } from './frame/nextFrame4Short';
import { nextFrame2Long } from './frame/nextFrame2Long';
import { nextFrame2Short } from './frame/nextFrame2Short';
import { NextUseClergy, } from '../types';
import { LANDSCAPES, findBuildingWithoutOffset, getAdjacentOffsets, occupiedBuildingsForPlayers } from './landscape';
import { isSettlement } from './buildings';
export const withFrame = (func) => (state) => {
if (state === undefined || state.frame === undefined)
return undefined;
const frame = func(state.frame);
if (frame === undefined)
return undefined;
return {
...state,
frame,
};
};
export const addBonusAction = (command) => withFrame((frame) => ({
...frame,
bonusActions: [...frame.bonusActions, command],
}));
const runProgression = (withFlow) => (state) => {
const nextFrameIndex = state?.frame?.next;
if (nextFrameIndex === undefined)
return undefined;
const { upkeep = [], ...frameUpdates } = withFlow[nextFrameIndex] ?? {};
return pipe(
// first, with all of the properties on the new frame, overlay them on the current frame
withFrame((frame) => {
const newFrame = {
...frame,
activePlayerIndex: frameUpdates.currentPlayerIndex ?? frame?.activePlayerIndex,
mainActionUsed: false,
bonusActions: [],
canBuyLandscape: true,
unusableBuildings: [],
usableBuildings: [],
nextUse: NextUseClergy.Any,
neutralBuildingPhase: false,
...frameUpdates,
};
return newFrame;
}),
// and then if there are any upkeep reducer functions on the frame, run them
(state) => {
return upkeep.reduce((accum, f) => f(accum), state);
})(state);
};
export const pickFrameFlow = (config) => match(config)
.with({ players: 3, length: 'long' }, () => nextFrame3Long)
.with({ players: 4, length: 'long' }, () => nextFrame4Long)
.with({ players: 3, length: 'short' }, () => nextFrame3Short)
.with({ players: 4, length: 'short' }, () => nextFrame4Short)
.with({ players: 2, length: 'long' }, () => nextFrame2Long)
.with({ players: 2, length: 'short' }, () => nextFrame2Short)
.with({ players: 1 }, () => nextFrameSolo)
.exhaustive();
export const nextFrame = (state) => {
if (state === undefined)
return undefined;
if (state.config === undefined)
return undefined;
return runProgression(pickFrameFlow(state.config))(state);
};
export const revertActivePlayerToCurrent = withFrame((frame) => ({
...frame,
activePlayerIndex: frame.currentPlayerIndex,
}));
const consumeCommandFromBonus = (command) => (frame) => {
if (frame === undefined)
return undefined;
const bonusIndex = findIndex(equals(command))(frame.bonusActions);
if (bonusIndex === -1)
return undefined;
const newValue = remove(bonusIndex, 1, frame.bonusActions);
return set(lensProp('bonusActions'), newValue, frame);
};
export const onlyViaBonusActions = (command) => withFrame(consumeCommandFromBonus(command));
export const oncePerFrame = (command) => withFrame((frame) => {
// first try to remove the proposed command from bonusActions
const bonusFrame = consumeCommandFromBonus(command)(frame);
if (bonusFrame !== undefined)
return bonusFrame;
// then if it couldn't be, consume the main action, if available
if (frame.mainActionUsed === false)
return { ...frame, mainActionUsed: true };
return undefined;
});
export const clearNeutralBuildingPhase = withFrame(assoc('neutralBuildingPhase', false));
export const setFrameToAllowFreeUsage = (building) => withFrame((frame) => ({
...frame,
usableBuildings: building,
nextUse: NextUseClergy.Free,
}));
export const disableFurtherUsage = (building) => withFrame((frame) => frame && {
...frame,
unusableBuildings: [...frame.unusableBuildings, building],
});
const whichIndexHasBuilding = (building) => (landscapes) => {
for (let i = 0; i < landscapes.length; i++) {
const location = findBuildingWithoutOffset(building)(landscapes[i]);
if (location)
return [i, ...location];
}
return undefined;
};
export const allowFreeUsageToNeighborsOf = (building) => (state) => {
if (state === undefined)
return state;
const location = whichIndexHasBuilding(building)(state.players.map((p) => p.landscape));
if (location === undefined)
return state;
const [player, row, col] = location;
return setFrameToAllowFreeUsage(reduce((accum, curr) => {
const [p, r, c] = curr;
const landStack = state.players[p].landscape?.[r]?.[c];
if (landStack === undefined)
return accum;
const [_, building, clergy] = landStack;
if (building === undefined)
return accum;
if (clergy !== undefined)
return accum;
if (isSettlement(building))
return accum;
if (LANDSCAPES.includes(building))
return accum;
accum.push(building);
return accum;
}, [], map(([rowMod, colMod]) => [player, row + rowMod, col + colMod], getAdjacentOffsets(col - 2))))(state);
};
export const allOccupiedBuildingsUsable = (state) => {
if (state === undefined)
return state;
return setFrameToAllowFreeUsage(occupiedBuildingsForPlayers(state.players))(state);
};
export const checkNotBonusRound = withFrame((frame) => {
if (frame?.bonusRoundPlacement === true)
return undefined;
return frame;
});
//# sourceMappingURL=frame.js.map