hathora-et-labora-game
Version:
Game logic reducer for Uwe Rosenberg's Ora et Labora
183 lines • 8.48 kB
JavaScript
import { all, any, complement, equals, find, forEach, pathSatisfies, pipe, range, reduce, view, when, without, } from 'ramda';
import { P, match } from 'ts-pattern';
import { payCost, getCost, withActivePlayer, isLayBrother, isPrior, activeLens, withPlayerIndex } from '../board/player';
import { LANDSCAPES, findBuildingWithoutOffset, moveClergyToNeutralBuilding, moveClergyToOwnBuilding, } from '../board/landscape';
import { costMoney, parseResourceParam } from '../board/resource';
import { checkNotBonusRound, oncePerFrame, revertActivePlayerToCurrent, setFrameToAllowFreeUsage, withFrame, } from '../board/frame';
import { BuildingEnum, GameCommandEnum, NextUseClergy, SettlementRound, } from '../types';
import { isSettlement } from '../board/buildings';
const workContractCost = (state) => state?.frame?.settlementRound === SettlementRound.S ||
state?.frame?.settlementRound === SettlementRound.A ||
(state?.config?.players === 1 &&
state?.frame?.settlementRound === SettlementRound.B &&
state.frame.neutralBuildingPhase) ||
state?.buildings.includes(BuildingEnum.WhiskeyDistillery) ||
state?.buildings.includes(BuildingEnum.Winery)
? 1
: 2;
const checkWithPriorOnlyOnSinglePlayer = (withPrior) => (state) => {
if (withPrior && state?.config.players !== 1)
return undefined;
return state;
};
const checkWorkContractPayment = (payment) => (state) => {
if (state === undefined)
return undefined;
if (payment.whiskey ?? 0 > 1)
return state;
if (payment.wine ?? 0 > 1)
return state;
if (costMoney(payment) < workContractCost(state))
return undefined;
return state;
};
const transferActiveToOwnerOf = (building) => (state) => {
if (state === undefined)
return undefined;
if (state.config.players === 1) {
return findBuildingWithoutOffset(building)(state.players[1].landscape) ? state : undefined;
}
// this makes it so we dont look at the current player's landscape... prevent work contract on yourself
const playerIndexes = without([state.frame.activePlayerIndex], range(0, state.config.players));
const foundWithPlayer = find((i) => !!findBuildingWithoutOffset(building)(state.players[i].landscape), playerIndexes);
if (foundWithPlayer === undefined)
return undefined;
return withFrame((frame) => ({ ...frame, activePlayerIndex: foundWithPlayer }))(state);
};
const checkModalPlayerBuildingUnoccupied = (building) => (state) => {
const whichPlayer = state?.config?.players === 1 ? 1 : state?.frame.activePlayerIndex ?? 0;
return withPlayerIndex(whichPlayer)((player) => {
const location = findBuildingWithoutOffset(building)(player.landscape);
// should not actually ever get this
if (location === undefined)
return undefined;
const [row, col] = location;
const [, , clergy] = player.landscape[row][col];
if (clergy !== undefined)
return undefined;
return player;
})(state);
};
const checkModalPlayerHasPriorOption = (building, withPrior) => (state) => {
if (state === undefined)
return undefined;
const { clergy } = state.players[state.config.players === 1 ? 1 : state.frame.activePlayerIndex];
// if theyre trying to work contract someone with no people, stop it all right here
if (clergy.length === 0)
return undefined;
if (state.config.players === 1) {
return pipe(
//
moveClergyToNeutralBuilding(building, withPrior), setFrameToAllowFreeUsage([building]))(state);
}
// if all of the the active player's clergy are all priors or all laybrothers, then the player has no choice here
if (all(isPrior, clergy) || all(isLayBrother, clergy)) {
return pipe(
//
moveClergyToOwnBuilding(building), setFrameToAllowFreeUsage([building]), revertActivePlayerToCurrent)(state);
}
// otherwise activePlayer stays on them, and lets leave usableBuildings as the building they need to look at
return withFrame((frame) => ({ ...frame, usableBuildings: [building] }))(state);
};
export const workContract = (building, paymentGift, withPrior = false) => {
const input = parseResourceParam(paymentGift);
const { penny } = input;
return pipe(
// Only allow if mainAction not consumed, and consume it
(state) => {
if (state?.frame.neutralBuildingPhase &&
state?.buildings.length === 0 &&
state?.frame?.nextUse === NextUseClergy.OnlyPrior)
return state;
return oncePerFrame(GameCommandEnum.WORK_CONTRACT)(state);
},
// dont let someone send in WITH_PRIOR unless this is single player
checkWithPriorOnlyOnSinglePlayer(withPrior),
// <-- not in bonus round
checkNotBonusRound,
// <-- check to make sure payment is enough
checkWorkContractPayment(input),
// <-- consume payment
withActivePlayer(payCost(input)),
// <-> find that buildings owner, among all non-active players, and set activePlayer to them
transferActiveToOwnerOf(building),
// --> give the new active player all of the coins that were given
when(pathSatisfies(complement(equals(1)), ['config', 'players']), withActivePlayer(getCost({ penny }))), checkModalPlayerBuildingUnoccupied(building),
// --> either that player:
// ...doesnt have any clergy, then fail
// ...only has laybrothers, or a single prior, and thus no choice, so just make it for them
// ...has both a laybrother and a prior available, and thus must make a choice
checkModalPlayerHasPriorOption(building, withPrior));
};
export const complete = (state) => (partial) => match(partial)
.with([], () => {
if (!state.frame.bonusActions.includes(GameCommandEnum.WORK_CONTRACT) &&
state.frame.mainActionUsed &&
!(state.frame.neutralBuildingPhase === true &&
state.buildings.length === 0 &&
state.frame.nextUse === NextUseClergy.OnlyPrior &&
any(isPrior, state?.players?.[1]?.clergy ?? [])))
return [];
const activePlayer = view(activeLens(state), state);
if (checkNotBonusRound(state) === undefined)
return [];
// if this player can't possibly pay the work contract fee
if (checkWorkContractPayment(activePlayer)(state) === undefined)
return [];
// if all other players have no clergy available
if (all((player) => player === activePlayer ||
//
player.clergy.length === 0, state.players))
return [];
// no need to check if there are buildings to be used, each player has 3 heartland buildings
return [GameCommandEnum.WORK_CONTRACT];
})
.with([GameCommandEnum.WORK_CONTRACT], () => {
if (state.frame.neutralBuildingPhase)
return state.frame.usableBuildings;
return reduce((accum, i) => {
if (state.frame.activePlayerIndex === i)
return accum;
const player = state.players[i];
if (player.clergy.length === 0)
return accum;
forEach(forEach((landStack) => {
if (landStack.length === 0)
return;
const [, erection, clergy] = landStack;
if (erection === undefined)
return;
if (clergy !== undefined)
return;
if (LANDSCAPES.includes(erection))
return;
if (isSettlement(erection))
return;
accum.push(erection);
}), player.landscape);
return accum;
}, [], range(0, state.players.length));
})
.with([GameCommandEnum.WORK_CONTRACT, P._], () => {
const options = [];
const { whiskey = 0, wine = 0, penny = 0, nickel = 0 } = view(activeLens(state), state);
if (wine)
options.push('Wn');
if (whiskey)
options.push('Wh');
if (workContractCost(state) === 1 && (penny >= 1 || nickel))
options.push('Pn');
if (workContractCost(state) === 2 && (penny >= 2 || nickel))
options.push('PnPn');
return options;
})
.with([GameCommandEnum.WORK_CONTRACT, P._, P._], () => {
if (state.config.players === 1 &&
any(isPrior, state.players[1].clergy) &&
any(isLayBrother, state.players[1].clergy))
return ['', 'WITH_PRIOR'];
return [''];
})
.with([GameCommandEnum.WORK_CONTRACT, P._, P._, 'WITH_PRIOR'], () => [''])
.otherwise(() => []);
//# sourceMappingURL=workContract.js.map