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.

212 lines 12.2 kB
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