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.
150 lines (143 loc) • 5.73 kB
text/typescript
import { pipe, view, filter, always, concat, append, set, lensPath, intersection, lensProp, 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, payCost, subtractCoins, withActivePlayer } from '../board/player'
import {
BuildingEnum,
Frame,
GameCommandBuildParams,
GameCommandEnum,
GameStatePlaying,
NextUseClergy,
StateReducer,
} from '../types'
const payBuildingCost =
(building: BuildingEnum): StateReducer =>
(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: GameStatePlaying): GameCommandEnum[] => {
// 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.
return [GameCommandEnum.USE]
}
export const allowPriorToUse =
(building: BuildingEnum): StateReducer =>
(state) => {
if (state === undefined) return undefined
const bonusActions = concat<GameCommandEnum, GameCommandEnum>(buildContinuation(state), state.frame.bonusActions)
const usableBuildings = append<BuildingEnum>(
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 }: GameCommandBuildParams): StateReducer =>
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: GameStatePlaying) =>
(partial: string[]): string[] => {
const player = view(activeLens(state), state)
return (
match<string[], string[]>(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: BuildingEnum): boolean =>
state.frame.neutralBuildingPhase || !!payCost(costForBuilding(building))(player),
state.buildings
)
)
)
.with([GameCommandEnum.BUILD, P._], ([, building]) => {
if (state.frame.neutralBuildingPhase) {
const isCloister = isCloisterBuilding(building as BuildingEnum)
const coords: [number, number][] = [
[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 as BuildingEnum)(state) === state
}, coords)
const ret = map(join(' '), acceptable)
return ret
}
// all the coords which match the terrain for this building...
return erectableLocations(building as BuildingEnum, 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 as BuildingEnum, col, player)
)
// TODO: only show '' if the command is ultimately valid
.with([GameCommandEnum.BUILD, P._, P._, P._], always(['']))
.otherwise(always([]))
)
}