UNPKG

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.

273 lines (264 loc) 10.3 kB
import { any, assoc, flatten, identity, includes, isEmpty, lensPath, map, pipe, set, sortBy, values, view, without, } from 'ramda' import { P, match } from 'ts-pattern' import { clearNeutralBuildingPhase, oncePerFrame, withFrame } from '../board/frame' import { allBuiltBuildings, allVacantUsableBuildings, moveClergyInBonusRoundTo, moveClergyToOwnBuilding, } from '../board/landscape' import { alehouse, bakery, bathhouse, brewery, buildersMarket, bulwark, calefactory, carpentry, castle, camera, chamberOfWonders, chapel, clayMound, cloisterChapterHouse, cloisterChurch, cloisterCourtyard, cloisterGarden, cloisterLibrary, cloisterOffice, cloisterWorkshop, coalHarbor, cooperage, cottage, dormitory, druidsHouse, estate, falseLighthouse, farmyard, festivalGround, filialChurch, financedEstate, forestHut, forgersWorkshop, fuelMerchant, grainStorage, granary, grandManor, grapevine, guesthouse, harborPromenade, hospice, houseOfTheBrotherhood, houseboat, inn, locutory, malthouse, market, palace, peatCoalKiln, pilgrimageSite, portico, printingOffice, priory, quarry, refectory, roundTower, sacredSite, sacristy, scriptorium, shippingCompany, shipyard, slaughterhouse, spinningMill, stoneMerchant, townEstate, whiskeyDistillery, windmill, winery, complete as completeBuilding, } from '../buildings' import { BuildingEnum, GameCommandEnum, GameStatePlaying, NextUseClergy, StateReducer, Tableau, Tile } from '../types' import { activeLens, isPrior } from '../board/player' const checkIfUseCanHappen = (building?: BuildingEnum): StateReducer => (state) => { if (state === undefined) return undefined if (state.frame.activePlayerIndex !== state.frame.currentPlayerIndex) return undefined // try to consume bonusAction and mainAction first const usedAction = oncePerFrame(GameCommandEnum.USE)(state) if (usedAction) return usedAction // but if mainActionUsed and bonusAction don't allow, still it is possible to use if // usableBuildings allows AND the building in question isn't in unusableBuildings if (building) { if ( state.frame.usableBuildings.includes(building) === true && state.frame.unusableBuildings.includes(building) === false && [NextUseClergy.Free, NextUseClergy.OnlyPrior].includes(state.frame.nextUse) ) { return state } } else if (without(state.frame.unusableBuildings, state.frame.usableBuildings).length > 0) { return state } // otherwise dont allow return undefined } const clearUsableBuildings: StateReducer = withFrame(set(lensPath(['usableBuildings']), [])) const moveClergyTo = (building: BuildingEnum): StateReducer => (state) => { if (state === undefined) return undefined if (state.frame.bonusRoundPlacement) return moveClergyInBonusRoundTo(building)(state) return moveClergyToOwnBuilding(building)(state) } export const use = (building: BuildingEnum, params: string[]): StateReducer => pipe( checkIfUseCanHappen(building), moveClergyTo(building), clearUsableBuildings, clearNeutralBuildingPhase, match<BuildingEnum, StateReducer>(building) .with(BuildingEnum.Alehouse, () => alehouse(params[0])) .with(BuildingEnum.Bakery, () => bakery(params[0])) .with(BuildingEnum.Bathhouse, () => bathhouse(params[0])) .with(BuildingEnum.Brewery, () => brewery(params[0])) .with(BuildingEnum.BuildersMarket, () => buildersMarket(params[0])) .with(BuildingEnum.Bulwark, () => bulwark(params[0])) .with(BuildingEnum.Calefactory, () => calefactory(params[0])) .with(BuildingEnum.Carpentry, () => carpentry(Number.parseInt(params[0] ?? '', 10), Number.parseInt(params[1] ?? '', 10)) ) .with(BuildingEnum.Castle, castle) .with(BuildingEnum.Camera, () => camera(params[0])) .with(BuildingEnum.ChamberOfWonders, () => chamberOfWonders(params[0])) .with(BuildingEnum.Chapel, () => chapel(params[0])) .with(BuildingEnum.ClayMoundR, BuildingEnum.ClayMoundG, BuildingEnum.ClayMoundB, BuildingEnum.ClayMoundW, () => clayMound(params[0]) ) .with(BuildingEnum.CloisterChapterHouse, cloisterChapterHouse) .with(BuildingEnum.CloisterChurch, () => cloisterChurch(params[0])) .with(BuildingEnum.CloisterCourtyard, () => cloisterCourtyard(params[0], params[1])) .with(BuildingEnum.CloisterGarden, () => cloisterGarden()) .with(BuildingEnum.CloisterLibrary, () => cloisterLibrary(params[0])) .with( BuildingEnum.CloisterOfficeR, BuildingEnum.CloisterOfficeG, BuildingEnum.CloisterOfficeB, BuildingEnum.CloisterOfficeW, () => cloisterOffice(params[0]) ) .with(BuildingEnum.CloisterWorkshop, () => cloisterWorkshop(params[0])) .with(BuildingEnum.CoalHarbor, () => coalHarbor(params[0])) .with(BuildingEnum.Cooperage, () => cooperage(params[0], params[1])) .with(BuildingEnum.Cottage, () => cottage()) .with(BuildingEnum.Dormitory, () => dormitory(params[0])) .with(BuildingEnum.DruidsHouse, () => druidsHouse(params[0], params[1])) .with(BuildingEnum.Estate, () => estate(params[0])) .with(BuildingEnum.FarmYardR, BuildingEnum.FarmYardG, BuildingEnum.FarmYardB, BuildingEnum.FarmYardW, () => farmyard(params[0]) ) .with(BuildingEnum.FalseLighthouse, () => falseLighthouse(params[0])) .with(BuildingEnum.FestivalGround, () => festivalGround(params[0], params[1])) .with(BuildingEnum.FilialChurch, () => filialChurch(params[0])) .with(BuildingEnum.FinancedEstate, () => financedEstate(params[0])) .with(BuildingEnum.ForestHut, () => forestHut((Number.parseInt(params[0] ?? '', 10), Number.parseInt(params[1] ?? '', 10))) ) .with(BuildingEnum.ForgersWorkshop, () => forgersWorkshop(params[0])) .with(BuildingEnum.FuelMerchant, () => fuelMerchant(params[0])) .with(BuildingEnum.Guesthouse, () => guesthouse()) .with(BuildingEnum.GrainStorage, () => grainStorage(params[0])) .with(BuildingEnum.Granary, () => granary(params[0])) .with(BuildingEnum.GrandManor, () => grandManor(params[0])) .with(BuildingEnum.GrapevineA, BuildingEnum.GrapevineA, () => grapevine(params[0])) .with(BuildingEnum.GrapevineB, BuildingEnum.GrapevineB, () => grapevine(params[0])) .with(BuildingEnum.HarborPromenade, harborPromenade) .with(BuildingEnum.Hospice, hospice) .with(BuildingEnum.Houseboat, () => houseboat()) .with(BuildingEnum.HouseOfTheBrotherhood, () => houseOfTheBrotherhood(params[0], params[1])) .with(BuildingEnum.Inn, () => inn(params[0])) .with(BuildingEnum.Locutory, () => locutory(params[0])) .with(BuildingEnum.Market, () => market(params[0])) .with(BuildingEnum.Malthouse, () => malthouse(params[0])) .with(BuildingEnum.Palace, () => palace(params[0])) .with(BuildingEnum.PeatCoalKiln, BuildingEnum.PeatCoalKiln, () => peatCoalKiln(params[0])) .with(BuildingEnum.PilgrimageSite, () => pilgrimageSite(params[0], params[1])) .with(BuildingEnum.Portico, () => portico(params[0])) .with(BuildingEnum.PrintingOffice, () => printingOffice(...params)) .with(BuildingEnum.Priory, priory) .with(BuildingEnum.QuarryA, BuildingEnum.QuarryB, () => quarry(params[0])) .with(BuildingEnum.Refectory, () => refectory(params[0])) .with(BuildingEnum.RoundTower, () => roundTower(params[0])) .with(BuildingEnum.Scriptorium, () => scriptorium(params[0])) .with(BuildingEnum.Sacristy, () => sacristy(params[0])) .with(BuildingEnum.SacredSite, () => sacredSite(params[0])) .with(BuildingEnum.ShippingCompany, () => shippingCompany(params[0])) .with(BuildingEnum.Shipyard, () => shipyard(params[0])) .with(BuildingEnum.Slaughterhouse, () => slaughterhouse(params[0])) .with(BuildingEnum.SpinningMill, () => spinningMill()) .with(BuildingEnum.StoneMerchant, () => stoneMerchant(params[0])) .with(BuildingEnum.TownEstate, () => townEstate(params[0])) .with(BuildingEnum.WhiskeyDistillery, () => whiskeyDistillery(params[0])) .with(BuildingEnum.Windmill, () => windmill(params[0])) .with(BuildingEnum.Winery, () => winery(params[0])) .otherwise(() => () => { throw new Error(`Invalid params ${JSON.stringify(params)} for building ${building}`) }) ) export const complete = (state: GameStatePlaying) => (partial: string[]): string[] => { const player = state.players[state.frame.activePlayerIndex] return match<string[], string[]>(partial) .with([], () => { if (checkIfUseCanHappen()(state) === undefined) return [] const hasClergyAvailable = view(activeLens(state), state).clergy?.length > 0 const hasPriorAvailable = any(identity, map(isPrior, view(activeLens(state), state).clergy)) if ( state.frame.bonusRoundPlacement === false && ((state.frame.nextUse === NextUseClergy.Any && !hasClergyAvailable) || (state.frame.nextUse === NextUseClergy.OnlyPrior && !hasPriorAvailable)) ) return [] if (state.frame.neutralBuildingPhase && state.frame.nextUse !== NextUseClergy.Free) return [] return [GameCommandEnum.USE] }) .with([GameCommandEnum.USE], () => { if (!isEmpty(state.frame.usableBuildings)) return state.frame.usableBuildings if (state.frame.bonusRoundPlacement) { return pipe( map<Tableau, Tile[][]>((player) => player.landscape), map(allBuiltBuildings), flatten, sortBy((s) => s.substring(1)) )(state.players) } return allVacantUsableBuildings(player.landscape) }) .with( P.when( ([command, building]) => (command as GameCommandEnum) === GameCommandEnum.USE && includes(building, values(BuildingEnum)) ), ([, building, ...params]) => completeBuilding[building as BuildingEnum](params)(state) ) .otherwise(() => { return [] }) }