hathora-et-labora-game
Version:
Plays Uwe Rosenberg's Ora et Labora for the Hathora engine. It reduces a list of moves into a board game state.
190 lines • 9.8 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) => {
var _a, _b, _c, _d;
return ((_a = state === null || state === void 0 ? void 0 : state.frame) === null || _a === void 0 ? void 0 : _a.settlementRound) === SettlementRound.S ||
((_b = state === null || state === void 0 ? void 0 : state.frame) === null || _b === void 0 ? void 0 : _b.settlementRound) === SettlementRound.A ||
(((_c = state === null || state === void 0 ? void 0 : state.config) === null || _c === void 0 ? void 0 : _c.players) === 1 &&
((_d = state === null || state === void 0 ? void 0 : state.frame) === null || _d === void 0 ? void 0 : _d.settlementRound) === SettlementRound.B &&
state.frame.neutralBuildingPhase) ||
(state === null || state === void 0 ? void 0 : state.buildings.includes(BuildingEnum.WhiskeyDistillery)) ||
(state === null || state === void 0 ? void 0 : state.buildings.includes(BuildingEnum.Winery))
? 1
: 2;
};
const checkWithPriorOnlyOnSinglePlayer = (withPrior) => (state) => {
if (withPrior && (state === null || state === void 0 ? void 0 : state.config.players) !== 1)
return undefined;
return state;
};
const checkWorkContractPayment = (payment) => (state) => {
var _a, _b;
if (state === undefined)
return undefined;
if ((_a = payment.whiskey) !== null && _a !== void 0 ? _a : 0 > 1)
return state;
if ((_b = payment.wine) !== null && _b !== void 0 ? _b : 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) => (Object.assign(Object.assign({}, frame), { activePlayerIndex: foundWithPlayer })))(state);
};
const checkModalPlayerBuildingUnoccupied = (building) => (state) => {
var _a, _b;
const whichPlayer = ((_a = state === null || state === void 0 ? void 0 : state.config) === null || _a === void 0 ? void 0 : _a.players) === 1 ? 1 : (_b = state === null || state === void 0 ? void 0 : state.frame.activePlayerIndex) !== null && _b !== void 0 ? _b : 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) => (Object.assign(Object.assign({}, 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) => {
var _a;
if ((state === null || state === void 0 ? void 0 : state.frame.neutralBuildingPhase) &&
(state === null || state === void 0 ? void 0 : state.buildings.length) === 0 &&
((_a = state === null || state === void 0 ? void 0 : state.frame) === null || _a === void 0 ? void 0 : _a.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([], () => {
var _a, _b, _c;
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, (_c = (_b = (_a = state === null || state === void 0 ? void 0 : state.players) === null || _a === void 0 ? void 0 : _a[1]) === null || _b === void 0 ? void 0 : _b.clergy) !== null && _c !== void 0 ? _c : [])))
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