hathora-et-labora-game
Version:
Game logic reducer for Uwe Rosenberg's Ora et Labora
175 lines (162 loc) • 6.27 kB
text/typescript
import { always, equals, findIndex, head, map, pipe, range, reject, remove, toString, view } from 'ramda'
import { P, match } from 'ts-pattern'
import { costMoney } from '../board/resource'
import { activeLens, subtractCoins, withActivePlayer } from '../board/player'
import { GameState, GameCommandBuyDistrictParams, Tile, LandEnum, BuildingEnum, GameCommandEnum } from '../types'
const checkCanGetLandscape = (state?: GameState): GameState | undefined => {
if (state === undefined) return undefined
if (state.frame!.currentPlayerIndex !== state.frame!.activePlayerIndex) return undefined
if (state.frame!.canBuyLandscape) return state
if (state.frame!.bonusActions.includes(GameCommandEnum.BUY_DISTRICT)) return state
return undefined
}
const checkForConnection = (y: number) =>
// new district must have an existing
withActivePlayer((player) => {
const { landscape, landscapeOffset } = player
const north = landscape[y + landscapeOffset - 1]?.[4]?.[0]
const east = landscape[y + landscapeOffset]?.[5]?.[0]
const west = landscape[y + landscapeOffset]?.[1]?.[0]
const south = landscape[y + landscapeOffset + 1]?.[4]?.[0]
if (north || east || west || south) return player
return undefined
})
const checkForOverlap = (y: number) =>
withActivePlayer((player) => {
const { landscape, landscapeOffset } = player
if (landscape[y + landscapeOffset]?.[3]?.[0] !== undefined) return undefined
return player
})
const checkCanAfford = (state?: GameState): GameState | undefined => {
if (state?.frame!.bonusActions?.includes(GameCommandEnum.BUY_DISTRICT)) return state
const cost = state?.districtPurchasePrices![0]
return withActivePlayer((player) => {
if (cost === undefined) return undefined
if (costMoney(player) < cost) return undefined
return player
})(state)
}
const payForDistrict = (state?: GameState): GameState | undefined => {
if (state === undefined) return undefined
if (state.frame!.bonusActions?.includes(GameCommandEnum.BUY_DISTRICT)) return state
const cost = state.districtPurchasePrices![0]
return withActivePlayer(subtractCoins(cost))(state)
}
const removeDistrictFromPool = (state?: GameState): GameState | undefined => {
if (state === undefined) return undefined
if (state.frame!.bonusActions?.includes(GameCommandEnum.BUY_DISTRICT)) return state
return {
...state,
districtPurchasePrices: state.districtPurchasePrices!.slice(1),
}
}
const denyBuyingAnyMoreLandscape = (state?: GameState): GameState | undefined => {
if (state === undefined) return undefined
const atIndex = findIndex((a: GameCommandEnum) => equals(a, GameCommandEnum.BUY_DISTRICT), state.frame!.bonusActions)
return {
...state,
frame: {
...state.frame!,
// if the command was found in bonusActions
...(atIndex !== -1
? {
// leave canBuyLandscape as it was
bonusActions: remove(atIndex, 1, state.frame!.bonusActions),
}
: {
// otherwise set canBuyLandscape to false
canBuyLandscape: false,
}),
},
}
}
const NEW_ROW: Tile[] = [[], [], [], [], [], [], [], [], []]
const expandLandscape = (y: number) =>
withActivePlayer((player) => {
const landscape = [...player.landscape]
let { landscapeOffset } = player
if (y + player.landscapeOffset < 0) {
landscape.unshift(NEW_ROW)
landscapeOffset++
} else if (y + player.landscapeOffset >= landscape.length) {
landscape.push(NEW_ROW)
}
return {
...player,
landscape,
landscapeOffset,
}
})
const newDistrict = (side: 'PLAINS' | 'HILLS'): Tile[] =>
match(side)
.with('HILLS', () => [
[LandEnum.Plains, BuildingEnum.Moor] as Tile,
[LandEnum.Plains, BuildingEnum.Forest] as Tile,
[LandEnum.Plains, BuildingEnum.Forest] as Tile,
[LandEnum.Hillside] as Tile,
[LandEnum.Hillside] as Tile,
])
.with('PLAINS', () => [
[LandEnum.Plains, BuildingEnum.Forest] as Tile,
[LandEnum.Plains] as Tile,
[LandEnum.Plains] as Tile,
[LandEnum.Plains] as Tile,
[LandEnum.Hillside] as Tile,
])
.exhaustive()
const addNewDistrict = (y: number, side: 'PLAINS' | 'HILLS') =>
withActivePlayer((player) => {
const newTiles = newDistrict(side)
const rowsBefore = player.landscape.slice(0, y + player.landscapeOffset)
const newRow = [
...player.landscape[y + player.landscapeOffset].slice(0, 2),
...newTiles,
...player.landscape[y + player.landscapeOffset].slice(7, 9),
]
const rowsAfter = player.landscape.slice(y + player.landscapeOffset + 1)
const landscape = [...rowsBefore, newRow, ...rowsAfter]
return {
...player,
landscape,
}
})
export const buyDistrict = ({ side, y }: GameCommandBuyDistrictParams) =>
pipe(
//
checkCanGetLandscape,
checkForOverlap(y),
checkForConnection(y),
checkCanAfford,
payForDistrict,
removeDistrictFromPool,
denyBuyingAnyMoreLandscape,
expandLandscape(y),
addNewDistrict(y, side)
)
export const complete =
(state: GameState) =>
(partial: string[]): string[] => {
const player = view(activeLens(state), state)
return match<string[], string[]>(partial)
.with([], () => {
if (state.frame!.bonusActions.includes(GameCommandEnum.BUY_DISTRICT) && head(state.districtPurchasePrices!))
return [GameCommandEnum.BUY_DISTRICT]
if (checkCanGetLandscape(state) === undefined) return []
const playerWealth = costMoney(player)
const nextDistrictCost = head(state.districtPurchasePrices!) ?? Infinity
if (playerWealth < nextDistrictCost) return []
return [GameCommandEnum.BUY_DISTRICT]
})
.with([GameCommandEnum.BUY_DISTRICT], () =>
map(
toString,
reject(
(y: number) => !checkForConnection(y)(state) || !checkForOverlap(y)(state),
range(-1 - player.landscapeOffset, 1 + player.landscape.length - player.landscapeOffset)
)
)
)
.with([GameCommandEnum.BUY_DISTRICT, P._], always(['PLAINS', 'HILLS']))
.with([GameCommandEnum.BUY_DISTRICT, P._, P._], always(['']))
.otherwise(always([]))
}