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.

348 lines 15.9 kB
import { addIndex, any, append, curry, equals, filter, find, lensPath, map, pipe, range, reduce, reduced, reject, set, sum, } from 'ramda'; import { match } from 'ts-pattern'; import { LandEnum, PlayerColor, BuildingEnum, NextUseClergy, } from '../types'; import { pointsForBuilding, pointsForDwelling, terrainForErection } from './erections'; import { isPrior, withActivePlayer, withPlayerIndex } from './player'; import { isCloisterBuilding, isSettlement } from './buildings'; export const districtPrices = (config) => match(config) .with({ players: 1 }, () => [8, 7, 6, 5, 5, 4, 4, 3, 2]) .otherwise(() => [2, 3, 4, 4, 5, 5, 6, 7, 8]); export const plotPrices = (config) => match(config) .with({ players: 1 }, () => [7, 6, 6, 5, 5, 5, 4, 4, 3]) .otherwise(() => [3, 4, 4, 5, 5, 5, 6, 6, 7]); // TODO: combine findBuilding with findBuildingOffset somehow export const findBuildingWithoutOffset = (building) => (landscape) => { let row; let col; landscape.forEach((landRow, r) => { landRow.forEach(([_l, b, _c], c) => { if (building === b) { row = r; col = c; } }); }); if (row === undefined || col === undefined) return undefined; return [row, col]; }; export const findBuilding = (landscape, landscapeOffset, building) => { let row; let col; landscape.forEach((landRow, r) => { landRow.forEach(([_l, b, _c], c) => { if (building === b) { row = r - landscapeOffset; col = c; } }); }); return { row, col }; }; export const findClergy = (clergy) => (landscape) => { const locationsFound = []; landscape.forEach((landRow, r) => { landRow.forEach((land, c) => { const [_l, _b, clergyHere] = land; if (clergyHere && clergy.includes(clergyHere)) { locationsFound.push([r, c, land]); } }); }); return locationsFound; }; const PP = [LandEnum.Plains, BuildingEnum.Moor]; const PF = [LandEnum.Plains, BuildingEnum.Forest]; const P = [LandEnum.Plains]; const startBuilding = { [PlayerColor.Red]: [BuildingEnum.ClayMoundR, BuildingEnum.FarmYardR, BuildingEnum.CloisterOfficeR], [PlayerColor.Green]: [BuildingEnum.ClayMoundG, BuildingEnum.FarmYardG, BuildingEnum.CloisterOfficeG], [PlayerColor.Blue]: [BuildingEnum.ClayMoundB, BuildingEnum.FarmYardB, BuildingEnum.CloisterOfficeB], [PlayerColor.White]: [BuildingEnum.ClayMoundW, BuildingEnum.FarmYardW, BuildingEnum.CloisterOfficeW], }; export const makeLandscape = (color) => { const cm = [LandEnum.Hillside, startBuilding[color][0]]; const fy = [LandEnum.Plains, startBuilding[color][1]]; const co = [LandEnum.Plains, startBuilding[color][2]]; return [ [[], [], PP, PF, PF, P, cm, [], []], [[], [], PP, PF, fy, P, co, [], []], ]; }; const checkLandTypeMatchesPlayer = (row, col, erection) => (player) => { const tile = player.landscape[row + player.landscapeOffset][col + 2]; const [landAtSpot] = tile; const eligibleTerrain = terrainForErection(erection); if (landAtSpot && !eligibleTerrain.includes(landAtSpot)) return undefined; return player; }; export const checkLandTypeMatches = (row, col, erection) => (state) => { if (state === null || state === void 0 ? void 0 : state.frame.neutralBuildingPhase) return state; return withActivePlayer(checkLandTypeMatchesPlayer(row, col, erection))(state); }; export const checkLandscapeFree = (row, col, toBuild) => (state) => { if ((state === null || state === void 0 ? void 0 : state.frame.neutralBuildingPhase) && isSettlement(toBuild) === false) { return withPlayerIndex(1)((player) => { var _a, _d, _e; const [, existing] = (_e = (_d = (_a = player.landscape) === null || _a === void 0 ? void 0 : _a[row + player.landscapeOffset]) === null || _d === void 0 ? void 0 : _d[col + 2]) !== null && _e !== void 0 ? _e : []; if (existing === undefined) return player; if (existing && isCloisterBuilding(existing) === isCloisterBuilding(toBuild)) return player; return undefined; })(state); } return withActivePlayer((player) => { var _a, _d, _e; const [land, erection] = (_e = (_d = (_a = player.landscape) === null || _a === void 0 ? void 0 : _a[row + player.landscapeOffset]) === null || _d === void 0 ? void 0 : _d[col + 2]) !== null && _e !== void 0 ? _e : []; if (land === undefined) return undefined; if (erection !== undefined) return undefined; return player; })(state); }; export const getAdjacentOffsets = (col) => match(col) .with(5, () => [ [1, 0], // south [-1, 0], // north [0, -1], // west [-1, 1], // northeast [0, 1], // southeast ]) .with(6, () => [ [0, -1], // northwest [1, -1], // southwest ]) .otherwise(() => [ [1, 0], // south [-1, 0], // north [0, 1], // east [0, -1], // west ]); export const checkCloisterAdjacencyPlayer = (row, col, building) => (player) => { if (isCloisterBuilding(building) === false) return player; return pipe(getAdjacentOffsets, map(([rowOffset, colOffset]) => { var _a, _d, _e; return (_e = (_d = player === null || player === void 0 ? void 0 : player.landscape[row + rowOffset + ((_a = player.landscapeOffset) !== null && _a !== void 0 ? _a : 0)]) === null || _d === void 0 ? void 0 : _d[col + 2 + colOffset]) === null || _e === void 0 ? void 0 : _e[1]; }), any(isCloisterBuilding))(col) ? player : undefined; }; export const checkCloisterAdjacency = (row, col, building) => (state) => { if (state === null || state === void 0 ? void 0 : state.frame.neutralBuildingPhase) { return withPlayerIndex(1)(checkCloisterAdjacencyPlayer(row, col, building))(state); } return withActivePlayer(checkCloisterAdjacencyPlayer(row, col, building))(state); }; export const moveClergyToOwnBuilding = (building) => (state) => { if (state === undefined) return undefined; if (state.frame.nextUse === NextUseClergy.Free) return state; const player = state.players[state.frame.activePlayerIndex]; const matrixLocation = findBuildingWithoutOffset(building)(player.landscape); if (matrixLocation === undefined) return undefined; const [row, col] = matrixLocation; const [land, ,] = player.landscape[row][col]; const priors = player.clergy.filter(isPrior); if (state.frame.nextUse === NextUseClergy.OnlyPrior && priors.length === 0) return undefined; // ^this line unnecessary const nextClergy = match(state.frame.nextUse) .with(NextUseClergy.Any, () => player.clergy[0]) .with(NextUseClergy.OnlyPrior, () => priors[0]) .exhaustive(); if (nextClergy === undefined) return undefined; return withActivePlayer(pipe( // set(lensPath(['landscape', row, col]), [land, building, nextClergy]), set(lensPath(['clergy']), filter((c) => c !== nextClergy)(player.clergy))))(state); }; export const moveClergyToNeutralBuilding = (building, withPrior) => (state) => { if (state === undefined) return undefined; const neutralPlayer = state.players[1]; const matrixLocation = findBuildingWithoutOffset(building)(neutralPlayer.landscape); if (matrixLocation === undefined) return undefined; const [row, col] = matrixLocation; const [land, ,] = neutralPlayer.landscape[row][col]; const nextClergy = withPrior || state.frame.nextUse === NextUseClergy.OnlyPrior ? find(isPrior, neutralPlayer.clergy) : neutralPlayer.clergy[0]; if (nextClergy === undefined) return undefined; return withPlayerIndex(1)(pipe( // set(lensPath(['landscape', row, col]), [land, building, nextClergy]), set(lensPath(['clergy']), filter((c) => c !== nextClergy)(neutralPlayer.clergy))))(state); }; const removeClergyFromActivePlayer = (clergy) => withActivePlayer((player) => { return Object.assign(Object.assign({}, player), { clergy: reject(equals(clergy), player.clergy) }); }); const addClergyToTile = (clergy) => (playerIndex, row, col) => withPlayerIndex(playerIndex)((player) => { if (player === undefined) return undefined; const [land, building, _] = player.landscape[row][col]; return Object.assign(Object.assign({}, player), { landscape: [ ...player.landscape.slice(0, row), [ ...player.landscape[row].slice(0, col), // what to do about existingClergy? Just overwrite it for now, but // it might be nice to make it stack when multiple players go there [land, building, clergy], ...player.landscape[row].slice(col + 1), ], ...player.landscape.slice(row + 1), ] }); }); const clearBonusRoundPlacement = (state) => { if (state === undefined) return undefined; return Object.assign(Object.assign({}, state), { frame: Object.assign(Object.assign({}, state.frame), { bonusRoundPlacement: false }) }); }; export const moveClergyInBonusRoundTo = (building) => (state) => { if (state === undefined) return undefined; const playerIndexes = range(0, state.config.players); const foundWithPlayer = reduce((accum, elem) => { const searchResult = findBuildingWithoutOffset(building)(state.players[elem].landscape); if (searchResult === undefined) return accum; const [searchRow, searchCol] = searchResult; const result = [elem, searchRow, searchCol]; return reduced(result); }, undefined, playerIndexes); if (foundWithPlayer === undefined) return undefined; const [p, r, c] = foundWithPlayer; const [prior] = state.players[state.frame.activePlayerIndex].clergy.filter(isPrior); return pipe( // removeClergyFromActivePlayer(prior), addClergyToTile(prior)(p, r, c), clearBonusRoundPlacement)(state); }; // given a row of tiles, return all of the buildings where there is a building AND a clergy const occupiedBuildingsInRow = (row) => reduce((rAccum, [_, building, clergy]) => { if (building !== undefined && clergy !== undefined && !isSettlement(building)) rAccum.push(building); return rAccum; }, [], row); // given a list of rows, do the thing const occupiedBuildingsForLandscape = (land) => reduce((rAccum, landRow) => { const occupied = occupiedBuildingsInRow(landRow); rAccum.push(...occupied); return rAccum; }, [], land); export const occupiedBuildingsForPlayers = reduce((pAccum, { landscape }) => { const occupied = occupiedBuildingsForLandscape(landscape); pAccum.push(...occupied); return pAccum; }, []); export const forestLocationsForCol = (rawCol, player) => { const col = Number.parseInt(rawCol, 10) + 2; const colsAtRow = map((row) => row[col], player.landscape); return addIndex((reduce))((accum, tile, rowIndex) => { if (tile[1] === BuildingEnum.Forest) accum.push(`${rowIndex - player.landscapeOffset}`); return accum; }, [], colsAtRow); }; export const forestLocations = (player) => addIndex((reduce))((accum, row, rowIndex) => addIndex((reduce))((innerAccum, tile, colIndex) => { if (tile[1] === BuildingEnum.Forest) innerAccum.push(`${colIndex - 2} ${rowIndex - player.landscapeOffset}`); return innerAccum; }, accum, row), [], player.landscape); export const moorLocationsForCol = (rawCol, player) => { const col = Number.parseInt(rawCol, 10) + 2; const colsAtRow = map((row) => row[col], player.landscape); return addIndex((reduce))((accum, tile, rowIndex) => { if (tile[1] === BuildingEnum.Moor) accum.push(`${rowIndex - player.landscapeOffset}`); return accum; }, [], colsAtRow); }; export const moorLocations = (player) => addIndex((reduce))((accum, row, rowIndex) => addIndex((reduce))((innerAccum, tile, colIndex) => { if (tile[1] === BuildingEnum.Moor) innerAccum.push(`${colIndex - 2} ${rowIndex - player.landscapeOffset}`); return innerAccum; }, accum, row), [], player.landscape); export const erectableLocationsCol = (erection, rawCol, player) => { const col = Number.parseInt(rawCol, 10) + 2; const colsAtRow = map((row) => row[col], player.landscape); return addIndex((reduce))((accum, tile, rowIndex) => { const [land, building] = tile; if (land && !building && !!checkLandTypeMatchesPlayer(rowIndex - player.landscapeOffset, col - 2, erection)(player) && !!checkCloisterAdjacencyPlayer(rowIndex - player.landscapeOffset, col - 2, erection)(player)) accum.push(`${rowIndex - player.landscapeOffset}`); return accum; }, [], colsAtRow); }; export const erectableLocations = curry((erection, player) => addIndex((reduce))((accum, row, rowIndex) => addIndex((reduce))((innerAccum, tile, colIndex) => { const [land, building] = tile; if (land && !building && !!checkLandTypeMatchesPlayer(rowIndex - player.landscapeOffset, colIndex - 2, erection)(player) && !!checkCloisterAdjacencyPlayer(rowIndex - player.landscapeOffset, colIndex - 2, erection)(player)) innerAccum.push(`${colIndex - 2} ${rowIndex - player.landscapeOffset}`); return innerAccum; }, accum, row), [], player.landscape)); export const LANDSCAPES = [BuildingEnum.Forest, BuildingEnum.Moor]; export const allVacantUsableBuildings = (landscape) => reduce((accum, row) => reduce((accum, tile) => { const [, building, clergy] = tile; if (building !== undefined && clergy === undefined && LANDSCAPES.includes(building) === false && isSettlement(building) === false) accum.push(building); return accum; }, accum, row), [], landscape); export const allBuiltBuildings = (landscape) => reduce((accum, row) => reduce((accum, tile) => { const [, building] = tile; if (building !== undefined && LANDSCAPES.includes(building) === false && isSettlement(building) === false) accum.push(building); return accum; }, accum, row), [], landscape); export const allBuildingPoints = (landscape) => reduce((accum, row) => reduce((accum, tile) => { const [, building] = tile; if (building !== undefined && LANDSCAPES.includes(building) === false) return accum + pointsForBuilding(building); return accum; }, accum, row), 0, landscape); export const allDwellingPoints = (landscape) => pipe( // first get the coordinates of all of the settlements - column offset by 2, but dont worry about row offset (weird) (landscape) => { if (landscape === undefined) return []; const locations = []; landscape.forEach((landRow, r) => { landRow.forEach(([_l, b, _c], c) => { if (b && isSettlement(b)) { locations.push([r, c - 2]); } }); }); return locations; }, map(([row, col]) => pipe( // get all of the offsets for the given column getAdjacentOffsets, // and also consider your own tile append([0, 0]), // grab each of those tiles map(([rowOff, colOff]) => { var _a; return (_a = landscape === null || landscape === void 0 ? void 0 : landscape[row + rowOff]) === null || _a === void 0 ? void 0 : _a[col + colOff + 2]; }), // and for each tile, calculate how much dwelling it adds map((tile) => { if (tile === undefined) return 0; const [land, building] = tile; if (building !== undefined) return pointsForDwelling(building); if (land === LandEnum.Water) return 3; return 0; }), // and sum that all together sum)(col)))(landscape); //# sourceMappingURL=landscape.js.map