import min from 'lodash/min' import chunk from 'lodash/chunk' import isInteger from 'lodash/isInteger' import sampleSize from 'lodash/sampleSize' import shuffle from 'lodash/shuffle' import sum from 'lodash/sum' import max from 'lodash/max' import head from 'lodash/head' import tail from 'lodash/tail' import compact from 'lodash/compact' export const numAges = 3 export const minPlayers = 3 export const maxPlayers = 7 export const startingGold = 3 export const discardGold = 3 export const victoryPoints = [1, 3, 5] export const defeatPoints = -1 export const goldPerPoint = 3 export const scienceSetPoints = 7 export type Resource = 'wood' | 'stone' | 'ore' | 'brick' | 'paper' | 'cloth' | 'glass' export const resources: Resource[] = ['wood', 'stone', 'ore', 'brick', 'paper', 'cloth', 'glass'] export type Science = 'engineering' | 'research' | 'education' export const sciences: Science[] = ['engineering', 'research', 'education'] export type StructureType = 'commerce' | 'culture' | 'science' | 'basic industry' | 'advanced industry' | 'guild' | 'military' export const structureTypes: StructureType[] = ['commerce', 'culture', 'science', 'basic industry', 'advanced industry', 'guild', 'military'] export type Countable = StructureType | 'wonder stage' | 'defeat' export interface Structure { type: StructureType name: string appears?: (undefined | (3 | 4 | 5 | 6 | 7)[])[] cost?: Map freeWith?: string[] paysFor?: string[] gold?: number goldPer?: Map goldPerNeighbor?: Map points?: number pointsPer?: Map pointsPerNeighbor?: Map sciences?: Set resources?: Map shields?: number discountTrade?: { resourceType: 'basic' | 'advanced' direction: ('left' | 'right')[] amount: number }[] } const structuresAge1: Structure[] = [ { type: 'advanced industry', name: 'Glassworks', appears: [[3, 6], [3, 5]], resources: new Map([['glass', 1]]) }, { type: 'advanced industry', name: 'Loom', appears: [[3, 6], [3, 5]], resources: new Map([['cloth', 1]]) }, { type: 'advanced industry', name: 'Press', appears: [[3, 6], [3, 5]], resources: new Map([['paper', 1]]) }, { type: 'basic industry', name: 'Clay Pit', appears: [[3]], cost: new Map([['gold', 1]]), resources: new Map([['brick', 1], ['ore', 1]]) }, { type: 'basic industry', name: 'Clay Pool', appears: [[3, 5]], resources: new Map([['brick', 1]]) }, { type: 'basic industry', name: 'Excavation', appears: [[4]], cost: new Map([['gold', 1]]), resources: new Map([['stone', 1], ['brick', 1]]) }, { type: 'basic industry', name: 'Forest Cave', appears: [[5]], cost: new Map([['gold', 1]]), resources: new Map([['wood', 1], ['ore', 1]]) }, { type: 'basic industry', name: 'Lumber Yard', appears: [[3, 4]], resources: new Map([['wood', 1]]) }, { type: 'basic industry', name: 'Mine', appears: [[6]], cost: new Map([['gold', 1]]), resources: new Map([['stone', 1], ['ore', 1]]) }, { type: 'basic industry', name: 'Ore Vein', appears: [[3, 4]], resources: new Map([['ore', 1]]) }, { type: 'basic industry', name: 'Stone Pit', appears: [[3, 5]], resources: new Map([['stone', 1]]) }, { type: 'basic industry', name: 'Timber Yard', appears: [[3]], cost: new Map([['gold', 1]]), resources: new Map([['stone', 1], ['wood', 1]]) }, { type: 'basic industry', name: 'Tree Farm', appears: [[6]], cost: new Map([['gold', 1]]), resources: new Map([['wood', 1], ['brick', 1]]) }, { type: 'commerce', name: 'East Trading Post', appears: [[3, 7]], discountTrade: [{ resourceType: 'basic', direction: ['right'], amount: 1 }] }, { type: 'commerce', name: 'Marketplace', appears: [[3, 6]], discountTrade: [{ resourceType: 'advanced', direction: ['left', 'right'], amount: 1 }] }, { type: 'commerce', name: 'Tavern', appears: [[4, 5, 7]], gold: 5 }, { type: 'commerce', name: 'West Trading Post', appears: [[3, 7]], discountTrade: [{ resourceType: 'basic', direction: ['left'], amount: 1 }] }, { type: 'culture', name: 'Altar', appears: [[3, 5]], points: 2 }, { type: 'culture', name: 'Baths', appears: [[3, 7]], cost: new Map([['stone', 1]]), points: 3 }, { type: 'culture', name: 'Pawnshop', appears: [[4, 7]], points: 3 }, { type: 'culture', name: 'Theater', appears: [[3, 6]], points: 2 }, { type: 'military', name: 'Barracks', appears: [[3, 5]], cost: new Map([['ore', 1]]), shields: 1 }, { type: 'military', name: 'Guard Tower', appears: [[3, 4]], cost: new Map([['brick', 1]]), shields: 1 }, { type: 'military', name: 'Stockade', appears: [[3, 7]], cost: new Map([['wood', 1]]), shields: 1 }, { type: 'science', name: 'Apothecary', appears: [[3, 5]], cost: new Map([['cloth', 1]]), sciences: new Set(['research']) }, { type: 'science', name: 'Scriptorium', appears: [[3, 4]], cost: new Map([['glass', 1]]), sciences: new Set(['education']) }, { type: 'science', name: 'Workshop', appears: [[3, 7]], cost: new Map([['glass', 1]]), sciences: new Set(['engineering']) }, ] const structuresAge2: Structure[] = [ { type: 'basic industry', name: 'Brickyard', appears: [, [3, 4]], cost: new Map([['gold', 1]]), resources: new Map([['brick', 2]]) }, { type: 'basic industry', name: 'Foundry', appears: [, [3, 4]], cost: new Map([['gold', 1]]), resources: new Map([['ore', 2]]) }, { type: 'basic industry', name: 'Sawmill', appears: [, [3, 4]], cost: new Map([['gold', 1]]), resources: new Map([['wood', 2]]) }, { type: 'basic industry', name: 'Quarry', appears: [, [3, 4]], cost: new Map([['gold', 1]]), resources: new Map([['stone', 2]]) }, { type: 'commerce', name: 'Bazar', appears: [, [4, 7]], goldPer: new Map([['advanced industry', 2]]), goldPerNeighbor: new Map([['advanced industry', 2]]) }, { type: 'commerce', name: 'Caravansery', appears: [, [3, 5, 6]], cost: new Map([['wood', 2]]), freeWith: ['Marketplace'], resources: new Map([['wood', 1], ['stone', 1], ['ore', 1], ['brick', 1]]) }, { type: 'commerce', name: 'Forum', appears: [, [3, 6, 7]], cost: new Map([['brick', 2]]), freeWith: ['East Trading Post', 'West Trading Post'], resources: new Map([['glass', 1], ['cloth', 1], ['paper', 1]]) }, { type: 'commerce', name: 'Vineyard', appears: [, [3, 6]], goldPer: new Map([['basic industry', 1]]), goldPerNeighbor: new Map([['basic industry', 1]]) }, { type: 'culture', name: 'Aqueduct', appears: [, [3, 7]], cost: new Map([['stone', 3]]), freeWith: ['Baths'], points: 5 }, { type: 'culture', name: 'Courthouse', appears: [, [3, 5]], cost: new Map([['brick', 2], ['cloth', 1]]), freeWith: ['Scriptorium'], points: 4 }, { type: 'culture', name: 'Statue', appears: [, [3, 7]], cost: new Map([['ore', 2], ['wood', 1]]), freeWith: ['Theater'], points: 4 }, { type: 'culture', name: 'Temple', appears: [, [3, 6]], cost: new Map([['brick', 1], ['wood', 1], ['glass', 1]]), freeWith: ['Altar'], points: 3 }, { type: 'military', name: 'Archery Range', appears: [, [3, 6]], cost: new Map([['wood', 2], ['ore', 1]]), freeWith: ['Workshop'], shields: 2 }, { type: 'military', name: 'Stables', appears: [, [3, 5]], cost: new Map([['wood', 1], ['ore', 1], ['brick', 1]]), freeWith: ['Apothecary'], shields: 2 }, { type: 'military', name: 'Training Ground', appears: [, [4, 6, 7]], cost: new Map([['ore', 2], ['wood', 1]]), shields: 2 }, { type: 'military', name: 'Walls', appears: [, [3, 7]], cost: new Map([['stone', 3]]), shields: 2 }, { type: 'science', name: 'Dispensary', appears: [, [3, 4]], cost: new Map([['glass', 1], ['ore', 2]]), freeWith: ['Apothecary'], sciences: new Set(['research']) }, { type: 'science', name: 'Laboratory', appears: [, [3, 5]], cost: new Map([['paper', 1], ['brick', 2]]), freeWith: ['Workshop'], sciences: new Set(['engineering']) }, { type: 'science', name: 'Library', appears: [, [3, 6]], cost: new Map([['cloth', 1], ['stone', 2]]), freeWith: ['Scriptorium'], sciences: new Set(['education']) }, { type: 'science', name: 'School', appears: [, [3, 7]], cost: new Map([['paper', 1], ['wood', 1]]), sciences: new Set(['education']) }, ] const structuresAge3: Structure[] = [ { type: 'commerce', name: 'Arena', appears: [, , [3, 3, 5, 7]], cost: new Map([['stone', 2], ['ore', 1]]), freeWith: ['Dispensary'], goldPer: new Map([['wonder stage', 3]]), pointsPer: new Map([['wonder stage', 1]]) }, { type: 'commerce', name: 'Chamber of Commerce', appears: [, , [4, 6]], cost: new Map([['brick', 2], ['paper', 1]]), goldPer: new Map([['advanced industry', 2]]), pointsPer: new Map([['advanced industry', 2]]) }, { type: 'commerce', name: 'Haven', appears: [, , [4]], cost: new Map([['wood', 1], ['ore', 1], ['paper', 1]]), goldPer: new Map([['basic industry', 1]]), pointsPer: new Map([['basic industry', 1]]) }, { type: 'commerce', name: 'Lighthouse', appears: [, , [3, 6]], cost: new Map([['stone', 1], ['glass', 1]]), freeWith: ['Caravansery'], goldPer: new Map([['commerce', 1]]), pointsPer: new Map([['commerce', 1]]) }, { type: 'culture', name: 'Gardens', appears: [, , [3, 4]], cost: new Map([['brick', 2], ['wood', 1]]), points: 5 }, { type: 'culture', name: 'Palace', appears: [, , [3, 7]], cost: new Map([['wood', 1], ['brick', 1], ['stone', 1], ['ore', 1], ['glass', 1], ['cloth', 1], ['paper', 1]]), points: 8 }, { type: 'culture', name: 'Pantheon', appears: [, , [3, 6]], cost: new Map([['brick', 2], ['ore', 1], ['glass', 1], ['paper', 1], ['cloth', 1]]), freeWith: ['Temple'], points: 7 }, { type: 'culture', name: 'Senate', appears: [, , [3, 5]], cost: new Map([['wood', 2], ['ore', 1], ['stone', 1]]), freeWith: ['Library'], points: 6 }, { type: 'culture', name: 'Town Hall', appears: [, , [3, 5, 6]], cost: new Map([['stone', 2], ['ore', 1], ['glass', 1]]), points: 6 }, { type: 'military', name: 'Arsenal', appears: [, , [3, 4, 7]], cost: new Map([['wood', 2], ['ore', 1], ['cloth', 1]]), shields: 3 }, { type: 'military', name: 'Circus', appears: [, , [4, 5, 6]], cost: new Map([['stone', 3], ['ore', 1]]), freeWith: ['Training Ground'], shields: 3 }, { type: 'military', name: 'Fortifications', appears: [, , [3, 7]], cost: new Map([['ore', 3], ['stone', 1]]), freeWith: ['Walls'], shields: 3 }, { type: 'military', name: 'Siege Workshop', appears: [, , [3, 5]], cost: new Map([['brick', 3], ['wood', 1]]), freeWith: ['Laboratory'], shields: 3 }, { type: 'science', name: 'Academy', appears: [, , [3, 7]], cost: new Map([['stone', 3], ['glass', 1]]), freeWith: ['School'], sciences: new Set(['research']) }, { type: 'science', name: 'Lodge', appears: [, , [3, 6]], cost: new Map([['brick', 2], ['paper', 1], ['cloth', 1]]), freeWith: ['Dispensary'], sciences: new Set(['research']) }, { type: 'science', name: 'Observatory', appears: [, , [3, 7]], cost: new Map([['ore', 2], ['glass', 1], ['cloth', 1]]), freeWith: ['Laboratory'], sciences: new Set(['engineering']) }, { type: 'science', name: 'Study', appears: [, , [3, 5]], cost: new Map([['wood', 1], ['paper', 1], ['cloth', 1]]), freeWith: ['School'], sciences: new Set(['engineering']) }, { type: 'science', name: 'University', appears: [, , [3, 4]], cost: new Map([['wood', 2], ['paper', 1], ['glass', 1]]), freeWith: ['Library'], sciences: new Set(['education']) }, ] const structuresGuilds: Structure[] = [ { type: 'guild', name: 'Builders Guild', cost: new Map([['stone', 2], ['brick', 2], ['glass', 1]]), pointsPer: new Map([['wonder stage', 1]]), pointsPerNeighbor: new Map([['wonder stage', 1]]) }, { type: 'guild', name: 'Craftsmens Guild', cost: new Map([['ore', 2], ['stone', 2]]), pointsPerNeighbor: new Map([['advanced industry', 2]]) }, { type: 'guild', name: 'Magistrates Guild', cost: new Map([['wood', 3], ['stone', 1], ['cloth', 1]]), pointsPerNeighbor: new Map([['culture', 1]]) }, { type: 'guild', name: 'Philosophers Guild', cost: new Map([['brick', 3], ['paper', 1], ['cloth', 1]]), pointsPerNeighbor: new Map([['science', 1]]) }, { type: 'guild', name: 'Scientists Guild', cost: new Map([['ore', 2], ['wood', 2], ['paper', 1]]), sciences: new Set(['engineering', 'research', 'education']) }, { type: 'guild', name: 'Shipowners Guild', cost: new Map([['wood', 3], ['glass', 1], ['paper', 1]]), pointsPer: new Map([['basic industry', 1], ['advanced industry', 1], ['guild', 1]]) }, { type: 'guild', name: 'Spies Guild', cost: new Map([['brick', 3], ['glass', 1]]), pointsPerNeighbor: new Map([['military', 1]]) }, { type: 'guild', name: 'Strategists Guild', cost: new Map([['ore', 2], ['stone', 1], ['cloth', 1]]), pointsPerNeighbor: new Map([['defeat', 1]]) }, { type: 'guild', name: 'Traders Guild', cost: new Map([['glass', 1], ['paper', 1], ['cloth', 1]]), pointsPerNeighbor: new Map([['commerce', 1]]) }, { type: 'guild', name: 'Workers Guild', cost: new Map([['ore', 2], ['brick', 1], ['stone', 1], ['wood', 1]]), pointsPerNeighbor: new Map([['basic industry', 1]]) }, ] export const structures = new Map([structuresAge1, structuresAge2, structuresAge3, structuresGuilds].flat().map(c => ([c.name, c]))) // Initialize 'freeWith' properties for (const structure of structures.values()) { for (const precursorName of structure.freeWith ?? []) { const precursor = structures.get(precursorName)! if (precursor.paysFor === undefined) { precursor.paysFor = [structure.name] } else { precursor.paysFor.push(structure.name) } } } export function buildDeck(age: number, numPlayers: number) { if (age < 0 || age >= numAges || !isInteger(age)) { throw new Error(`Unsupported age ${age}`) } if (numPlayers < minPlayers || numPlayers > maxPlayers || !isInteger(numPlayers)) { throw new Error(`Unsupported player count ${numPlayers}`) } const cards: Structure[] = [...structures.values()].flatMap(structure => structure.appears?.[age] ?.filter(minPlayers => numPlayers <= minPlayers) ?.map(() => structure) ?? [] ) if (age === numAges - 1) { cards.push(...sampleSize(structuresGuilds, numPlayers + 2)) // BAD } return cards } export interface Wonder { name: string innateResource: Resource //stages: Structure[] } const wonderList: Wonder[] = [ { name: 'Great Pyramid of Giza A', innateResource: 'stone' }, { name: 'Great Pyramid of Giza B', innateResource: 'stone' }, { name: 'Hanging Gardens of Babylon A', innateResource: 'brick' }, { name: 'Hanging Gardens of Babylon B', innateResource: 'brick' }, { name: 'Temple of Artemis at Ephesus A', innateResource: 'paper' }, { name: 'Temple of Artemis at Ephesus B', innateResource: 'paper' }, { name: 'Statue of Zeus at Olympia A', innateResource: 'wood' }, { name: 'Statue of Zeus at Olympia B', innateResource: 'wood' }, { name: 'Mausoleum at Halicarnassus A', innateResource: 'cloth' }, { name: 'Mausoleum at Halicarnassus B', innateResource: 'cloth' }, { name: 'Colossus of Rhodes A', innateResource: 'ore' }, { name: 'Colossus of Rhodes B', innateResource: 'ore' }, { name: 'Lighthouse of Alexandria A', innateResource: 'glass' }, { name: 'Lighthouse of Alexandria B', innateResource: 'glass' }, ] export const wonders = new Map(wonderList.map(w => [w.name, w])) export interface TurnPlan { action: 'structure' | 'discard' cardIdx: number goldLeft: number goldRight: number } export interface Player { name: string wonder?: string gold: number structures: { age: number structure: string }[] hand: string[] turnPlan?: TurnPlan leftBattles: number[] rightBattles: number[] } export function sumMaps(ms?: (Map | undefined)[]) { const sum = new Map() for (const m of ms ?? []) { for (const [k, v] of m ?? []) { sum.set(k, (sum.get(k) ?? 0) + v) } } return sum } export function playerStats(p: Player) { const structObjs = compact(p.structures.map(({ structure }) => structures.get(structure))) return { structObjs, resources: sumMaps(structObjs.map(s => s.resources)), shields: sum(structObjs.map(s => s.shields)) ?? 0, sciences: compact(structObjs.map(s => s.sciences)), } } export interface State { stage: 'starting' | 'play' | 'finished' players: Player[] age?: number turnsRemaining?: number discard: string[] } export const initial: State = { stage: 'starting', players: [], discard: [], } export const sampleState: State = { stage: 'play', age: 2, turnsRemaining: 4, discard: [ 'Scriptorium', 'Scriptorium', 'Guard Tower', 'Baths', 'Stone Pit', 'Caravansery', 'Loom', 'Loom', 'Stables', 'Courthouse', 'Craftsmens Guild', 'Strategists Guild', 'Pantheon', 'Palace', 'Chamber of Commerce' ], players: [ { name: 'Player A', gold: 2, leftBattles: [1, 3], rightBattles: [1, 3], wonder: 'Mausoleum at Halicarnassus B', hand: ['Study', 'Arsenal', 'Haven', 'Haven'], structures: [ { age: 0, structure: 'Stockade' }, { age: 0, structure: 'Barracks' }, { age: 0, structure: 'Barracks' }, { age: 0, structure: 'Glassworks' }, { age: 0, structure: 'Stone Pit' }, { age: 0, structure: 'Lumber Yard' }, { age: 1, structure: 'Walls' }, { age: 1, structure: 'Bazar' }, { age: 1, structure: 'Caravansery' }, { age: 1, structure: 'Forum' }, { age: 1, structure: 'Aqueduct' }, { age: 1, structure: 'Quarry' }, { age: 2, structure: 'Arena' }, { age: 2, structure: 'Fortifications' }, ] }, { name: 'Player B', gold: 5, leftBattles: [-1, -1], rightBattles: [-1, -1], wonder: 'Temple of Artemis at Ephesus B', hand: ['Town Hall', 'University', 'Arsenal', 'Circus'], structures: [ { age: 0, structure: 'Apothecary' }, { age: 0, structure: 'Pawnshop' }, { age: 0, structure: 'Timber Yard' }, { age: 0, structure: 'Press' }, { age: 0, structure: 'Ore Vein' }, { age: 0, structure: 'Clay Pool' }, { age: 1, structure: 'Courthouse' }, { age: 1, structure: 'Statue' }, { age: 1, structure: 'Laboratory' }, { age: 1, structure: 'Library' }, { age: 1, structure: 'Sawmill' }, { age: 1, structure: 'Foundry' }, { age: 2, structure: 'Lighthouse' }, { age: 2, structure: 'Builders Guild' }, ] }, { name: 'Player C', gold: 3, leftBattles: [1, 3], rightBattles: [1, 3], wonder: 'Hanging Gardens of Babylon B', hand: ['Town Hall', 'Magistrates Guild', 'Scientists Guild', 'Workers Guild'], structures: [ { age: 0, structure: 'Clay Pool' }, { age: 0, structure: 'Forest Cave' }, { age: 0, structure: 'Loom' }, { age: 0, structure: 'Ore Vein' }, { age: 0, structure: 'Guard Tower' }, { age: 0, structure: 'East Trading Post' }, { age: 1, structure: 'Laboratory' }, { age: 1, structure: 'Archery Range' }, { age: 1, structure: 'Dispensary' }, { age: 1, structure: 'Glassworks' }, { age: 1, structure: 'Press' }, { age: 1, structure: 'Glassworks' }, { age: 2, structure: 'Siege Workshop' }, { age: 2, structure: 'Philosophers Guild' }, ] }, { name: 'Player D', gold: 10, leftBattles: [-1, -1], rightBattles: [], wonder: 'Colossus of Rhodes A', hand: ['Senate', 'Study', 'Senate', 'Arena'], structures: [ { age: 0, structure: 'Excavation' }, { age: 0, structure: 'Lumber Yard' }, { age: 0, structure: 'Clay Pit' }, { age: 0, structure: 'Workshop' }, { age: 0, structure: 'Tavern' }, { age: 0, structure: 'West Trading Post' }, { age: 1, structure: 'Temple' }, { age: 1, structure: 'Vineyard' }, { age: 1, structure: 'Stables' }, { age: 1, structure: 'Press' }, { age: 1, structure: 'Quarry' }, { age: 1, structure: 'Brickyard' }, { age: 2, structure: 'Gardens' }, { age: 2, structure: 'Circus' }, ] }, { name: 'Player E', gold: 0, leftBattles: [], rightBattles: [-1, -1], wonder: 'Great Pyramid of Giza A', hand: ['Observatory', 'Academy', 'University', 'Lodge'], structures: [ { age: 0, structure: 'Apothecary' }, { age: 0, structure: 'Tavern' }, { age: 0, structure: 'Altar' }, { age: 0, structure: 'Theater' }, { age: 0, structure: 'Marketplace' }, { age: 0, structure: 'Altar' }, { age: 1, structure: 'Sawmill' }, { age: 1, structure: 'Foundry' }, { age: 1, structure: 'Brickyard' }, { age: 1, structure: 'Dispensary' }, { age: 1, structure: 'Training Ground' }, { age: 1, structure: 'School' }, { age: 2, structure: 'Siege Workshop' }, { age: 2, structure: 'Gardens' }, ] }, ], } export interface BaseAction { type: string } export interface AddPlayerAction extends BaseAction { type: 'add player' name: string } export function addPlayerAction(name: string): AddPlayerAction { return { type: 'add player', name, } } export interface RemovePlayerAction extends BaseAction { type: 'remove player' name: string } export function removePlayerAction(name: string): RemovePlayerAction { return { type: 'remove player', name, } } export interface ChooseWonderAction extends BaseAction { type: 'choose wonder' player: string wonder: string } export function chooseWonderAction(player: string, wonder: string): ChooseWonderAction { return { type: 'choose wonder', player, wonder, } } export interface StartGameAction extends BaseAction { type: 'start game' shuffle: number[] } export function startGameAction(numPlayers: number): StartGameAction { return { type: 'start game', shuffle: shuffle([...Array(numPlayers).keys()]), } } export interface PlanTurnAction extends BaseAction { type: 'plan turn' player: string turnPlan?: TurnPlan } export function planTurnAction(player: string, turnPlan?: TurnPlan): PlanTurnAction { return { type: 'plan turn', player, turnPlan, } } export interface ResetAction extends BaseAction { type: 'reset' } export function resetAction(): ResetAction { return { type: 'reset' } } export interface LoadSampleAction extends BaseAction { type: 'load sample' } export function loadSampleAction(): LoadSampleAction { return { type: 'load sample' } } export type Action = AddPlayerAction | RemovePlayerAction | ChooseWonderAction | StartGameAction | PlanTurnAction | ResetAction | LoadSampleAction function sumCountable(player: Player, countable: Countable): number { switch (countable) { case 'wonder stage': return 0 // TODO case 'defeat': return [...player.leftBattles, ...player.rightBattles].filter(b => b === defeatPoints).length default: return player.structures.filter(({ structure }) => structures.get(structure)?.type === countable).length } } function sumCountableMap(player: Player, map: Map): number { return sum([...map.entries()].map(([countable, number]) => number * sumCountable(player, countable))) } function scoreScience( sciences: Set[], counts: Map = new Map([['engineering', 0], ['research', 0], ['education', 0]]), ): number { if (sciences.length === 0) { return (scienceSetPoints * (min([...counts.values()]) ?? 0)) + sum([...counts.values()].map(c => c * c)) } else { return max([...head(sciences)!.values()].map(science => scoreScience(tail(sciences), new Map([...counts, [science, (counts.get(science) ?? 0) + 1]])) )) ?? 0 } } export function countPoints(state: State, playerIdx: number): number { const player = state.players[playerIdx] const left = state.players[(playerIdx + 1) % state.players.length] const right = state.players[(playerIdx - 1 + state.players.length) % state.players.length] return sum(player.leftBattles) + sum(player.rightBattles) + sum(player.structures.map(({ structure }) => { const card = structures.get(structure)! return (card.points ?? 0) + sumCountableMap(player, card.pointsPer ?? new Map()) + sumCountableMap(left, card.pointsPerNeighbor ?? new Map()) + sumCountableMap(right, card.pointsPerNeighbor ?? new Map()) })) + Math.floor(player.gold / goldPerPoint) + scoreScience(player.structures.flatMap(({ structure }) => { const sciences = structures.get(structure)?.sciences return sciences === undefined ? [] : [sciences] })) } function beginAge(state: State): State { const age = state.age === undefined ? 0 : state.age + 1 const deck = shuffle(buildDeck(age, state.players.length).map(s => s.name)) // BAD const handSize = Math.ceil(deck.length / state.players.length) const hands = chunk(deck, handSize) return { ...state, age, turnsRemaining: handSize - 1, players: state.players.map((p, i) => ({ ...p, hand: hands[i] })), } } function countShields(player: Player): number { return sum(player.structures.map(({ structure }) => structures.get(structure)?.shields)) } function battleResult(state: State, playerIdx: number, delta: number): number | undefined { const playerShields = countShields(state.players[playerIdx]) const otherShields = countShields(state.players[(playerIdx + delta + state.players.length) % state.players.length]) if (playerShields > otherShields) { return victoryPoints[state.age!] } else if (playerShields < otherShields) { return defeatPoints } } function endAge(state: State): State { return { ...state, turnsRemaining: undefined, discard: [...state.discard, ...state.players.flatMap(p => p.hand)], players: state.players.map((p, i) => { const left = battleResult(state, i, 1) const right = battleResult(state, i, -1) return { ...p, hand: [], leftBattles: [...p.leftBattles, ...(left === undefined ? [] : [left])], rightBattles: [...p.rightBattles, ...(right === undefined ? [] : [right])], } }), } } function doTurn(state: State): State { state = { ...state, players: state.players.map((player, playerIdx) => { const turnPlan = player.turnPlan! const left = state.players[(playerIdx + 1) % state.players.length] const right = state.players[(playerIdx - 1 + state.players.length) % state.players.length] const card = structures.get(player.hand[turnPlan.cardIdx])! return { ...player, gold: player.gold + left.turnPlan!.goldRight + right.turnPlan!.goldLeft + (turnPlan.action === 'discard' ? 3 : 0) + (turnPlan.action !== 'structure' ? 0 : (card.gold ?? 0) + sumCountableMap(player, card.goldPer ?? new Map()) + sumCountableMap(left, card.goldPerNeighbor ?? new Map()) + sumCountableMap(right, card.goldPerNeighbor ?? new Map()) ), hand: [ ...player.hand.slice(0, turnPlan.cardIdx), ...player.hand.slice(turnPlan.cardIdx + 1) ], structures: [ ...player.structures, ...(turnPlan.action !== 'structure' ? [] : [ { age: state.age!, structure: player.hand[turnPlan.cardIdx] } ]), ] } }), turnsRemaining: state.turnsRemaining! - 1, } if (state.turnsRemaining === 0) { state = endAge(state) if (state.age === numAges - 1) { return { ...state, stage: 'finished', age: undefined, } } else { return beginAge(state) } } else { const delta = state.age! % 2 === 1 ? -1 : 1 return { ...state, players: state.players.map((p, i) => ({ ...p, hand: state.players[(i + delta + state.players.length) % state.players.length].hand, })), } } } const reducers: { [type: string]: (state: State, action: any) => State } = { 'add player': (state, action: AddPlayerAction) => { if (state.stage !== 'starting' || state.players.some(p => p.name === action.name)) { return state } return { ...state, players: [...state.players, { name: action.name, gold: startingGold, structures: [], hand: [], leftBattles: [], rightBattles: [], }], } }, 'remove player': (state, action: RemovePlayerAction) => { if (state.stage !== 'starting') { return state } return { ...state, players: state.players.filter(p => p.name !== action.name), } }, 'choose wonder': (state, action: ChooseWonderAction) => { if (state.stage !== 'starting' || state.players.some(p => p.wonder === action.wonder)) { return state } return { ...state, players: state.players.map(p => p.name === action.player ? { ...p, wonder: action.wonder } : p), } }, 'start game': (state, action: StartGameAction) => { if (state.stage !== 'starting' || state.players.length < minPlayers || state.players.length > maxPlayers || state.players.some(p => p.wonder === undefined)) { return state } return beginAge({ ...state, stage: 'play', players: action.shuffle.map(i => state.players[i]), }) }, 'plan turn': (state, action: PlanTurnAction) => { if (state.stage !== 'play') { return state } const newState = { ...state, players: state.players.map( p => p.name !== action.player ? p : { ...p, turnPlan: action.turnPlan } ), } return newState.players.every(p => p.turnPlan !== undefined) ? doTurn(newState) : newState }, 'reset': () => initial, 'load sample': () => sampleState, } export function reducer(state: State, action: Action): State { return reducers[action.type](state, action) }