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.
212 lines • 12.2 kB
JavaScript
import { match } from 'ts-pattern';
import { any, curry, keys, join, lift, map, pipe, range, reduce, repeat, addIndex, min, add, reverse, zipWith, zip, uniq, comparator, sort, when, pathSatisfies, equals, always, isNil, gte, __, allPass, } from 'ramda';
import { ResourceEnum } from '../types';
import { getCost, withEachPlayer } from './player';
export const basicResources = [
ResourceEnum.Peat,
ResourceEnum.Sheep,
ResourceEnum.Wood,
ResourceEnum.Clay,
ResourceEnum.Penny,
ResourceEnum.Grain,
];
export const allResource = [
['peat', ResourceEnum.Peat],
['penny', ResourceEnum.Penny],
['grain', ResourceEnum.Grain],
['clay', ResourceEnum.Clay],
['wood', ResourceEnum.Wood],
['sheep', ResourceEnum.Sheep],
['stone', ResourceEnum.Stone],
['flour', ResourceEnum.Flour],
['grape', ResourceEnum.Grape],
['nickel', ResourceEnum.Nickel],
['malt', ResourceEnum.Malt],
['coal', ResourceEnum.Coal],
['book', ResourceEnum.Book],
['ceramic', ResourceEnum.Ceramic],
['whiskey', ResourceEnum.Whiskey],
['straw', ResourceEnum.Straw],
['meat', ResourceEnum.Meat],
['ornament', ResourceEnum.Ornament],
['bread', ResourceEnum.Bread],
['wine', ResourceEnum.Wine],
['beer', ResourceEnum.Beer],
['reliquary', ResourceEnum.Reliquary],
];
export const combinations = (maxLength, tokens, prefix = []) => {
if (prefix.length >= maxLength) {
if (prefix.length === 0)
return [];
return [join('', prefix)];
}
return addIndex((reduce))((accum, item, ndx) => {
accum.push(...combinations(maxLength, tokens.slice(ndx + 1), [...prefix, item]));
return accum;
}, [], tokens);
};
function* resourceSlicer(s) {
for (let i = 0; i + 1 < s.length; i += 2) {
yield s.slice(i, i + 2);
}
}
const addRes = (resource, cost) => () => {
var _a;
cost[resource] = 1 + ((_a = cost[resource]) !== null && _a !== void 0 ? _a : 0);
};
export const parseResourceParam = (p) => {
const cost = {};
if (p === undefined)
return cost;
for (const r of resourceSlicer(p)) {
match(r)
.with(ResourceEnum.Wood, addRes('wood', cost))
.with(ResourceEnum.Whiskey, addRes('whiskey', cost))
.with(ResourceEnum.Grain, addRes('grain', cost))
.with(ResourceEnum.Straw, addRes('straw', cost))
.with(ResourceEnum.Sheep, addRes('sheep', cost))
.with(ResourceEnum.Meat, addRes('meat', cost))
.with(ResourceEnum.Clay, addRes('clay', cost))
.with('Po', addRes('ceramic', cost))
.with(ResourceEnum.Ceramic, addRes('ceramic', cost))
.with(ResourceEnum.Peat, addRes('peat', cost))
.with(ResourceEnum.Coal, addRes('coal', cost))
.with(ResourceEnum.Penny, addRes('penny', cost))
.with(ResourceEnum.Book, addRes('book', cost))
.with(ResourceEnum.Stone, addRes('stone', cost))
.with(ResourceEnum.Ornament, addRes('ornament', cost))
.with(ResourceEnum.Flour, addRes('flour', cost))
.with(ResourceEnum.Bread, addRes('bread', cost))
.with(ResourceEnum.Grape, addRes('grape', cost))
.with(ResourceEnum.Wine, addRes('wine', cost))
.with(ResourceEnum.Nickel, addRes('nickel', cost))
.with(ResourceEnum.Reliquary, addRes('reliquary', cost))
.with('Ho', addRes('malt', cost))
.with(ResourceEnum.Malt, addRes('malt', cost))
.with(ResourceEnum.Beer, addRes('beer', cost))
.with(ResourceEnum.BonusPoint, () => { })
.with(ResourceEnum.Joker, () => { })
.otherwise(() => { });
}
return cost;
};
export const stringRepeater = curry((repeated, count) => pipe(repeat(repeated), join(''))(count));
export const resourceArray = (resource, maxAmount = Infinity) => pipe(
//
min(maxAmount), add(1), range(0), (reverse), map(stringRepeater(resource)));
// would have been nice if zip took > 2 arrays
export const zip3 = (arrA, arrB, arrC) => zipWith(([a, b], c) => a + b + c)(zip(arrA, arrB), arrC);
const amountCostOptions = (outputs, token, foodValue, totalCount) => (incompletes) => reduce((accum, prevTrace) => {
const [prevPrefix, prevFood] = prevTrace;
map((numSheep) => {
const nextFood = prevFood - numSheep * foodValue;
const nextPrefix = `${prevPrefix}${stringRepeater(token, numSheep)}`;
if (nextFood <= 0) {
outputs.push(nextPrefix);
}
else {
accum.push([nextPrefix, nextFood]);
}
}, range(0, Math.min(totalCount, Math.ceil(prevFood / foodValue)) + 1));
return accum;
}, [], incompletes);
const rewardOptions = (outputs, token, points, pointsNext) => (incompletes) => reduce((accum, prevTrace) => {
const [prevPrefix, prevFood] = prevTrace;
map((numSheep) => {
const nextFood = prevFood - numSheep * points;
const nextPrefix = `${prevPrefix}${stringRepeater(token, numSheep)}`;
if (nextFood >= 0) {
outputs.push(nextPrefix);
}
if (nextFood >= pointsNext) {
accum.push([nextPrefix, nextFood]);
}
}, reverse(range(0, 1 + Math.floor(prevFood / points))));
return accum;
}, [], incompletes);
export const foodCostOptions = curry((food, player) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
const output = [];
pipe(
// first try eating the big stuff, which is most likely food
amountCostOptions(output, ResourceEnum.Meat, 5, (_a = player.meat) !== null && _a !== void 0 ? _a : 0), amountCostOptions(output, ResourceEnum.Beer, 5, (_b = player.beer) !== null && _b !== void 0 ? _b : 0), amountCostOptions(output, ResourceEnum.Bread, 3, (_c = player.bread) !== null && _c !== void 0 ? _c : 0), amountCostOptions(output, ResourceEnum.Sheep, 2, (_d = player.sheep) !== null && _d !== void 0 ? _d : 0),
// try eating small food, raw stuff first
amountCostOptions(output, ResourceEnum.Grain, 1, (_e = player.grain) !== null && _e !== void 0 ? _e : 0), amountCostOptions(output, ResourceEnum.Flour, 1, (_f = player.flour) !== null && _f !== void 0 ? _f : 0), amountCostOptions(output, ResourceEnum.Malt, 1, (_g = player.malt) !== null && _g !== void 0 ? _g : 0), amountCostOptions(output, ResourceEnum.Grape, 1, (_h = player.grape) !== null && _h !== void 0 ? _h : 0),
// then try eating money, which they might be saving for land
amountCostOptions(output, ResourceEnum.Nickel, 5, (_j = player.nickel) !== null && _j !== void 0 ? _j : 0), amountCostOptions(output, ResourceEnum.Penny, 1, (_k = player.penny) !== null && _k !== void 0 ? _k : 0),
// then try eating wine/whiskey, which has more utility than money
amountCostOptions(output, ResourceEnum.Wine, 1, (_l = player.wine) !== null && _l !== void 0 ? _l : 0), amountCostOptions(output, ResourceEnum.Whiskey, 2, (_m = player.whiskey) !== null && _m !== void 0 ? _m : 0))([['', food]]);
return output;
});
export const energyCostOptions = curry((energy, player) => {
var _a, _b, _c, _d;
const output = [];
pipe(
// first try eating the big stuff, which is most likely energy
amountCostOptions(output, ResourceEnum.Coal, 3, (_a = player.coal) !== null && _a !== void 0 ? _a : 0), amountCostOptions(output, ResourceEnum.Peat, 2, (_b = player.peat) !== null && _b !== void 0 ? _b : 0), amountCostOptions(output, ResourceEnum.Wood, 1, (_c = player.wood) !== null && _c !== void 0 ? _c : 0), amountCostOptions(output, ResourceEnum.Straw, 0.5, (_d = player.straw) !== null && _d !== void 0 ? _d : 0))([['', energy]]);
return output;
});
export const coinCostOptions = curry((coins, player) => {
var _a, _b, _c, _d;
const output = [];
pipe(amountCostOptions(output, ResourceEnum.Nickel, 5, (_a = player.nickel) !== null && _a !== void 0 ? _a : 0), amountCostOptions(output, ResourceEnum.Penny, 1, (_b = player.penny) !== null && _b !== void 0 ? _b : 0), amountCostOptions(output, ResourceEnum.Whiskey, 2, (_c = player.whiskey) !== null && _c !== void 0 ? _c : 0), amountCostOptions(output, ResourceEnum.Wine, 1, (_d = player.wine) !== null && _d !== void 0 ? _d : 0))([['', coins]]);
return output;
});
export const pointCostOptions = curry((points, player) => {
var _a, _b, _c, _d, _e, _f, _g;
const output = [];
pipe(amountCostOptions(output, ResourceEnum.Reliquary, 8, (_a = player.reliquary) !== null && _a !== void 0 ? _a : 0), amountCostOptions(output, ResourceEnum.Ornament, 4, (_b = player.ornament) !== null && _b !== void 0 ? _b : 0), amountCostOptions(output, ResourceEnum.Ceramic, 3, (_c = player.ceramic) !== null && _c !== void 0 ? _c : 0), amountCostOptions(output, ResourceEnum.Book, 2, (_d = player.book) !== null && _d !== void 0 ? _d : 0), amountCostOptions(output, ResourceEnum.Nickel, 2, (_e = player.nickel) !== null && _e !== void 0 ? _e : 0), amountCostOptions(output, ResourceEnum.Whiskey, 1, (_f = player.whiskey) !== null && _f !== void 0 ? _f : 0), amountCostOptions(output, ResourceEnum.Wine, 1, (_g = player.wine) !== null && _g !== void 0 ? _g : 0))([['', points]]);
return output;
});
export const concatStr = (a, b) => `${a}${b}`;
export const settlementCostOptions = curry(({ food, energy }, player) => lift(concatStr)(foodCostOptions(food, player), energyCostOptions(energy, player)));
export const differentGoods = (cost) => Object.keys(cost).filter((k) => { var _a; return (_a = cost[k]) !== null && _a !== void 0 ? _a : 0 >= 1; }).length;
export const totalGoods = (cost) => Object.keys(cost).reduce((sum, k) => { var _a; return sum + ((_a = cost[k]) !== null && _a !== void 0 ? _a : 0); }, 0);
export const multiplyGoods = (by) => (cost) => Object.keys(cost).reduce((cost, k) => {
const c = cost[k];
if (c !== undefined) {
cost[k] = c * by;
}
return cost;
}, Object.assign({}, cost));
export const maskGoods = (goods) => (cost) => goods.reduce((outCost, key) => {
if (cost[key])
outCost[key] = cost[key];
return outCost;
}, {});
export const costMoney = ({ penny = 0, nickel = 0, wine = 0, whiskey = 0 }) => penny + 5 * nickel + wine + 2 * whiskey;
export const costEnergy = ({ coal = 0, peat = 0, wood = 0, straw = 0 }) => coal * 3 + peat * 2 + wood + straw * 0.5;
export const costFood = ({ penny = 0, grain = 0, sheep = 0, flour = 0, grape = 0, nickel = 0, malt = 0, whiskey = 0, meat = 0, bread = 0, wine = 0, beer = 0, }) => grain +
2 * sheep +
flour +
grape +
malt +
5 * meat +
3 * bread +
5 * beer +
costMoney({ penny, nickel, wine, whiskey });
export const costPoints = ({ nickel = 0, whiskey = 0, ceramic = 0, book = 0, reliquary = 0, ornament = 0, wine = 0 }) => 2 * nickel + 1 * whiskey + 3 * ceramic + 2 * book + 8 * reliquary + 4 * ornament + 1 * wine;
export const settlementCost = (cost) => ({
food: costFood(cost),
energy: costEnergy(cost),
});
export const canAfford = (cost) => (player) => any(
//
(key) => { var _a; return player[key] < ((_a = cost[key]) !== null && _a !== void 0 ? _a : 0); }, keys(cost))
? undefined
: player;
const byPoints = comparator((a, b) => costPoints(parseResourceParam(a)) > costPoints(parseResourceParam(b)));
export const rewardCostOptions = curry((totalPoints) => {
// this one is different because we want our output to be <= points, and cannot
// give the player money or whiskey/wine, but also there's an infinite amount of
// each thing they can get, so that's nice
const output = [];
pipe(rewardOptions(output, ResourceEnum.Reliquary, 8, 4), rewardOptions(output, ResourceEnum.Ornament, 4, 3), rewardOptions(output, ResourceEnum.Ceramic, 3, 2), rewardOptions(output, ResourceEnum.Book, 2, Infinity))([['', totalPoints]]);
return uniq(sort(byPoints, output));
});
export const shortGameBonusProduction = (cost) => pipe(when(isNil, always(undefined)), when(allPass([
//
pathSatisfies(equals('short'), ['config', 'length']),
pathSatisfies(gte(__, 3), ['config', 'players']),
]), withEachPlayer(getCost(cost))));
//# sourceMappingURL=resource.js.map