diff --git a/packages/wonders-common/src/action.ts b/packages/wonders-common/src/action.ts new file mode 100644 index 0000000..926b584 --- /dev/null +++ b/packages/wonders-common/src/action.ts @@ -0,0 +1,83 @@ +import shuffle from 'lodash/shuffle' +import { TurnPlan } from './basic' + +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 diff --git a/packages/wonders-common/src/basic.ts b/packages/wonders-common/src/basic.ts new file mode 100644 index 0000000..f79fbbc --- /dev/null +++ b/packages/wonders-common/src/basic.ts @@ -0,0 +1,181 @@ +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 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 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' }, + ] + }, + ], +} diff --git a/packages/wonders-common/src/index.ts b/packages/wonders-common/src/index.ts index 02ad455..68aa125 100644 --- a/packages/wonders-common/src/index.ts +++ b/packages/wonders-common/src/index.ts @@ -1,694 +1,6 @@ -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) -} +export * from './basic' +export * from './structure' +export * from './wonder' +export * from './util' +export * from './action' +export * from './reducer' diff --git a/packages/wonders-common/src/reducer.ts b/packages/wonders-common/src/reducer.ts new file mode 100644 index 0000000..10bf4e2 --- /dev/null +++ b/packages/wonders-common/src/reducer.ts @@ -0,0 +1,166 @@ +import chunk from "lodash/chunk" +import shuffle from "lodash/shuffle" +import sum from 'lodash/sum' +import { Action, AddPlayerAction, ChooseWonderAction, PlanTurnAction, RemovePlayerAction, StartGameAction } from "./action" +import { defeatPoints, initial, maxPlayers, minPlayers, numAges, Player, sampleState, startingGold, State, victoryPoints } from "./basic" +import { structures } from "./structure" +import { buildDeck, sumCountableMap } from "./util" + +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) +} diff --git a/packages/wonders-common/src/structure.ts b/packages/wonders-common/src/structure.ts new file mode 100644 index 0000000..688faee --- /dev/null +++ b/packages/wonders-common/src/structure.ts @@ -0,0 +1,129 @@ +import { Resource, Science } from "./basic" + +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 + }[] +} + +export const structuresByAge: 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']) }, + ], [ + { 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']) }, + ], [ + { 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']) }, + ], +] + +export 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([...structuresByAge, structuresGuilds].flat().map(c => ([c.name, c]))) +// Initialize 'paysFor' 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) + } + } +} diff --git a/packages/wonders-common/src/util.ts b/packages/wonders-common/src/util.ts new file mode 100644 index 0000000..78661ed --- /dev/null +++ b/packages/wonders-common/src/util.ts @@ -0,0 +1,97 @@ +import { defeatPoints, goldPerPoint, maxPlayers, minPlayers, numAges, Player, Science, scienceSetPoints, State } from "./basic" +import isInteger from 'lodash/isInteger' +import sampleSize from 'lodash/sampleSize' +import compact from 'lodash/compact' +import sum from 'lodash/sum' +import min from 'lodash/min' +import max from 'lodash/max' +import head from 'lodash/head' +import tail from 'lodash/tail' +import { Countable, Structure, structures, structuresGuilds } from "./structure" + +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 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 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 + } +} + +export function sumCountableMap(player: Player, map: Map): number { + return sum([...map.entries()].map(([countable, number]) => number * sumCountable(player, countable))) +} + +export 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] + })) +} diff --git a/packages/wonders-common/src/wonder.ts b/packages/wonders-common/src/wonder.ts new file mode 100644 index 0000000..ec7f2ad --- /dev/null +++ b/packages/wonders-common/src/wonder.ts @@ -0,0 +1,54 @@ +import { Resource } from "./basic" + +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]))