UNPKG

hathora-et-labora-game

Version:

Game logic reducer for Uwe Rosenberg's Ora et Labora

105 lines 5.33 kB
import { pipe, view, filter, always, any, concat, append, intersection, map, join } from 'ramda'; import { P, match } from 'ts-pattern'; import { costForBuilding, isCloisterBuilding, removeBuildingFromUnbuilt } from '../board/buildings'; import { addErectionAtLandscape } from '../board/erections'; import { oncePerFrame } from '../board/frame'; import { allVacantUsableBuildings, checkCloisterAdjacency, checkLandscapeFree, checkLandTypeMatches, erectableLocations, erectableLocationsCol, } from '../board/landscape'; import { activeLens, isPrior, payCost, subtractCoins, withActivePlayer } from '../board/player'; import { GameCommandEnum, NextUseClergy, } from '../types'; const payBuildingCost = (building) => (state) => { if (state?.frame?.neutralBuildingPhase) return state; const cost = costForBuilding(building); const buyCost = (cost.penny ?? 0) + (cost.nickel ?? 0) * 5; if (buyCost) { return withActivePlayer(subtractCoins(buyCost))(state); } return withActivePlayer(payCost(cost))(state); }; const buildContinuation = (state) => { // solo play settlements are preceeded by a neutral building phase if (state.frame.neutralBuildingPhase) { // while there are buildings, only allow BUILD if (state.buildings.length !== 0) return [GameCommandEnum.BUILD]; // and when that's finished, allow the player to SETTLE or COMMIT. USE will also be available because of buildings built return [GameCommandEnum.SETTLE, GameCommandEnum.COMMIT]; } // but most of the time, we just want to allow to follow BUILD with an OnlyPrior USE, // but only if any of their clergy are a prior. if (any(isPrior, view(activeLens(state), state).clergy)) return [GameCommandEnum.USE]; return []; }; export const allowPriorToUse = (building) => (state) => { if (state === undefined) return undefined; const bonusActions = concat(buildContinuation(state), state.frame.bonusActions); const usableBuildings = append(building, intersection(state.frame.neutralBuildingPhase ? allVacantUsableBuildings(state.players[1].landscape) : [], state.frame.usableBuildings)); return { ...state, frame: { ...state.frame, bonusActions, nextUse: NextUseClergy.OnlyPrior, usableBuildings, }, }; }; export const build = ({ row, col, building }) => pipe( // any of these not defined here are probably shared with SETTLE oncePerFrame(GameCommandEnum.BUILD), checkLandscapeFree(row, col, building), checkLandTypeMatches(row, col, building), checkCloisterAdjacency(row, col, building), payBuildingCost(building), removeBuildingFromUnbuilt(building), addErectionAtLandscape(row, col, building), allowPriorToUse(building)); export const complete = (state) => (partial) => { const player = view(activeLens(state), state); return (match(partial) .with([], () => { if (!oncePerFrame(GameCommandEnum.BUILD)(state)) return []; if (complete(state)([GameCommandEnum.BUILD]).length === 0) return []; // TODO this could be faster if we bail as soon as we know at least one building is buildable return [GameCommandEnum.BUILD]; }) .with([GameCommandEnum.BUILD], // return all buildings we have the resources for always(filter((building) => state.frame.neutralBuildingPhase || !!payCost(costForBuilding(building))(player), state.buildings))) .with([GameCommandEnum.BUILD, P._], ([, building]) => { if (state.frame.neutralBuildingPhase) { const isCloister = isCloisterBuilding(building); const coords = [ [0, 0], [1, 0], [2, 0], [3, 0], [4, 0], [0, 1], [1, 1], [2, 1], [3, 1], [4, 1], ]; const acceptable = filter(([col, row]) => { const [, localBuilding] = state.players[1].landscape[row][col + 2]; // if there is a building, only match if it is also similarly cloister or not cloister if (localBuilding !== undefined) return isCloisterBuilding(localBuilding) === isCloister; // otherwise this space is blank, build under normal rules return checkCloisterAdjacency(row, col, building)(state) === state; }, coords); const ret = map(join(' '), acceptable); return ret; } // all the coords which match the terrain for this building... return erectableLocations(building, player); }) .with([GameCommandEnum.BUILD, P._, P._], ([, building, col]) => state.frame.neutralBuildingPhase ? // in a neutral building phase, we can build on netural heartland on top of anything ['0', '1'] : // all the coords which match the terrain from the previous step, and have // the same prefix as the column erectableLocationsCol(building, col, player)) // TODO: only show '' if the command is ultimately valid .with([GameCommandEnum.BUILD, P._, P._, P._], always([''])) .otherwise(always([]))); }; //# sourceMappingURL=build.js.map