UNPKG

hathora-et-labora-game

Version:

Game logic reducer for Uwe Rosenberg's Ora et Labora

258 lines (237 loc) 9.11 kB
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, Cost, Frame, GameCommandEnum, GameState, NextUseClergy, SettlementRound, StateReducer, Tile, } from '../types' import { isSettlement } from '../board/buildings' const workContractCost = (state: GameState | undefined): number => 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: boolean): StateReducer => (state) => { if (withPrior && state?.config!.players !== 1) return undefined return state } const checkWorkContractPayment = (payment: Cost): StateReducer => (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: BuildingEnum): StateReducer => (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<number>([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) => ({ ...frame, activePlayerIndex: foundWithPlayer }))(state) } const checkModalPlayerBuildingUnoccupied = (building: BuildingEnum): StateReducer => (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: BuildingEnum, withPrior: boolean): StateReducer => (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: BuildingEnum, paymentGift: string, withPrior: boolean = false): StateReducer => { 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: GameState) => (partial: string[]): string[] => match<string[], string[]>(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<number, BuildingEnum[]>( (accum: BuildingEnum[], i: number) => { if (state.frame!.activePlayerIndex === i) return accum const player = state.players![i] if (player.clergy.length === 0) return accum forEach<Tile[]>( forEach((landStack: Tile) => { 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 as BuildingEnum) }), player.landscape ) return accum }, [] as BuildingEnum[], 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(() => [])