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
JavaScript
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