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<Resource | 'gold', number>
  freeWith?: string[]
  paysFor?: string[]

  gold?: number
  goldPer?: Map<Countable, number>
  goldPerNeighbor?: Map<Countable, number>
  points?: number
  pointsPer?: Map<Countable, number>
  pointsPerNeighbor?: Map<Countable, number>
  sciences?: Set<Science>
  resources?: Map<Resource, number>
  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([['ore', 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<string, Structure>([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 - 1]
      ?.filter(minPlayers => numPlayers <= minPlayers)
      ?.map(() => structure)
    ?? []
  )
  if (age === numAges - 1) {
    cards.push(...sampleSize(structuresGuilds, numPlayers + 2))
  }
  return cards
}

export interface Wonder {
  name: string
  innateResource: Resource
}

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<V>(ms?: (Map<V, number> | undefined)[]) {
  const sum = new Map<V, number>()
  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)),
    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 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<Countable, number>): number {
  return sum([...map.entries()].map(([countable, number]) => number * sumCountable(player, countable)))
}
function scoreScience(
  sciences: Set<Science>[],
  counts: Map<Science, number> = 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))
  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': () => ({
    ...initial, // TODO
  }),
}
export function reducer(state: State, action: Action): State {
  return reducers[action.type](state, action)
}
