"use strict"

const data = require("./data.js")

function find_scenario(n) {
	let ix = data.scenarios.findIndex(s => s.number === n)
	if (ix < 0)
		throw new Error("cannot find scenario " + n)
	return ix
}

function find_card(s, n) {
	let ix = data.cards.findIndex(c => c.scenario === s && c.name === n)
	if (ix < 0)
		throw new Error("cannot find card " + n)
	return ix
}

// for (let c of data.cards) for (let a of c.actions) console.log(a.type + ":", a.effect)
// for (let c of data.cards) { if (c.rule_text_1) console.log(c.rule_text_1); if (c.rule_text_2) console.log(c.rule_text_2) }
// for (let c of data.cards) console.log(c.dice)
// for (let c of data.cards) for (let a of c.actions) { if (a.type === "Counterattack") console.log(c.number, a.type, a.sequence, a.target) }

function check_attack_res(c, a) {
	if (a.choice)
		return
	if (c.rules && c.rules["attack_reserve"])
		return
	if (c.rules && c.rules["ignore_reserve"])
		return
	let dead = []
	let last = a.target_list[a.target_list.length-1]
	for (let tid of a.target_list) {
		let t = data.cards[tid]
		if (!t.reserve) {
			// all good, targetable out of reserve
			dead.push(tid)
		} else if (Array.isArray(t.reserve)) {
			// in reserve
			for (let rid of t.reserve)
				if (!dead.includes(rid) && tid !== last)
					console.log("BLOCK (RES)", c.scenario, c.number, a.target, "(" + t.name + ")", "\n\t" + c.rule_text_1)
			dead.push(tid)
		} else {
			// commanded?
			if (tid !== last)
				console.log("BLOCK (CMD)", c.scenario, c.number, a.target, "(" + t.name + ")", "\n\t" + c.rule_text_1)
		}
	}
}

//for (let c of data.cards) for (let a of c.actions) { if (a.type === "Attack") check_attack_res(c, a) }

const P1 = "First"
const P2 = "Second"

var states = {}
var game = null
var view = null

const POOL = -1

const RED = 0
const PINK = 1
const BLUE = 2
const DKBLUE = 3

exports.scenarios = {
	"": [ "Random" ],
}

const scenario_roles = {}

for (let s of data.scenarios) {
	let id = s.number + " - " + s.name
	let x = s.expansion
	if (!(x in exports.scenarios)) {
		exports.scenarios[""].push("Random - " + x)
		exports.scenarios[x] = []
	}
	exports.scenarios[x].push(id)
	scenario_roles[id] = [ s.players[0].name, s.players[1].name ]
}

exports.is_random_scenario = function (scenario) {
	return scenario.startsWith("Random")
}

exports.select_random_scenario = function (scenario, seed) {
	if (scenario === "Random") {
		let info = data.scenarios[seed % data.scenarios.length]
		return info.number + " - " + info.name
	}
	if (scenario.startsWith("Random - ")) {
		let list = exports.scenarios[scenario.replace("Random - ", "")]
		return list[seed % list.length]
	}
	return scenario
}

exports.roles = [ P1, P2 ]

exports.action = function (state, player, action, arg) {
	game = state
	let S = states[game.state]
	if (action in S)
		S[action](arg, player)
	else if (action === "undo" && game.undo && game.undo.length > 0)
		pop_undo()
	else
		throw new Error("Invalid action: " + action)
	return game
}

// JSON Schema for view data
exports.VIEW_SCHEMA = {
	type: "object",
	properties: {
		log: { type: "array", items: { type: "string" } },
		prompt: { type: "string" },
		scenario: { type: "integer" },

		dice: { type: "array", minItems: 24, maxItems: 24, items: { type: "integer", minimum: -1 } },
		sticks: { type: "array", items: { type: "integer", minimum: 0 } },
		cubes: { type: "array", items: { type: "integer", minimum: 0 } },

		morale: { type: "array", minItems: 2, maxItems: 2, items: { type: "integer", minimum: 0 } },
		tv1: { type: "integer", minimum: 0 },
		tv2: { type: "integer", minimum: 0 },
		front: { type: "array", items: { type: "array", items: { type: "integer", minimum: 0 } } },
		reserve: { type: "array", items: { type: "array", items: { type: "integer", minimum: 0 } } },

		selected: { type: "integer", minimum: -1 },
		target: { type: "integer", minimum: -1 },
		hits: { type: "integer", minimum: 0 },
		self: { type: "integer", minimum: 0 },

		shift: { type: "array", items: { type: "integer", minimum: 0 } },
		target2: { type: "integer", minimum: 0 },
		hits2: { type: "integer", minimum: 0 },

		actions: { type: "object" },
	},
	required: [
		"log",
		"prompt",
		"scenario",
		"dice",
		"sticks",
		"cubes",
		"morale",
		"tv1",
		"tv2",
		"front",
		"reserve",
		"selected",
		"target",
		"hits",
		"self",
	],
	additionalProperties: false,
}

exports.view = function (state, player) {
	game = state

	view = {
		log: game.log,
		prompt: null,
		scenario: game.scenario,
		dice: game.dice,
		sticks: game.sticks,
		cubes: game.cubes,
		morale: game.morale,
		tv1: get_tactical_victory_points(0),
		tv2: get_tactical_victory_points(1),
		front: game.front,
		reserve: game.reserve,
		selected: game.selected,
		target: game.target,
		hits: game.hits,
		self: game.self,
	}

	if (game.target2 >= 0 && game.hits2 >= 0) {
		view.target2 = game.target2
		view.hits2 = game.hits2
	}

	if (game.shift)
		view.shift = game.shift

	if (game.state === "game_over") {
		view.prompt = game.victory
	} else if (player !== game.active) {
		let inactive = states[game.state].inactive || game.state
		view.prompt = `Waiting for ${player_name(player_index())} to ${inactive}.`
	} else {
		view.actions = {}
		states[game.state].prompt()
		if (game.undo && game.undo.length > 0)
			view.actions.undo = 1
		else
			view.actions.undo = 0
	}

	return view
}

function goto_game_over(result, victory) {
	if (result === P1)
		victory = player_name(0) + " won:\n" + victory
	else if (result === P2)
		victory = player_name(1) + " won:\n" + victory
	else
		victory = result + ":\n" + victory
	game.state = "game_over"
	game.active = "None"
	game.result = result
	game.victory = victory
	log("")
	log(victory)
	return true
}

states.game_over = {
	prompt() {
		view.prompt = game.victory
	},
}

// === SPECIAL RULES - CARD NUMBERS ===

const S2_MARSTON_MOOR = find_scenario(2)
const S2_RUPERTS_LIFEGUARD = find_card(2, "Rupert's Lifeguard")
const S2_NORTHERN_HORSE = find_card(2, "Northern Horse")
const S2_TILLIERS_LEFT = find_card(2, "Tillier's Left")
const S2_TILLIERS_RIGHT = find_card(2, "Tillier's Right")
const S2_BYRON = find_card(2, "Byron")

const S4_BOSWORTH_FIELD = find_scenario(4)
const S4_THE_STANLEYS = find_card(4, "The Stanleys")
const S4_NORTHUMBERLAND = find_card(4, "Northumberland")

const S7_THE_DUNES = find_scenario(7)
const S7_THE_ENGLISH_FLEET = find_card(7, "The English Fleet")
const S7_DON_JUAN_JOSE = find_card(7, "Don Juan Jose")
const S7_SPANISH_RIGHT_CAVALRY = find_card(7, "Spanish Right Cavalry")

const S8_BROOKLYN_HEIGHTS = find_scenario(8)
const S8_CLINTON = find_card(8, "Clinton")
const S8_GRANT = find_card(8, "Grant")
const S8_HESSIANS = find_card(8, "Hessians")

const S36_FOURTH_LINE = find_card(36, "The Fourth Line")

const S3201_GAINES_MILL = find_scenario(3201)
const S3201_JACKSON = find_card(3201, "Jackson")
const S3201_DH_HILL = find_card(3201, "D.H. Hill")
const S3201_AP_HILL = find_card(3201, "A.P. Hill")
const S3201_LONGSTREET = find_card(3201, "Longstreet")

const S9_ST_ALBANS = find_scenario(9)
const S9_HENRY_VI = find_card(9, "Henry VI")
const S9_SHROPSHIRE_LANE = find_card(9, "Shropshire Lane")
const S9_SOPWELL_LANE = find_card(9, "Sopwell Lane")
const S9_ARCHERS = find_card(9, "Archers")

const S11_MORTIMERS_CROSS = find_scenario(11)
const S12_TOWTON = find_scenario(12)
const S13_EDGECOTE_MOOR = find_scenario(13)

const S15_TEWKESBURY = find_scenario(15)
const S15_A_PLUMP_OF_SPEARS = find_card(15, "A Plump of Spears")
const S15_SOMERSET = find_card(15, "Somerset")
const S15_WENLOCK = find_card(15, "Wenlock")

const S14_BARNET = find_scenario(14)
const S14_TREASON = find_card(14, "\"Treason!\"")

const S16_STOKE_FIELD = find_scenario(16)

const S22_GABIENE = find_scenario(22)
const S22_EUMENES_CAMP = find_card(22, "Eumenes's Camp")
const S22_SILVER_SHIELDS = find_card(22, "The Silver Shields")

const S25_WHEATFIELD = find_scenario(25)
const S25_STONY_HILL = find_card(25, "Stony Hill")
const S25_WOFFORD = find_card(25, "Wofford")
const S25_ZOOK = find_card(25, "Zook")
const S25_KELLY = find_card(25, "Kelly")

const S26_PEACH_ORCHARD = find_scenario(26)
const S26_FATAL_BLUNDER = find_card(26, "Fatal Blunder")

const S28_CULPS_HILL = find_scenario(28)
const S28_BREASTWORKS = find_card(28, "Breastworks")
const S28_GEARY = find_card(28, "Geary")

const S29_GETTYS_2ND = find_scenario(29)
const S29_MCLAWS = find_card(29, "McLaws")
const S29_ANDERSON = find_card(29, "Anderson")
const S29_HOOD = find_card(29, "Hood")
const S29_EARLY = find_card(29, "Early")
const S29_JOHNSON = find_card(29, "Johnson")
const S29_MEADE = find_card(29, "Meade")
const S29_LITTLE_ROUND_TOP = find_card(29, "Little Round Top")

const S30_EDGEHILL = find_scenario(30)
const S30_BALFOUR = find_card(30, "Balfour")
const S30_STAPLETON = find_card(30, "Stapleton")
const S30_RUPERT = find_card(30, "Rupert of the Rhine")
const S30_WILMOT = find_card(30, "Wilmot")
const S30_ESSEX = find_card(30, "Charles Essex")

const S31_NEWBURY_1ST = find_scenario(31)
const S31_BYRON = find_card(31, "Byron")
const S31_SKIPPON = find_card(31, "Skippon")
const S31_WENTWORTH = find_card(31, "Wentworth")
const S31_GERARD = find_card(31, "Gerard")

const S34_TULLIBARDINE = find_card(34, "Tullibardine")

const S35_AULDEARN = find_scenario(35)
const S35_MONTROSE = find_card(35, "Montrose")
const S35_GORDON = find_card(35, "Gordon")

const S37_INKERMAN = find_scenario(37)
const S37_PAULOFFS_LEFT = find_card(37, "Pauloff's Left")
const S37_BRITISH_TROOPS = find_card(37, "British Troops")
const S37_FRENCH_TROOPS = find_card(37, "French Troops")
const S37_SOIMONOFF = find_card(37, "Soimonoff")
const S37_THE_FOG = find_card(37, "The Fog")

const S38_FLEURUS = find_scenario(38)
const S38_WALDECK = find_card(38, "Waldeck")
const S38_RETREAT_TO_NIVELLES = find_card(38, "Retreat to Nivelles")
const S38_LUXEMBOURGS_HORSE = find_card(38, "Luxembourg's Horse")
const S38_GOURNAYS_HORSE = find_card(38, "Gournay's Horse")
const S38_DUTCH_LEFT_FOOT = find_card(38, "Dutch Left Foot")
const S38_DUTCH_HORSE = find_card(38, "Dutch Horse")

const S39_MARSAGLIA = find_scenario(39)
const S39_CANNONS = find_card(39, "Cannons")
const S39_EUGENE = find_card(39, "Eugene")
const S39_DUKE_OF_SAVOY = find_card(39, "Duke of Savoy")
const S39_BAYONETS = find_card(39, "Bayonets!")
const S39_CATINAT = find_card(39, "Catinat")
const S39_HOGUETTE = find_card(39, "Hoguette")

const S40_CHIARI = find_scenario(40)
const S40_CASSINES_I = find_card(40, "Cassines I")
const S40_NIGRELLI = find_card(40, "Nigrelli")
const S40_KRIECHBAUM = find_card(40, "Kriechbaum")
const S40_CASSINES_II = find_card(40, "Cassines II")
const S40_MANNSFELDT = find_card(40, "Mannsfeldt")
const S40_GUTTENSTEIN = find_card(40, "Guttenstein")

const S41_BLENHEIM_SCENARIO = find_scenario(41)
const S41_CLERAMBAULT = find_card(41, "Clerambault")
const S41_BLENHEIM_CARD = find_card(41, "Blenheim")
const S41_PRINCE_EUGENE = find_card(41, "Prince Eugene")

const S42_RAMILLIES = find_scenario(42)
const S42_MARLBOROUGH = find_card(42, "Marlborough")
const S42_DUTCH_GUARDS = find_card(42, "Dutch Guards")

const S43_DENAIN = find_scenario(43)
const S43_DUTCH_HORSE = find_card(43, "Dutch Horse")
const S43_VILLARS_LEFT = find_card(43, "Villars's Left")
const S43_BROGLIE = find_card(43, "Broglie")
const S43_PRINCE_DE_TINGRY = find_card(43, "Prince de Tingry")

const S44_HOHENFRIEDBERG = find_scenario(44)
const S44_CHARLES = find_card(44, "Charles")
const S44_FREDERICK_II = find_card(44, "Frederick II")
const S44_BAYREUTH_DRAGOONS = find_card(44, "Bayreuth Dragoons")
const S44_LEOPOLDS_L = find_card(44, "Leopold's Left")
const S44_LEOPOLDS_C = find_card(44, "Leopold's Center")
const S44_LEOPOLDS_R = find_card(44, "Leopold's Right")
const S44_DU_MOULIN = find_card(44, "Du Moulin")
const S44_SAXON_HORSE = find_card(44, "Saxon Horse")

const S45_SOOR = find_scenario(45)
const S45_AUSTRIAN_GUNS = find_card(45, "Austrian Guns")
const S45_CUIRASSIERS = find_card(45, "Cuirassiers")

const S46_ROCOUX = find_scenario(46)
const S46_AUSTRIANS = find_card(46, "Austrians")
const S46_THE_MOUTH_OF_HELL = find_card(46, "The Mouth of Hell")

const S47_PRAGUE = find_scenario(47)
const S47_BROWNE = find_card(47, "Browne")
const S47_SCHWERIN = find_card(47, "Schwerin")
const S47_CHARLES_LORRAINE = find_card(47, "Charles Lorraine")

const S48_BRESLAU = find_scenario(48)
const S48_AUSTRIAN_GUNS = find_card(48, "Austrian Guns")
const S48_PRUSSIAN_GUNS = find_card(48, "Prussian Guns")
const S48_BEVERN = find_card(48, "Bevern")
const S48_GRENZERS = find_card(48, "Grenzers")

const S49_LEUTHEN = find_scenario(49)
const S49_RETZOW = find_card(49, "Retzow")
const S49_COLLOREDO = find_card(49, "Colloredo")
const S49_NADASDY = find_card(49, "Nadasdy")
const S49_BORNE = find_card(49, "Borne")
const S49_FEINT = find_card(49, "Feint")
const S49_POOR_CHARLES = find_card(49, "Poor Charles :-(")
const S49_THE_LEUTHEN_CHORALE = find_card(49, "The Leuthen Chorale")

// === SETUP ===

exports.setup = function (seed, scenario, options) {
	scenario = parseInt(scenario)
	scenario = data.scenarios.findIndex(s => s.number === scenario)
	if (scenario < 0)
		throw Error("cannot find scenario: " + scenario)

	let info = data.scenarios[scenario]

	game = {
		seed: seed,
		scenario: scenario,
		log: [],
		undo: [],

		active: P1,
		state: "roll",

		reacted: -1,

		// dice value and position
		dice: [
			0, POOL, 0, POOL, 0, POOL, 0, POOL, 0, POOL, 0, POOL,
			0, POOL, 0, POOL, 0, POOL, 0, POOL, 0, POOL, 0, POOL,
		],

		// sticks (map normal formation -> count)
		sticks: [],

		// cubes (map special formation -> count)
		cubes: [],

		morale: [ info.players[0].morale, info.players[1].morale ],
		lost: [ 0, 0 ],
		front: [ [], [], ],
		reserve: [ [], [] ],

		// dice value placed on what card
		rolled: 0,
		placed: [],
		summary: 0,

		// current action
		routed: [ 0, 0 ],
		selected: -1,
		target: -1,
		hits: 0,
		self: 0,

		// for breastworks etc
		attack_target: -1, // original target
		self2: 0,
		target2: -1,
		hits2: 0,
	}

	// Charles Alexander of Lorraine -- shift special
	if (info.number >= 44 && info.number <= 49)
		game.shift = []

	function setup_formation(front, reserve, c) {
		let card = data.cards[c]
		if (card.reserve)
			set_add(reserve, c)
		else
			set_add(front, c)
		if (card.special) {
			if (card_has_rule(c, "start_with_no_cubes"))
				add_cubes(c, 0)
			else
				add_cubes(c, 1)
		} else {
			set_sticks(c, card.strength)
		}
	}

	for (let p = 0; p < 2; ++p) {
		for (let c of info.players[p].cards)
			setup_formation(game.front[p], game.reserve[p], c)
	}

	log(".h1 " + info.name)
	log(".h2 " + info.date)
	log(".img")
	log("")

	if (info.players[0].tactical > 0 || info.players[1].tactical > 0) {
		if (game.scenario === S38_FLEURUS)
			log("Tactical Draw:")
		else
			log("Tactical Victory:")
		if (info.players[0].tactical > 0)
			log(">" + player_name(0) + ": " + info.players[0].tactical)
		if (info.players[1].tactical > 0)
			log(">" + player_name(1) + ": " + info.players[1].tactical)
		log("")
	}

	if (info.lore_text) {
		for (let line of info.lore_text.split("<p>"))
			log(".lore " + line)
		log("")
	}

	if (info.rule_text) {
		for (let line of info.rule_text.split("<p>"))
			log(".rule " + line)
		log("")
	}

	if (game.scenario === S37_INKERMAN) {
		map_set(game.cubes, S37_THE_FOG, 3)
	}

	if (game.scenario === S38_FLEURUS) {
		map_set(game.cubes, S38_RETREAT_TO_NIVELLES, 3)
	}

	goto_start_turn()

	return game
}

// === GAME STATE ACCESSORS ===

function count_total_cubes() {
	let n = game.morale[0] + game.morale[1]
	for (let i = 1; i < game.cubes.length; i += 2)
		n += game.cubes[i]
	return n
}

function is_infantry(c) {
	return !!data.cards[c].infantry
}

function is_cavalry(c) {
	return !!data.cards[c].cavalry
}

function card_has_rule(c, name) {
	let rules = data.cards[c].rules
	if (rules)
		return rules[name]
	return false
}

function card_name(c) {
	return data.cards[c].name
}

function player_name(p) {
	return data.scenarios[game.scenario].players[p].name
}

function set_opponent_active() {
	if (game.active === P1)
		game.active = P2
	else
		game.active = P1
}

function player_index() {
	if (game.active === P1)
		return 0
	return 1
}

function get_cubes(c) {
	return map_get(game.cubes, c, 0)
}

function add_cubes(c, n) {
	let limit = data.cards[c].special
	let old = get_cubes(c)
	map_set(game.cubes, c, Math.min(limit, old + n))
}

function remove_cubes(c, n) {
	let old = get_cubes(c)
	map_set(game.cubes, c, Math.max(0, old - n))
}

function get_sticks(c) {
	return map_get(game.sticks, c, 0)
}

function set_sticks(c, n) {
	map_set(game.sticks, c, n)
}

function move_sticks(from, to, n) {
	// log(`Moved ${n} sticks from C${from} to C${to}.`) // TODO?
	set_sticks(from, get_sticks(from) - n)
	set_sticks(to, get_sticks(to) + n)
}

function move_all_sticks(from, to) {
	move_sticks(from, to, get_sticks(from))
}

function get_shift_sticks(c) {
	if (game.shift)
		return map_get(game.shift, c, 0)
	return 0
}

function set_shift_sticks(c, n) {
	if (game.shift) {
		if (n)
			map_set(game.shift, c, n)
		else
			map_delete(game.shift, c)
	}
}

function remove_sticks(c, n) {
	let p = find_card_owner(c)
	let old = get_sticks(c)
	n = Math.min(n, old)
	game.lost[p] += n
	set_sticks(c, old - n)
}

function remove_dice(c) {
	for (let i = 0; i < 12; ++i) {
		if (get_dice_location(i) === c) {
			set_dice_location(i, POOL)
			set_dice_value(i, 0)
		}
	}
}

function take_all_dice(from, to) {
	log("Dice from C" + from)
	for (let i = 0; i < 12; ++i) {
		if (get_dice_location(i) === from) {
			set_dice_location(i, to)
		}
	}
}

function take_one_die(from, to) {
	log("Die from C" + from)
	for (let i = 0; i < 12; ++i) {
		if (get_dice_location(i) === from) {
			set_dice_location(i, to)
			if (to === POOL)
				set_dice_value(i, 0)
			to = POOL
		}
	}
}

function take_wild_die(from, to) {
	log("Wild die from C" + from)
	for (let i = 0; i < 12; ++i) {
		if (get_dice_location(i) === from) {
			set_dice_location(i, to)
			set_dice_value(i, 0)
			to = POOL
		}
	}
}

function eliminate_card(c) {
	remove_dice(c)
	map_delete(game.cubes, c)
	map_delete(game.sticks, c)
	set_delete(game.front[0], c)
	set_delete(game.front[1], c)
	set_delete(game.reserve[0], c)
	set_delete(game.reserve[1], c)
}

function rout_card(c) {
	let p = find_card_owner(c)
	game.lost[p] += get_sticks(c)
	game.lost[p] += get_shift_sticks(c)
	log("C" + c + " routed.")
	eliminate_card(c)
}

function pursue_card(c) {
	let p = find_card_owner(c)
	game.lost[p] += get_shift_sticks(c) // TODO ?
	eliminate_card(c)
}

function retire_card(c) {
	let p = find_card_owner(c)
	game.lost[p] += get_shift_sticks(c) // TODO ?
	eliminate_card(c)
}

function remove_card(c) {
	let p = find_card_owner(c)

	log("C" + c + " removed.")

	game.lost[p] += get_shift_sticks(c) // TODO ?

	if (game.scenario === S49_LEUTHEN) {
		if (c === S49_BORNE)
			game.lost[0] += get_sticks(S49_BORNE)
	}

	eliminate_card(c)
}

function pay_for_action(c) {
	if (game.scenario === S46_ROCOUX) {
		if (c === S46_THE_MOUTH_OF_HELL) {
			// icky test for second reaction...
			if (game.state === "screen") {
				remove_cubes(c, 2)
				return
			}
		}
	}

	if (data.cards[c].special)
		remove_cubes(c, 1)
	else
		remove_dice(c)
}

function get_dice_value(d) {
	return game.dice[d * 2]
}

function get_dice_location(i) {
	return game.dice[i * 2 + 1]
}

function set_dice_location(d, v) {
	game.dice[d * 2 + 1] = v
}

function set_dice_value(d, v) {
	game.dice[d * 2] = v
}

// === HOW TO WIN ===

function is_card_in_play(c) {
	return (
		set_has(game.front[0], c) ||
		set_has(game.front[1], c)
	)
}

function is_card_in_reserve(c) {
	return (
		set_has(game.reserve[0], c) ||
		set_has(game.reserve[1], c)
	)
}

function is_card_in_play_or_reserve(c) {
	return (
		set_has(game.front[0], c) ||
		set_has(game.front[1], c) ||
		set_has(game.reserve[0], c) ||
		set_has(game.reserve[1], c)
	)
}

function is_removed_from_play(c) {
	return !is_card_in_play_or_reserve(c)
}

function card_has_active_link(c) {
	// Ignores Link.
	if (game.scenario === S48_BRESLAU) {
		if (game.selected === S48_GRENZERS)
			return false
	}

	let link = data.cards[c].link
	if (link) {
		for (let t of link)
			if (is_card_in_play(t))
				return true
	}
	return false
}

function card_has_attack_with_valid_target(c) {
	for (let a of data.cards[c].actions) {
		if (a.type === "Attack") {
			let attack_reserve = card_has_rule(c, "attack_reserve")
			for (let t of a.target_list) {
				if (is_card_in_play(t))
					return true
				if (attack_reserve && is_card_in_reserve(t))
					return true
			}
		}
	}
	return false
}

function is_impossible_to_attack() {
	let p = player_index()
	for (let c of game.front[p])
		if (card_has_attack_with_valid_target(c))
			return false
	return true
}

function check_impossible_to_attack_victory() {
	if (is_impossible_to_attack()) {
		if (player_index() === 0)
			return goto_game_over(P2, player_name(0) + " has no more attacks!")
		else
			return goto_game_over(P1, player_name(1) + " has no more attacks!")
	}
	return false
}

function get_tactical_victory_points(p) {
	let n = game.lost[1-p]

	if (game.scenario === S46_ROCOUX) {
		if (p === 0)
			n += get_sticks(S46_AUSTRIANS)
	}

	if (game.scenario === S47_PRAGUE) {
		if (p === 1)
			n += get_sticks(S47_CHARLES_LORRAINE)
	}

	return n
}

function check_victory() {
	let info = data.scenarios[game.scenario]

	if (game.scenario === S39_MARSAGLIA) {
		if (is_removed_from_play(S39_HOGUETTE) && is_removed_from_play(S39_CATINAT))
			return goto_game_over(P1, player_name(1) + " lost both linked formations!")
		if (is_removed_from_play(S39_DUKE_OF_SAVOY) && is_removed_from_play(S39_EUGENE))
			return goto_game_over(P2, player_name(0) + " lost both linked formations!")
	}

	if (game.scenario === S43_DENAIN) {
		if (is_removed_from_play(S43_PRINCE_DE_TINGRY))
			return goto_game_over(P1, "Eugene is able to cross!")
	}

	if (game.scenario === S49_LEUTHEN) {
		if (is_removed_from_play(S49_NADASDY) && is_card_in_reserve(S49_COLLOREDO))
			return goto_game_over(P1, "Nadasdy routed before Colloredo entered play!")
	}

	if (game.morale[0] === 0)
		return goto_game_over(P2, player_name(0) + " ran out of morale!")
	if (game.morale[1] === 0)
		return goto_game_over(P1, player_name(1) + " ran out of morale!")

	let tc1 = info.players[0].tactical
	let tc2 = info.players[1].tactical
	let tv1 = get_tactical_victory_points(0)
	let tv2 = get_tactical_victory_points(1)

	if (game.scenario === S38_FLEURUS) {
		if (tv2 >= 22)
			return goto_game_over("Draw", player_name(1) + " secured a draw!")
	}

	if (tc1 > 0 && tc2 > 0 && tv1 >= tc1 && tv2 >= tc2)
		return goto_game_over("Draw", player_name(0) + " and " + player_name(1) + " achieved tactical victory at the same time.")
	if (tc1 > 0 && tv1 >= tc1)
		return goto_game_over(P1, player_name(0) + " won a tactical victory!")
	if (tc2 > 0 && tv2 >= tc2)
		return goto_game_over(P2, player_name(1) + " won a tactical victory!")

	return false
}

// === ROLL PHASE ===

function is_pool_die(i, v) {
	let p = player_index()
	return get_dice_location(p * 6 + i) < 0 && get_dice_value(p * 6 + i) === v
}

function is_pool_die_range(i, lo, hi) {
	let p = player_index()
	if (get_dice_location(p * 6 + i) < 0) {
		let v = get_dice_value(p * 6 + i)
		return v >= lo && v <= hi
	}
	return false
}

function placed_any_dice() {
	return game.placed.length > 0
}

function placed_any_dice_on_wing(w) {
	for (let i = 0; i < game.placed.length; i += 2) {
		let c = game.placed[i]
		if (data.cards[c].wing === w)
			return true
	}
	return false
}

function is_straight_4_or_3(_) {
	if (game.scenario === S28_CULPS_HILL) {
		if (game.rolled >= 5)
			return 4
		else
			return 3
	}

	if (game.scenario === S31_NEWBURY_1ST) {
		if (is_card_in_play(S31_SKIPPON))
			return 4
		else
			return 3
	}

	if (game.scenario === S48_BRESLAU) {
		if (game.rolled >= 5)
			return 4
		else
			return 3
	}

	throw new Error("Missing rule for Straight 3/4 choice")
}

const place_dice_once = {
	"(1)": true,
	"(2)": true,
	"(3)": true,
	"(4)": true,
	"(5)": true,
	"(6)": true,
	"(1/2)": true,
	"(1-3)": true,
	"(1-4)": true,
	"(1-5)": true,
	"(2/3)": true,
	"(2-4)": true,
	"(2-5)": true,
	"(2-6)": true,
	"(3/4)": true,
	"(3-5)": true,
	"(3-6)": true,
	"(4/5)": true,
	"(4-6)": true,
	"(5/6)": true,
}

const place_dice_check = {
	"Full House": check_full_house,
	"Straight 4/3": check_straight_4_or_3,
	"Straight 3": check_straight_3,
	"Straight 4": check_straight_4,
	"Doubles": check_doubles,
	"Triples": check_triples,
	"1": (c) => check_single(c, 1),
	"2": (c) => check_single(c, 2),
	"3": (c) => check_single(c, 3),
	"4": (c) => check_single(c, 4),
	"5": (c) => check_single(c, 5),
	"6": (c) => check_single(c, 6),
	"(1)": (c) => check_single(c, 1),
	"(2)": (c) => check_single(c, 2),
	"(3)": (c) => check_single(c, 3),
	"(4)": (c) => check_single(c, 4),
	"(5)": (c) => check_single(c, 5),
	"(6)": (c) => check_single(c, 6),
	"Any": (c) => check_range(c, 1, 6),
	"1/2": (c) => check_range(c, 1, 2),
	"1-3": (c) => check_range(c, 1, 3),
	"1-4": (c) => check_range(c, 1, 4),
	"1-5": (c) => check_range(c, 1, 5),
	"2/3": (c) => check_range(c, 2, 3),
	"2-4": (c) => check_range(c, 2, 4),
	"2-5": (c) => check_range(c, 2, 5),
	"2-6": (c) => check_range(c, 2, 6),
	"3/4": (c) => check_range(c, 3, 4),
	"3-5": (c) => check_range(c, 3, 5),
	"3-6": (c) => check_range(c, 3, 6),
	"4/5": (c) => check_range(c, 4, 5),
	"4-6": (c) => check_range(c, 4, 6),
	"5/6": (c) => check_range(c, 5, 6),
	"(1/2)": (c) => check_range(c, 1, 2),
	"(1-3)": (c) => check_range(c, 1, 3),
	"(1-4)": (c) => check_range(c, 1, 4),
	"(1-5)": (c) => check_range(c, 1, 5),
	"(2/3)": (c) => check_range(c, 2, 3),
	"(2-4)": (c) => check_range(c, 2, 4),
	"(2-5)": (c) => check_range(c, 2, 5),
	"(2-6)": (c) => check_range(c, 2, 6),
	"(3/4)": (c) => check_range(c, 3, 4),
	"(3-5)": (c) => check_range(c, 3, 5),
	"(3-6)": (c) => check_range(c, 3, 6),
	"(4/5)": (c) => check_range(c, 4, 5),
	"(4-6)": (c) => check_range(c, 4, 6),
	"(5/6)": (c) => check_range(c, 5, 6),
}

const place_dice_gen = {
	"Full House": gen_full_house,
	"Straight 4/3": gen_straight_4_or_3,
	"Straight 3": gen_straight_3,
	"Straight 4": gen_straight_4,
	"Doubles": gen_doubles,
	"Triples": gen_triples,
	"1": (c) => gen_single(c, 1),
	"2": (c) => gen_single(c, 2),
	"3": (c) => gen_single(c, 3),
	"4": (c) => gen_single(c, 4),
	"5": (c) => gen_single(c, 5),
	"6": (c) => gen_single(c, 6),
	"(1)": (c) => gen_single(c, 1),
	"(2)": (c) => gen_single(c, 2),
	"(3)": (c) => gen_single(c, 3),
	"(4)": (c) => gen_single(c, 4),
	"(5)": (c) => gen_single(c, 5),
	"(6)": (c) => gen_single(c, 6),
	"Any": (c) => gen_range(c, 1, 6),
	"1/2": (c) => gen_range(c, 1, 2),
	"1-3": (c) => gen_range(c, 1, 3),
	"1-4": (c) => gen_range(c, 1, 4),
	"1-5": (c) => gen_range(c, 1, 5),
	"2/3": (c) => gen_range(c, 2, 3),
	"2-4": (c) => gen_range(c, 2, 4),
	"2-5": (c) => gen_range(c, 2, 5),
	"2-6": (c) => gen_range(c, 2, 6),
	"3/4": (c) => gen_range(c, 3, 4),
	"3-5": (c) => gen_range(c, 3, 5),
	"3-6": (c) => gen_range(c, 3, 6),
	"4/5": (c) => gen_range(c, 4, 5),
	"4-6": (c) => gen_range(c, 4, 6),
	"5/6": (c) => gen_range(c, 5, 6),
	"(1/2)": (c) => gen_range(c, 1, 2),
	"(1-3)": (c) => gen_range(c, 1, 3),
	"(1-4)": (c) => gen_range(c, 1, 4),
	"(1-5)": (c) => gen_range(c, 1, 5),
	"(2/3)": (c) => gen_range(c, 2, 3),
	"(2-4)": (c) => gen_range(c, 2, 4),
	"(2-5)": (c) => gen_range(c, 2, 5),
	"(2-6)": (c) => gen_range(c, 2, 6),
	"(3/4)": (c) => gen_range(c, 3, 4),
	"(3-5)": (c) => gen_range(c, 3, 5),
	"(3-6)": (c) => gen_range(c, 3, 6),
	"(4/5)": (c) => gen_range(c, 4, 5),
	"(4-6)": (c) => gen_range(c, 4, 6),
	"(5/6)": (c) => gen_range(c, 5, 6),
}

const place_dice_take = {
	"Full House": take_full_house,
	"Straight 4/3": take_straight_4_or_3,
	"Straight 3": take_straight_3,
	"Straight 4": take_straight_4,
	"Doubles": take_doubles,
	"Triples": take_triples,
	"1": take_single,
	"2": take_single,
	"3": take_single,
	"4": take_single,
	"5": take_single,
	"6": take_single,
	"(1)": take_single,
	"(2)": take_single,
	"(3)": take_single,
	"(4)": take_single,
	"(5)": take_single,
	"(6)": take_single,
	"Any": (c, d) => take_single(c, d),
	"1/2": (c, d) => take_single(c, d),
	"1-3": (c, d) => take_single(c, d),
	"1-4": (c, d) => take_single(c, d),
	"1-5": (c, d) => take_single(c, d),
	"2/3": (c, d) => take_single(c, d),
	"2-4": (c, d) => take_single(c, d),
	"2-5": (c, d) => take_single(c, d),
	"2-6": (c, d) => take_single(c, d),
	"3/4": (c, d) => take_single(c, d),
	"3-5": (c, d) => take_single(c, d),
	"3-6": (c, d) => take_single(c, d),
	"4/5": (c, d) => take_single(c, d),
	"4-6": (c, d) => take_single(c, d),
	"5/6": (c, d) => take_single(c, d),
	"(1/2)": (c, d) => take_single(c, d),
	"(1-3)": (c, d) => take_single(c, d),
	"(1-4)": (c, d) => take_single(c, d),
	"(1-5)": (c, d) => take_single(c, d),
	"(2/3)": (c, d) => take_single(c, d),
	"(2-4)": (c, d) => take_single(c, d),
	"(2-5)": (c, d) => take_single(c, d),
	"(2-6)": (c, d) => take_single(c, d),
	"(3/4)": (c, d) => take_single(c, d),
	"(3-5)": (c, d) => take_single(c, d),
	"(3-6)": (c, d) => take_single(c, d),
	"(4/5)": (c, d) => take_single(c, d),
	"(4-6)": (c, d) => take_single(c, d),
	"(5/6)": (c, d) => take_single(c, d),
}

function can_place_dice(c) {

	let pattern = data.cards[c].dice
	if (!pattern)
		return false

	let pred = place_dice_check[pattern]
	if (!pred)
		throw Error("bad pattern definition: " + pattern)

	// At per card limit?
	if (place_dice_once[pattern]) {
		if (map_has(game.placed, c))
			return false
	}

	// At cube limit?
	if (data.cards[c].special) {
		// Max on card
		if (get_cubes(c) >= data.cards[c].special)
			return false

		// Max available
		let n_cubes = count_total_cubes()
		if (n_cubes >= 10)
			return false

		if (game.scenario === S12_TOWTON) {
			if (n_cubes >= 8)
				return false
		}
	}

	// At per wing limit?
	let wing = data.cards[c].wing
	let n_wing = 0
	for (let i = 0; i < game.placed.length; i += 2) {
		let x = game.placed[i]
		if (x !== c) {
			let i_wing = data.cards[x].wing
			if (i_wing === wing)
				n_wing ++
		}
	}
	if (n_wing >= game.place_max[wing])
		return false

	if (game.scenario === S8_BROOKLYN_HEIGHTS) {
		if (c === S8_CLINTON) {
			if (is_removed_from_play(S8_GRANT))
				return false
			if (is_removed_from_play(S8_HESSIANS))
				return false
		}
	}

	if (game.scenario === S3201_GAINES_MILL) {
		if (c === S3201_JACKSON) {
			if (!has_any_dice_on_card(S3201_DH_HILL))
				return false
		}
	}

	if (game.scenario === S29_GETTYS_2ND) {
		if (c === S29_MCLAWS) {
			if (!has_any_dice_on_card(S29_HOOD))
				return false
		}
		if (c === S29_ANDERSON) {
			if (!has_any_dice_on_card(S29_MCLAWS))
				return false
		}
		if (c === S29_EARLY || c === S29_JOHNSON) {
			let red = 0
			if (has_any_dice_on_card(S29_HOOD)) ++red
			if (has_any_dice_on_card(S29_MCLAWS)) ++red
			if (has_any_dice_on_card(S29_ANDERSON)) ++red
			if (red < 2)
				return false
		}
	}

	return pred(c)
}

function can_place_value(c, v) {
	let old_v = map_get(game.placed, c, 0)
	return old_v === 0 || old_v === v
}

function pool_has_single(v) {
	for (let i = 0; i < 6; ++i)
		if (is_pool_die(i, v))
			return true
	return false
}

function check_single_count(c, v, x) {
	if (!can_place_value(c, v))
		return false
	let n = 0
	for (let i = 0; i < 6; ++i)
		if (is_pool_die(i, v) && ++n >= x)
			return true
	return false
}

function check_single(c, v) {
	if (!can_place_value(c, v))
		return false
	for (let i = 0; i < 6; ++i)
		if (is_pool_die(i, v))
			return true
	return false
}

function check_range(c, lo, hi) {
	let old_v = map_get(game.placed, c, 0)
	if (old_v > 0)
		return pool_has_single(old_v)
	for (let i = 0; i < 6; ++i)
		if (is_pool_die_range(i, lo, hi))
			return true
	return false
}

function check_all_3(c, x, y, z) {
	if (!can_place_value(c, x))
		return false
	return pool_has_single(x) && pool_has_single(y) && pool_has_single(z)
}

function check_all_4(c, x, y, z, w) {
	if (!can_place_value(c, x))
		return false
	return pool_has_single(x) && pool_has_single(y) && pool_has_single(z) && pool_has_single(w)
}

function check_straight_4_or_3(c) {
	if (is_straight_4_or_3(c) === 4)
		return check_straight_4(c)
	else
		return check_straight_3(c)
}

function check_straight_3(c) {
	return (
		check_all_3(c, 1, 2, 3) ||
		check_all_3(c, 2, 3, 4) ||
		check_all_3(c, 3, 4, 5) ||
		check_all_3(c, 4, 5, 6)
	)
}

function check_straight_4(c) {
	return (
		check_all_4(c, 1, 2, 3, 4) ||
		check_all_4(c, 2, 3, 4, 5) ||
		check_all_4(c, 3, 4, 5, 6)
	)
}

function check_doubles(c) {
	return (
		check_single_count(c, 1, 2) ||
		check_single_count(c, 2, 2) ||
		check_single_count(c, 3, 2) ||
		check_single_count(c, 4, 2) ||
		check_single_count(c, 5, 2) ||
		check_single_count(c, 6, 2)
	)
}

function check_triples(c) {
	return (
		check_single_count(c, 1, 3) ||
		check_single_count(c, 2, 3) ||
		check_single_count(c, 3, 3) ||
		check_single_count(c, 4, 3) ||
		check_single_count(c, 5, 3) ||
		check_single_count(c, 6, 3)
	)
}

function check_full_house(c) {
	for (let x = 1; x <= 6; ++x) {
		for (let y = 1; y <= 6; ++y) {
			if (x !== y) {
				if (check_single_count(c, x, 3) && check_single_count(c, y, 2))
					return true
			}
		}
	}
	return false
}

function gen_pool_die(v) {
	let p = player_index()
	for (let i = 0; i < 6; ++i)
		if (get_dice_location(p * 6 + i) < 0 && get_dice_value(p * 6 + i) === v)
			gen_action_die(p * 6 + i)
}

function gen_single(c, v) {
	if (!can_place_value(c, v))
		return false
	gen_pool_die(v)
}

function gen_range(c, lo, hi) {
	for (let v = lo; v <= hi; ++v)
		gen_single(c, v)
}

function gen_straight_4_or_3(c) {
	if (is_straight_4_or_3(c) === 4)
		gen_straight_4(c)
	else
		gen_straight_3(c)
}

function gen_straight_3(c) {
	if (check_all_3(c, 1, 2, 3))
		gen_pool_die(1)
	if (check_all_3(c, 2, 3, 4))
		gen_pool_die(2)
	if (check_all_3(c, 3, 4, 5))
		gen_pool_die(3)
	if (check_all_3(c, 4, 5, 6))
		gen_pool_die(4)
}

function gen_straight_4(c) {
	if (check_all_4(c, 1, 2, 3, 4))
		gen_pool_die(1)
	if (check_all_4(c, 2, 3, 4, 5))
		gen_pool_die(2)
	if (check_all_4(c, 3, 4, 5, 6))
		gen_pool_die(3)
}

function gen_doubles(c) {
	for (let v = 1; v <= 6; ++v)
		if (check_single_count(c, v, 2))
			gen_pool_die(v)
}

function gen_triples(c) {
	for (let v = 1; v <= 6; ++v)
		if (check_single_count(c, v, 3))
			gen_pool_die(v)
}

function gen_full_house(c) {
	for (let x = 1; x <= 6; ++x) {
		for (let y = 1; y <= 6; ++y) {
			if (x !== y) {
				if (check_single_count(c, x, 3) && check_single_count(c, y, 2))
					gen_pool_die(x)
			}
		}
	}
}

function find_and_take_single(c, v) {
	let p = player_index()
	for (let i = 0; i < 6; ++i) {
		let d = p * 6 + i
		if (get_dice_location(d) < 0 && get_dice_value(d) === v) {
			set_dice_location(d, c)
			game.summary |= (1 << d)
			return
		}
	}
	throw new Error("cannot find die of value " + v)
}

function take_single(c, d) {
	set_dice_location(d, c)
	game.summary |= (1 << d)
	map_set(game.placed, c, get_dice_value(d))
}

function take_doubles(c, d) {
	let v = get_dice_value(d)
	take_single(c, d)
	find_and_take_single(c, v)
}

function take_triples(c, d) {
	let v = get_dice_value(d)
	take_single(c, d)
	find_and_take_single(c, v)
	find_and_take_single(c, v)
}

function take_full_house(c, d) {
	let x = get_dice_value(d)
	for (let y = 1; y <= 6; ++y) {
		if (x !== y) {
			if (check_single_count(c, x, 3) && check_single_count(c, y, 2)) {
				find_and_take_single(c, x)
				find_and_take_single(c, x)
				find_and_take_single(c, x)
				find_and_take_single(c, y)
				find_and_take_single(c, y)
			}
		}
	}
}

function take_straight_4_or_3(c, d) {
	if (is_straight_4_or_3(c) === 4)
		take_straight_4(c, d)
	else
		take_straight_3(c, d)
}

function take_straight_3(c, d) {
	let v = get_dice_value(d)
	take_single(c, d)
	find_and_take_single(c, v+1)
	find_and_take_single(c, v+2)
}

function take_straight_4(c, d) {
	let v = get_dice_value(d)
	take_single(c, d)
	find_and_take_single(c, v+1)
	find_and_take_single(c, v+2)
	find_and_take_single(c, v+3)
}

function goto_roll_phase() {
	game.selected = -1
	game.attack_target = -1
	game.target = -1
	game.target2 = -1
	game.action = 0
	game.state = "roll"

	game.place_max = [ 1, 1, 1, 1 ]

	let p = player_index()
	for (let c of game.front[p]) {
		if (card_has_rule(c, "place_2_blue"))
			game.place_max[BLUE] = 2
		if (card_has_rule(c, "place_2_red"))
			game.place_max[RED] = 2
		if (card_has_rule(c, "place_2_red_if_dice") && has_any_dice_on_card(c))
			game.place_max[RED] = 2
		if (card_has_rule(c, "place_2_dkblue"))
			game.place_max[DKBLUE] = 2

		/*
		// NOT USED (YET)
		if (card_has_rule(c, "place_2_pink")) game.place_max[PINK] = 2
		if (card_has_rule(c, "place_2_blue_if_dice") && has_any_dice_on_card(c)) game.place_max[BLUE] = 2
		if (card_has_rule(c, "place_2_dkblue_if_dice") && has_any_dice_on_card(c)) game.place_max[DKBLUE] = 2
		if (card_has_rule(c, "place_2_pink_if_dice") && has_any_dice_on_card(c)) game.place_max[PINK] = 2
		*/
	}

	if (game.scenario === S38_FLEURUS) {
		if (p === 1) {
			if (get_sticks(S38_RETREAT_TO_NIVELLES)) {
				if (game.starting_on_your_next_turn++) {
					set_sticks(S38_RETREAT_TO_NIVELLES, get_sticks(S38_RETREAT_TO_NIVELLES) - 1)
					game.lost[0] += 1
					if (check_victory())
						return
				}
			}
		}
	}
}

states.skip_action = {
	inactive: "roll",
	prompt() {
		view.prompt = "Skipped action phase; roll the dice in your pool."

		if (can_shift())
			view.actions.shift = 1

		if (count_dice_in_pool() > 0) {
			view.actions.roll = 1
			view.actions.end_turn = 0
		} else {
			view.actions.roll = 0
			view.actions.end_turn = 1
		}
	},
	shift() {
		push_undo()
		game.state = "shift_from"
	},
	roll() {
		clear_undo()
		goto_roll_phase()
		roll_dice_in_pool()
	},
	end_turn() {
		goto_roll_phase()
		roll_dice_in_pool()
		end_roll_phase()
	},
}

states.roll = {
	inactive: "roll",
	prompt() {
		view.prompt = "Roll the dice in your pool."
		view.actions.roll = 1
		view.actions.end_turn = 0
	},
	roll() {
		clear_undo()
		roll_dice_in_pool()
	},
}

function roll_dice_in_pool() {
	let rolled = []

	if (game.reacted === player_index())
		game.reacted = -1

	let p = player_index()
	for (let i = 0; i < 6; ++i) {
		if (get_dice_location(p * 6 + i) < 0) {
			let v = random(6) + 1
			set_dice_value(p * 6 + i, v)
			rolled.push(v)
		}
	}

	log("Roll\n" + rolled.map(d => "D" + d).join(" "))

	game.rolled = rolled.length
	game.summary = 0
	game.state = "place"
}

function gen_place_dice_select_card() {
	let p = player_index()
	for (let c of game.front[p]) {
		if (c === game.selected)
			continue
		if (can_place_dice(c))
			gen_action_card(c)
	}
}

states.place = {
	inactive: "place dice",
	prompt() {
		view.prompt = "Place dice on your formations."
		gen_place_dice_select_card()
		view.actions.end_turn = 1
	},
	card(c) {
		push_undo()
		game.selected = c
		game.state = "place_on_card"
	},
	end_turn() {
		end_roll_phase()
	},
}

states.place_on_card = {
	inactive: "place dice",
	prompt() {
		let card = data.cards[game.selected]
		view.prompt = "Place dice on " + card.name + "."

		gen_place_dice_select_card()

		place_dice_gen[card.dice](game.selected)

		view.actions.end_turn = 1
	},
	card(c) {
		if (c === game.selected) {
			game.selected = -1
			game.state = "place"
		} else {
			game.selected = c
			game.state = "place_on_card"
		}
	},
	die(d) {
		push_undo()
		place_dice_take[data.cards[game.selected].dice](game.selected, d)
		if (!can_place_dice(game.selected)) {
			game.selected = -1
			game.state = "place"
		}
	},
	end_turn() {
		end_roll_phase()
	},
}

function end_roll_phase() {
	push_undo()

	game.selected = -1

	if (game.placed.length > 0) {
		for (let i = 0; i < game.placed.length; i += 2) {
			let c = game.placed[i]
			let s = []
			for (let d = 0; d < 12; ++d)
				if (game.summary & (1 << d))
					if (get_dice_location(d) === c)
						s.push("D" + get_dice_value(d))
			log("C" + c + "\n" + s.join(" "))
		}
	}

	// Remove placed dice to add cube on special cards.
	for (let c of game.front[player_index()]) {
		let s = data.cards[c].special
		if (s && has_any_dice_on_card(c)) {
			map_set(game.cubes, c, Math.min(s, get_cubes(c) + 1))
			remove_dice(c)
		}
	}

	// Blank out unused dice.
	let p = player_index()
	for (let i = 0; i < 6; ++i)
		if (get_dice_location(p * 6 + i) < 0)
			set_dice_value(p * 6 + i, 0)

	if (game.scenario === S26_PEACH_ORCHARD) {
		if (player_index() === 0) {
			if (is_card_in_play(S26_FATAL_BLUNDER)) {
				if (!placed_any_dice_on_wing(PINK)) {
					game.state = "s26_fatal_blunder"
					return
				}
			}
		}
	}

	if (game.scenario === S28_CULPS_HILL) {
		if (get_cubes(S28_GEARY) === 5)
			return goto_game_over(P1, "Geary's Division arrived!")
	}

	if (game.scenario === S41_BLENHEIM_SCENARIO) {
		if (player_index() === 1) {
			if (is_card_in_play(S41_CLERAMBAULT)) {
				if (!placed_any_dice_on_wing(DKBLUE)) {
					game.state = "s41_clerambault"
					return
				}
			}
		}
	}

	if (game.scenario === S47_PRAGUE) {
		if (player_index() === 0) {
			if (is_card_in_play(S47_SCHWERIN) && !placed_any_dice()) {
				game.target2 = S47_SCHWERIN
				game.state = "s47_browne_and_schwerin"
				return
			}
		}
		if (player_index() === 1) {
			if (is_card_in_play(S47_BROWNE) && !placed_any_dice()) {
				game.target2 = S47_BROWNE
				game.state = "s47_browne_and_schwerin"
				return
			}
		}
	}

	if (game.scenario === S48_BRESLAU) {
		if (player_index() === 0) {
			if (is_card_in_play(S48_AUSTRIAN_GUNS)) {
				if (get_cubes(S48_AUSTRIAN_GUNS) === 0 && get_cubes(S48_PRUSSIAN_GUNS) === 2) {
					game.target2 = S48_AUSTRIAN_GUNS
					game.state = "s48_artillery_duel"
					return
				}
			}
		}
		if (player_index() === 1) {
			if (is_card_in_play(S48_PRUSSIAN_GUNS)) {
				if (get_cubes(S48_PRUSSIAN_GUNS) === 0 && get_cubes(S48_AUSTRIAN_GUNS) === 2) {
					game.target2 = S48_PRUSSIAN_GUNS
					game.state = "s48_artillery_duel"
					return
				}
			}
		}
	}

	end_turn()
}

states.s26_fatal_blunder = {
	inactive: "remove cards",
	prompt() {
		view.prompt = "Fatal Blunder!"
		if (is_card_in_play(S26_FATAL_BLUNDER)) {
			gen_action_card(S26_FATAL_BLUNDER)
		} else {
			let done = true
			for (let c of game.front[0]) {
				if (data.cards[c].wing === PINK) {
					gen_action_card(c)
					done = false
				}
			}
			if (done)
				view.actions.end_turn = 1
		}
	},
	card(c) {
		if (c === S26_FATAL_BLUNDER) {
			log("Fatal Blunder!")
			remove_card(S26_FATAL_BLUNDER)
			game.morale[0] ++
		} else {
			rout_card(c)
			game.morale[0] --
			game.morale[1] ++
		}
	},
	end_turn() {
		if (check_victory())
			return
		end_turn()
	}
}

states.s41_clerambault = {
	inactive: "rout cards",
	prompt() {
		view.prompt = "Rout Clerambault and add his sticks to Blenheim!"
		if (is_card_in_play(S41_BLENHEIM_CARD) && get_sticks(S41_CLERAMBAULT)) {
			view.selected = S41_CLERAMBAULT
			gen_action_card(S41_BLENHEIM_CARD)
		} else {
			gen_action_card(S41_CLERAMBAULT)
		}
	},
	card(c) {
		if (c === S41_BLENHEIM_CARD)
			move_all_sticks(S41_CLERAMBAULT, S41_BLENHEIM_CARD)
		else
			set_sticks(S41_CLERAMBAULT, 0)
		if (c === S41_CLERAMBAULT) {
			rout_card(S41_CLERAMBAULT)
			game.morale[0]++
			game.morale[1]--
			end_turn()
		}
	},
}

states.s47_browne_and_schwerin = {
	inactive: "remove cards",
	prompt() {
		view.prompt = "Remove " + card_name(game.target2) + "."
		gen_action_card(game.target2)
	},
	card(_) {
		remove_card(game.target2)
		game.target2 = -1
		end_turn()
	},
}

states.s48_artillery_duel = {
	inactive: "remove cards",
	prompt() {
		view.prompt = "Artillery Duel: Remove " + card_name(game.target2) + "."
		gen_action_card(game.target2)
	},
	card(_) {
		remove_card(game.target2)
		game.target2 = -1
		end_turn()
	},
}

function end_turn() {
	clear_undo()

	map_clear(game.placed)
	game.place_max = null

	set_opponent_active()
	goto_start_turn()
}

// === ACTION PHASE ===

function side_get_wild_die_card(p) {
	if (game.scenario === S11_MORTIMERS_CROSS || game.scenario === S12_TOWTON || game.scenario === S16_STOKE_FIELD) {
		for (let c of game.front[p])
			if (card_has_rule(c, "wild") && has_any_dice_on_card(c))
				return c
	}
	return -1
}

function has_any_red_normal_cards() {
	for (let c of game.front[0])
		if (data.cards[c].wing === RED && !data.cards[c].special)
			return true
	for (let c of game.front[1])
		if (data.cards[c].wing === RED && !data.cards[c].special)
			return true
}

function side_has_wild_die(p) {
	return side_get_wild_die_card(p) >= 0
}

function has_any_dice_on_card(c) {
	for (let i = 0; i < 12; ++i)
		if (get_dice_location(i) === c)
			return true
	return false
}

function has_any_cubes_on_card(c) {
	return get_cubes(c) >= 1
}

function count_dice_in_pool() {
	let n = 0
	let p = player_index()
	for (let i = 0; i < 6; ++i)
		if (get_dice_location(p * 6 + i) < 0)
			++n
	return n
}

function count_dice_on_card(c) {
	let n = 0
	for (let i = 0; i < 12; ++i)
		if (get_dice_location(i) === c)
			++n
	return n
}

function count_dice_on_card_with_value(c, v) {
	let n = 0
	for (let i = 0; i < 12; ++i)
		if (get_dice_location(i) === c && get_dice_value(i) === v)
			++n
	return n
}

function require_pair(c) {
	for (let v = 1; v <= 6; ++v)
		if (count_dice_on_card_with_value(c, v) >= 2)
			return true
	return false
}

function require_triplet(c) {
	for (let v = 1; v <= 6; ++v)
		if (count_dice_on_card_with_value(c, v) >= 3)
			return true
	return false
}

function require_full_house(c) {
	let n3 = 0
	let n2 = 0
	for (let v = 1; v <= 6; ++v) {
		let n = count_dice_on_card_with_value(c, v)
		if (n >= 3)
			++n3
		else if (n >= 2)
			++n2
	}
	return (n3 >= 2) || (n3 >= 1 && n2 >= 1)
}

function require_two_pairs(c) {
	let n = 0
	for (let v = 1; v <= 6; ++v)
		if (count_dice_on_card_with_value(c, v) >= 2)
			++n
	return n >= 2
}

function check_cube_requirement(c, req) {
	switch (req) {
		case "3 cubes":
		case "Three Cubes":
			return get_cubes(c) >= 3
		case "Two Cubes":
			return get_cubes(c) >= 2
		case "One Cube*":
		case "Voluntary":
		case undefined:
			return get_cubes(c) >= 1
		default:
			throw new Error("invalid action requirement: " + req)
	}
}

function check_dice_requirement(c, req, wild) {
	switch (req) {
		case "Three Dice":
			return count_dice_on_card(c) >= 3
		case "Full House":
			return require_full_house(c)
		case "Pair":
		case "Pair, Voluntary":
			// NOTE: Only requirement needed for Wild die scenarios.
			if (wild)
				return has_any_dice_on_card(c)
			return require_pair(c)
		case "Triplet":
			return require_triplet(c)
		case "Two Pairs":
			return require_two_pairs(c)
		case "Voluntary":
		case undefined:
			return has_any_dice_on_card(c)
		default:
			throw new Error("invalid action requirement: " + req)
	}
}

function card_has_any_actions(c) {
	for (let a of data.cards[c].actions)
		if (is_action(c, a))
			return true
	return false
}

function is_action(c, a) {
	return (a.type === "Bombard" || a.type === "Attack" || a.type === "Command")
}

function is_reaction(c, a) {
	return (a.type === "Screen" || a.type === "Counterattack" || a.type === "Absorb")
}

function is_mandatory_reaction(c, a) {

	if (game.scenario === S44_HOHENFRIEDBERG) {
		if (c === S44_DU_MOULIN && game.selected === S44_SAXON_HORSE)
			return false
	}

	return (
		a.requirement !== "Voluntary" &&
		a.requirement !== "Pair, Voluntary"
	)
}

function can_take_action(c, a, ix) {
	if (a.type === "Attack") {
		if (find_target_of_attack(c, a) < 0)
			return false
	}

	if (a.type === "Command") {
		if (find_first_target_of_command(c, a) < 0)
			return false
	}

	if (a.type === "Bombard") {
		// cannot Bombard last morale cube
		let p = player_index()
		if (game.morale[1-p] === 1)
			return false
	}

	if (game.scenario === S8_BROOKLYN_HEIGHTS) {
		if (c === S8_CLINTON) {
			// Clinton - may only attack if both Grant and Hessians have dice on them.
			if (!has_any_dice_on_card(S8_GRANT) || !has_any_dice_on_card(S8_HESSIANS))
				return false
		}
	}

	if (game.scenario === S3201_GAINES_MILL) {
		if (c === S3201_JACKSON) {
			// Jackson - may only attack if D.H. Hill and one other formation have dice
			if (!has_any_dice_on_card(S3201_DH_HILL))
				return false
			if (!has_any_dice_on_card(S3201_AP_HILL) && !has_any_dice_on_card(S3201_LONGSTREET))
				return false
		}
	}

	if (game.scenario === S35_AULDEARN) {
		if (c === S35_MONTROSE) {
			// May only perform the second action after having previously performed the first.
			if (ix === 1) {
				if (is_card_in_reserve(S35_GORDON))
					return false
			}
		}
	}

	if (game.scenario === S45_SOOR) {
		if (c === S45_AUSTRIAN_GUNS) {
			// May only attack Buddenbrock if Cuirassiers still in play
			if (ix === 0) {
				if (is_removed_from_play(S45_CUIRASSIERS))
					return false
			}
		}
	}

	if (game.scenario === S49_LEUTHEN) {
		if (c === S49_POOR_CHARLES) {
			// Cannot command if dice on Feint
			if (has_any_dice_on_card(S49_FEINT))
				return false
		}
	}

	if (a.type === "Bombard" || a.type === "Attack" || a.type === "Command") {
		if (data.cards[c].special > 0)
			return check_cube_requirement(c, a.requirement)
		else
			return check_dice_requirement(c, a.requirement, false)
	}
	return false
}

function s40_can_take_cassines_action(c, a, b) {
	return (get_sticks(c) < 3) && (is_card_in_play(a) || is_card_in_play(b))
}

function can_take_any_action() {
	let p = player_index()
	for (let c of game.front[p]) {
		if (card_has_any_actions(c)) {
			if (has_any_dice_on_card(c))
				return true
			if (has_any_cubes_on_card(c)) // TODO: check requirements!
				return true
		}
	}

	if (game.scenario === S40_CHIARI) {
		if (player_index() === 1) {
			if (s40_can_take_cassines_action(S40_CASSINES_I, S40_NIGRELLI, S40_KRIECHBAUM))
				return true
			if (s40_can_take_cassines_action(S40_CASSINES_II, S40_MANNSFELDT, S40_GUTTENSTEIN))
				return true
		}
	}

	if (game.scenario === S41_BLENHEIM_SCENARIO) {
		if (player_index() === 0) {
			if (has_any_dice_on_card(S41_PRINCE_EUGENE) && has_any_red_normal_cards())
				return true
		}
	}

	if (game.scenario === S42_RAMILLIES) {
		if (player_index() === 0) {
			if (has_any_dice_on_card(S42_MARLBOROUGH))
				return true
		}
	}

	if (can_shift())
		return true

	return false
}

function count_cards_remaining_from_wing(w) {
	let n = 0
	for (let c of game.front[0])
		if (data.cards[c].wing === w)
			++n
	for (let c of game.front[1])
		if (data.cards[c].wing === w)
			++n
	for (let c of game.reserve[0])
		if (data.cards[c].wing === w)
			++n
	for (let c of game.reserve[1])
		if (data.cards[c].wing === w)
			++n
	return n
}

function goto_start_turn() {
	let p = player_index()

	log(".p" + (p + 1))

	if (check_impossible_to_attack_victory())
		return

	// TODO: manual step to shift?
	if (game.shift) {
		for (let c of game.front[p]) {
			let n = get_shift_sticks(c)
			if (n > 0) {
				set_sticks(c, get_sticks(c) + n)
				set_shift_sticks(c, 0)
			}
		}
	}

	if (game.scenario === S25_WHEATFIELD) {
		// Rout Stony Hill at start of Union turn if it is the only Blue card left.
		if (p === 1) {
			if (is_card_in_play(S25_STONY_HILL)) {
				if (count_cards_remaining_from_wing(BLUE) === 1) {
					game.state = "s25_stony_hill"
					return
				}
			}
		}
	}

	if (game.scenario === S44_HOHENFRIEDBERG) {
		if (p === 1) {
			let have_inf_or_cav = false
			for (let c of game.front[1])
				if (is_infantry(c) || is_cavalry(c))
					have_inf_or_cav = true
			if (!have_inf_or_cav)
				return goto_game_over(P1, "Frederick had no Infantry or Cavalry in play!")
		}
	}

	goto_action_phase()
}

states.s25_stony_hill = {
	inactive: "rout cards",
	prompt() {
		view.prompt = "Rout Stony Hill!"
		gen_action_card(S25_STONY_HILL)
	},
	card(_) {
		rout_card(S25_STONY_HILL)
		game.morale[0] ++
		game.morale[1] --
		if (check_victory())
			return
		goto_action_phase()
	},
}

function goto_action_phase() {
	if (game.reacted === player_index()) {
		game.state = "skip_action"
	} else {
		if (can_take_any_action())
			game.state = "action"
		else
			end_action_phase()
	}
}

function end_action_phase() {
	game.hits = 0
	game.self = 0
	game.hits2 = 0
	game.self2 = 0
	game.selected = -1
	game.target = -1
	game.target2 = -1
	goto_routing()
}

states.action = {
	inactive: "take an action",
	prompt() {
		view.prompt = "Take an action."

		if (count_dice_in_pool() > 0) {
			view.actions.roll = 1
			view.actions.end_turn = 0
		} else {
			view.actions.roll = 0
			view.actions.end_turn = 1
		}

		let p = player_index()
		for (let c of game.front[p]) {
			let has_dice = has_any_dice_on_card(c)
			let has_cube = has_any_cubes_on_card(c)
			if (has_dice || has_cube) {
				if (data.cards[c].actions.length >= 1) {
					if (is_action(c, data.cards[c].actions[0])) {
						if (can_take_action(c, data.cards[c].actions[0], 0))
							gen_action_action1(c)
						else if (has_dice)
							gen_action_null1(c)
					}
				}
				if (data.cards[c].actions.length >= 2) {
					if (is_action(c, data.cards[c].actions[1])) {
						if (can_take_action(c, data.cards[c].actions[1], 1))
							gen_action_action2(c)
						else if (has_dice)
							gen_action_null2(c)
					}
				}
				if (data.cards[c].retire)
					gen_action_retire(c)
			}
		}

		if (can_shift())
			view.actions.shift = 1

		if (game.scenario === S40_CHIARI) {
			if (player_index() === 1) {
				if (s40_can_take_cassines_action(S40_CASSINES_I, S40_NIGRELLI, S40_KRIECHBAUM))
					gen_action_card(S40_CASSINES_I)
				if (s40_can_take_cassines_action(S40_CASSINES_II, S40_MANNSFELDT, S40_GUTTENSTEIN))
					gen_action_card(S40_CASSINES_II)
			}
		}

		if (game.scenario === S41_BLENHEIM_SCENARIO) {
			if (player_index() === 0) {
				if (has_any_dice_on_card(S41_PRINCE_EUGENE) && has_any_red_normal_cards())
					gen_action_card(S41_PRINCE_EUGENE)
			}
		}

		if (game.scenario === S42_RAMILLIES) {
			if (player_index() === 0) {
				if (has_any_dice_on_card(S42_MARLBOROUGH))
					gen_action_card(S42_MARLBOROUGH)
			}
		}

	},
	retire(c) {
		push_undo()

		log("Retire\nC" + c)

		if (game.scenario === S38_FLEURUS) {
			if (c === S38_LUXEMBOURGS_HORSE) {
				if (is_card_in_play(S38_GOURNAYS_HORSE))
					move_all_sticks(S38_LUXEMBOURGS_HORSE, S38_GOURNAYS_HORSE)
			}
		}

		retire_card(c)
		end_action_phase()
	},
	a1(c) {
		push_undo()
		goto_take_action(c, 0)
	},
	a2(c) {
		push_undo()
		goto_take_action(c, 1)
	},
	n1(c) {
		push_undo()
		goto_null(c)
	},
	n2(c) {
		push_undo()
		goto_null(c)
	},
	roll() {
		clear_undo()
		goto_roll_phase()
		roll_dice_in_pool()
	},
	shift() {
		push_undo()
		game.state = "shift_from"
	},
	card(c) {
		push_undo()

		game.selected = c

		if (game.scenario === S40_CHIARI) {
			game.state = "s40_cassines"
			return
		}

		if (game.scenario === S41_BLENHEIM_SCENARIO) {
			game.target2 = -1
			game.self = Math.min(get_sticks(S41_PRINCE_EUGENE), count_dice_on_card(S41_PRINCE_EUGENE))
			game.state = "s41_prince_eugene"
			game.prince_eugene = 0
			return
		}

		if (game.scenario === S42_RAMILLIES) {
			game.state = "s42_marlborough"
			return
		}

		throw new Error("missing rule for special action: " + card_name(c))
	},
	end_turn() {
		goto_roll_phase()
		roll_dice_in_pool()
		end_roll_phase()
	},
}

function can_shift() {
	if (!game.shift)
		return false

	if (game.scenario === S49_LEUTHEN) {
		if (player_index() === 1) {
			if (has_any_dice_on_card(S49_FEINT))
				return false
		}
	}

	return can_shift_any_infantry() || can_shift_any_cavalry()
}

function can_shift_any_infantry() {
	let n = 0, m = 0
	for (let c of game.front[player_index()]) {
		if (is_infantry(c)) {
			if (get_sticks(c) > 1)
				++m

			if (game.scenario === S46_ROCOUX) {
				// cannot shift to Austrians
				if (c === S46_AUSTRIANS)
					continue
			}

			++n
		}
	}
	return n > 1 && m > 0
}

function can_shift_any_cavalry() {
	let n = 0, m = 0
	for (let c of game.front[player_index()]) {
		if (is_cavalry(c)) {
			if (get_sticks(c) > 1)
				++m
			++n
		}
	}
	return n > 1 && m > 0
}

states.shift_from = {
	inactive: "shift sticks",
	prompt() {
		view.prompt = "Shift sticks from one Formation to another."
		let p = player_index()
		if (can_shift_any_infantry())
			for (let c of game.front[p])
				if (is_infantry(c) && get_sticks(c) > 1)
					gen_action_card(c)
		if (can_shift_any_cavalry())
			for (let c of game.front[p])
				if (is_cavalry(c) && get_sticks(c) > 1)
					gen_action_card(c)
	},
	card(c) {
		game.selected = c
		game.target2 = -1
		game.state = "shift_to"
	},
}

states.shift_to = {
	inactive: "shift sticks",
	prompt() {
		view.prompt = "Shift sticks from " + card_name(game.selected) + "."
		let p = player_index()
		if (game.target2 < 0) {
			if (is_infantry(game.selected))
				for (let c of game.front[p])
					if (c !== game.selected && is_infantry(c) && c !== S46_AUSTRIANS)
						gen_action_card(c)
			if (is_cavalry(game.selected))
				for (let c of game.front[p])
					if (c !== game.selected && is_cavalry(c))
						gen_action_card(c)
		} else {
			gen_action_card(game.target2)
			view.actions.next = 1
		}
	},
	card(c) {
		game.target2 = c

		set_sticks(game.selected, get_sticks(game.selected) - 1)
		set_shift_sticks(game.target2, get_shift_sticks(game.target2) + 1)

		if (get_sticks(game.selected) === 1)
			this.next()
	},
	next() {
		// TODO: skip action phase?

		log("Shift\nC" + game.selected + "\nC" + game.target2 + "\n" + get_shift_sticks(game.target2) + " sticks.")

		end_action_phase()
	},
}

states.s40_cassines = {
	inactive: "move sticks",
	prompt() {
		view.prompt = "Cassines: Move one unit stick to this card."
		if (game.selected === S40_CASSINES_I) {
			if (is_card_in_play(S40_NIGRELLI))
				gen_action_card(S40_NIGRELLI)
			if (is_card_in_play(S40_KRIECHBAUM))
				gen_action_card(S40_KRIECHBAUM)
		}
		if (game.selected === S40_CASSINES_II) {
			if (is_card_in_play(S40_MANNSFELDT))
				gen_action_card(S40_MANNSFELDT)
			if (is_card_in_play(S40_GUTTENSTEIN))
				gen_action_card(S40_GUTTENSTEIN)
		}
	},
	card(c) {
		log("Move\nC" + c + "\nC" + game.selected + "\n1 stick.")
		move_sticks(c, game.selected, 1)
		end_action_phase()
	},
}

states.s41_prince_eugene = {
	inactive: "move sticks",
	prompt() {
		if (game.target2 < 0) {
			view.prompt = `Prince Eugene: Move up to ${game.self} unit sticks to any Red card.`
			for (let c of game.front[0])
				if (data.cards[c].wing === RED && !data.cards[c].special)
					gen_action_card(c)
		} else {
			view.prompt = `Prince Eugene: Move up to ${game.self} unit sticks to ${card_name(game.target2)}.`
			gen_action_card(game.target2)
			view.actions.next = 1
		}
	},
	card(c) {
		game.target2 = c
		move_sticks(game.selected, game.target2, 1)
		++game.prince_eugene
		if (--game.self === 0)
			this.next()
	},
	next() {
		log("Move\nC" + game.selected + "\nC" + game.target2 + "\n" + game.prince_eugene + " sticks.")
		delete game.prince_eugene
		pay_for_action(game.selected)
		end_action_phase()
	},
}

states.s42_marlborough = {
	inactive: "move sticks",
	prompt() {
		view.prompt = "Marlborough: Move 4 sticks to any red or pink card except the Dutch Guards."
		for (let c of game.front[0])
			if (c !== S42_MARLBOROUGH && c !== S42_DUTCH_GUARDS)
				gen_action_card(c)
	},
	card(c) {
		log("Move\nC" + game.selected + "\nC" + c + "\n4 sticks.")

		move_sticks(S42_MARLBOROUGH, c, 4)
		pay_for_action(S42_MARLBOROUGH)
		if (get_sticks(S42_MARLBOROUGH) === 0)
			remove_card(S42_MARLBOROUGH)
		end_action_phase()
	},
}

function goto_null(c) {
	log("Null\nC" + c)

	pay_for_action(c)
	end_action_phase()
}

function goto_take_action(c, ix) {
	let a = data.cards[c].actions[ix]
	game.selected = c
	game.action = ix
	switch (a.type) {
		case "Attack":
			if (a.choice)
				goto_attack_choose_target()
			else
				goto_attack(find_target_of_attack(c, a))
			break
		case "Bombard":
			game.state = "bombard"
			break
		case "Command":
			goto_command()
			break
	}
}

function current_action() {
	return data.cards[game.selected].actions[game.action]
}

function find_target_of_attack(c, a) {
	if (card_has_rule(c, "attack_reserve")) {
		for (let t of a.target_list)
			if (is_card_in_play_or_reserve(t))
				return t
		return -1
	}

	if (card_has_rule(c, "ignore_reserve")) {
		for (let t of a.target_list)
			if (is_card_in_play(t))
				return t
		return -1
	}

	for (let t of a.target_list) {
		if (is_card_in_play(t))
			return t
		if (is_card_in_reserve(t))
			return -1
	}
	return -1
}

function find_target_of_counterattack(a) {
	for (let t of a.target_list) {
		if (set_has(game.front[0], t))
			return t
		if (set_has(game.front[1], t))
			return t
	}
	return -1
}

function find_target_of_absorb(a) {
	for (let t of a.target_list) {
		if (set_has(game.front[0], t))
			return t
		if (set_has(game.front[1], t))
			return t
	}
	return -1
}

function find_first_target_of_command(c, a) {

	if (game.scenario === S37_INKERMAN) {
		if (c === S37_THE_FOG)
			return S37_THE_FOG
	}

	if (game.scenario === S38_FLEURUS) {
		if (c === S38_WALDECK)
			return S38_RETREAT_TO_NIVELLES
	}

	if (game.scenario === S44_HOHENFRIEDBERG) {
		if (c === S44_CHARLES) {
			if (game.reserve[1].length > 0)
				return game.reserve[1]
			return -1
		}
	}

	if (!a.target_list)
		throw new Error("no rule for Command target: " + a.target)

	for (let t of a.target_list) {
		if (is_card_in_reserve(t))
			return t
	}

	return -1
}

function find_all_targets_of_command(c, a) {

	if (game.scenario === S44_HOHENFRIEDBERG) {
		if (c === S44_FREDERICK_II) {
			for (let t of a.target_list)
				if (is_card_in_reserve(t))
					return [ t ]
		}
	}

	let list = []
	for (let t of a.target_list) {
		if (is_card_in_reserve(t))
			list.push(t)
	}
	return list
}

states.bombard = {
	inactive: "bombard",
	prompt() {
		view.prompt = "Bombard."
		view.actions.bombard = 1
	},
	bombard() {
		log("Bombard\nC" + game.selected)

		let opp = 1 - player_index()
		game.morale[opp] --
		pay_for_action(game.selected)
		end_action_phase()
	},
}

function goto_attack_choose_target() {
	let a = current_action()
	let candidates = []
	for (let c of a.target_list) {
		if (set_has(game.front[0], c))
			candidates.push(c)
		if (set_has(game.front[1], c))
			candidates.push(c)
	}
	if (candidates.length > 1)
		game.state = "attack_choose_target"
	else
		goto_attack(candidates[0])
}

states.attack_choose_target = {
	inactive: "attack",
	prompt() {
		view.prompt = "Choose the target of your attack."
		let a = current_action()
		for (let c of a.target_list) {
			if (set_has(game.front[0], c))
				gen_action_card(c)
			if (set_has(game.front[1], c))
				gen_action_card(c)
		}
	},
	card(c) {
		goto_attack(c)
	},
}

function goto_attack(target) {
	let take_from = card_has_rule(game.selected, "take_from")
	if (take_from) {
		for (let from of take_from)
			if (has_any_dice_on_card(from))
				take_all_dice(from, game.selected)
	}

	let take_1_from = card_has_rule(game.selected, "take_1_from")
	if (take_1_from) {
		for (let from of take_1_from)
			if (has_any_dice_on_card(from))
				take_one_die(from, game.selected)
	}

	game.state = "attack"
	game.attack_target = target
	game.target = target

	update_attack1()
	update_attack2()
}

// Update hits and self hits.
function update_attack1(is_absorb = false) {
	let a = current_action()

	let n = count_dice_on_card(game.selected)
	let target = game.attack_target

	switch (a.effect) {
		default:
			throw new Error("invalid attack effect: " + a.effect)

		case "1 hit per die.":
			game.self = 0
			game.hits = n
			break

		case "1 hit per die. 1 self.":
			game.self = 1
			game.hits = n
			break

		case "1 hit per die. 1 self. If reduced to one stick, no self hits.":
			if (get_sticks(game.selected) === 1)
				game.self = 0
			else
				game.self = 1
			game.hits = n
			break

		case "1 hit per pair.":
			game.self = 0
			game.hits = n / 2
			break

		case "1 hit per pair. 1 self.":
			game.self = 1
			game.hits = n / 2
			break

		case "1 hit plus 1 hit per die.":
			game.self = 0
			game.hits = 1 + n
			break
		case "1 hit plus 1 hit per die. 1 self.":
			game.self = 1
			game.hits = 1 + n
			break

		case "1 hit. 1 self.":
			game.self = 1
			game.hits = 1
			break

		case "2 hits per die.":
			game.self = 0
			game.hits = 2 * n
			break

		case "2 hits plus 1 hit per die. 1 self.":
			game.self = 1
			game.hits = 2 + n
			break

		case "1 hit.":
			game.self = 0
			game.hits = 1
			break

		case "2 hits.":
			game.self = 0
			game.hits = 2
			break

		case "5 hits.":
			game.self = 0
			game.hits = 5
			break

		case "1 hit per die (2 hits per die versus Blenheim). 1 self.":
			game.self = 1
			game.hits = n
			if (target === S41_BLENHEIM_CARD)
				game.hits = 2 * n
			break

		case "1 hit per die (2 hits per die versus Villars's Left). 1 self.":
			game.self = 1
			game.hits = n
			if (target === S43_VILLARS_LEFT)
				game.hits = 2 * n
			break

		case "1 hit per die versus Driesen. 2 hits per die versus Retzow.":
			game.self = 0
			if (target === S49_RETZOW)
				game.hits = 2 * n
			else
				game.hits = n
			break

		case "1 hit per die. 1 extra hit if Fourth Line is in play.":
			game.self = 0
			game.hits = n
			if (is_card_in_play(S36_FOURTH_LINE))
				game.hits += 1
			break

		case "1 hit per die. 1 self. 1 extra hit if Dutch Horse routed.":
			game.self = 1
			game.hits = n
			if (is_removed_from_play(S38_DUTCH_HORSE))
				game.hits += 1
			break

		case "1 hit per die. 1 self. 1 extra vs Dutch Left Foot.":
			game.self = 1
			game.hits = n
			if (target === S38_DUTCH_LEFT_FOOT)
				game.hits += 1
			break

		case "1 hit per die. 1 self. 1 extra vs Essex.":
			game.self = 1
			game.hits = n
			if (target === S30_ESSEX)
				game.hits += 1
			break

		case "1 hit per die. 1 self. 1 extra vs Tullibardine.":
			game.self = 1
			game.hits = n
			if (target === S34_TULLIBARDINE)
				game.hits += 1
			break

		case "Oxford immediately routs. This attack cannot be screened.":
			game.self = 0
			game.hits = 8
			break
	}

	if (game.scenario === S2_MARSTON_MOOR) {
		if (is_card_in_play(S2_RUPERTS_LIFEGUARD)) {
			if (target === S2_TILLIERS_LEFT)
				game.hits -= 1
			if (target === S2_TILLIERS_RIGHT)
				game.hits -= 1
		}
	}

	if (game.scenario === S9_ST_ALBANS) {
		// Defensive Works (negated by Archers)
		if (target === S9_SHROPSHIRE_LANE || target === S9_SOPWELL_LANE) {
			if (is_card_in_play(S9_HENRY_VI))
				if (!has_any_cubes_on_card(S9_ARCHERS))
					game.hits = Math.min(1, game.hits)
		}
	}

	if (game.scenario === S15_TEWKESBURY) {
		if (target === S15_SOMERSET) {
			if (has_any_dice_on_card(S15_A_PLUMP_OF_SPEARS))
				game.hits += 1
		}
	}

	if (game.scenario === S22_GABIENE) {
		if (target === S22_SILVER_SHIELDS) {
			if (is_card_in_play(S22_EUMENES_CAMP)) {
				game.hits = Math.min(1, game.hits - 1)
			}
		}
	}

	if (game.scenario === S31_NEWBURY_1ST) {
		if (game.selected === S31_WENTWORTH) {
			if (has_any_dice_on_card(S31_BYRON) && is_card_in_play(S31_SKIPPON)) {
				game.hits += 1
			}
		}
		if (target === S31_WENTWORTH) {
			if (has_any_dice_on_card(S31_BYRON) && is_card_in_play(S31_SKIPPON)) {
				game.hits -= 1
			}
		}
	}

	if (game.scenario === S37_INKERMAN) {
		// Until the first Fog Cube is lifted.
		if (target === S37_SOIMONOFF && get_cubes(S37_THE_FOG) === 3) {
			game.hits -= 1
		}
	}

	if (game.scenario === S39_MARSAGLIA) {
		if (game.selected === S39_EUGENE) {
			if (has_any_dice_on_card(S39_CANNONS)) {
				game.self = 0
			}
		}
		if (game.selected === S39_CATINAT && game.target2 === S39_BAYONETS) {
			game.self = 0
		}
	}

	if (game.scenario === S44_HOHENFRIEDBERG) {
		if (target === S44_CHARLES) {
			if (game.selected === S44_LEOPOLDS_L || game.selected === S44_LEOPOLDS_C || game.selected === S44_LEOPOLDS_R)
				game.self = 0
		}
	}

	// Oblique Attack (CAL expansion rule)
	if (is_infantry(game.selected)) {
		if (get_sticks(game.selected) >= get_sticks(target) + 3)
			game.hits += 1
	}

	let extra = card_has_rule(game.selected, "extra_hit_if_dice_on")
	if (extra && has_any_dice_on_card(extra[0]))
		game.hits += 1

	// Linked Formations (TGA and CAL expansion rule)
	// If a card Absorbs Hits, the presence of a Link does not reduce the number of hits.
	if (!is_absorb) {
		if (card_has_active_link(target))
			game.hits = Math.max(0, game.hits - 1)
	}

	if (card_has_rule(target, "suffer_1_less"))
		game.hits = Math.max(0, game.hits - 1)

	if (card_has_rule(target, "suffer_1_less_1_max"))
		game.hits = Math.max(0, Math.min(1, game.hits - 1))

	if (game.self2)
		game.self += game.self2
}

// Update hits and self hits for defensive abilities that redirect or steal hits.
function update_attack2() {
	if (game.scenario === S28_CULPS_HILL) {
		if (is_card_in_play(S28_BREASTWORKS)) {
			if (data.cards[game.target].wing === DKBLUE) {
				if (game.hits > 0) {
					game.target2 = S28_BREASTWORKS
					if (game.hits > 1) {
						game.hits2 = game.hits - 1
						game.hits = 1
					} else {
						game.hits2 = 1
						game.hits = 0
					}
				}
			}
		}
	}
}

states.attack = {
	inactive: "attack",
	prompt() {
		view.prompt = "Attack " + card_name(game.target) + "."
		gen_action_card(game.target)

		let w = side_get_wild_die_card(player_index())
		if (w >= 0)
			gen_action_dice_on_card(w)

		let may_take_from = card_has_rule(game.selected, "may_take_from")
		if (may_take_from) {
			for (let from of may_take_from)
				gen_action_dice_on_card(from)
		}

		let may_take_from_extra = card_has_rule(game.selected, "may_take_from_extra_self")
		if (may_take_from_extra) {
			for (let from of may_take_from_extra)
				gen_action_dice_on_card(from)
		}

		if (game.scenario === S39_MARSAGLIA) {
			if (game.selected === S39_CATINAT) {
				gen_action_dice_on_card(S39_BAYONETS)
			}
		}

		if (game.scenario === S49_LEUTHEN) {
			if (player_index() === 0)
				gen_action_dice_on_card(S49_THE_LEUTHEN_CHORALE)
		}

		view.actions.attack = 1
	},
	attack() {
		log("Attack\nC" + game.selected + "\nC" + game.target)

		if (can_opponent_react()) {
			clear_undo()
			set_opponent_active()
			game.state = "react"
		} else {
			resume_attack()
		}
	},
	card(_) {
		this.attack()
	},
	die(d) {
		let from = get_dice_location(d)

		let w = side_get_wild_die_card(player_index())
		if (w === from) {
			take_wild_die(w, game.selected)
			update_attack1()
			update_attack2()
			return
		}

		let may_take_from = card_has_rule(game.selected, "may_take_from")
		if (may_take_from) {
			take_all_dice(from, game.selected)
			update_attack1()
			update_attack2()
			return
		}

		let may_take_from_extra = card_has_rule(game.selected, "may_take_from_extra_self")
		if (may_take_from_extra) {
			take_all_dice(from, game.selected)
			game.self2 = 1
			update_attack1()
			update_attack2()
			return
		}

		if (game.scenario === S39_MARSAGLIA) {
			if (game.selected === S39_CATINAT && from === S39_BAYONETS) {
				take_all_dice(from, game.selected)
				game.target2 = S39_BAYONETS
				update_attack1()
				update_attack2()
				return
			}
		}

		if (game.scenario === S49_LEUTHEN) {
			if (from === S49_THE_LEUTHEN_CHORALE) {
				take_all_dice(from, game.selected)
				game.target2 = S49_THE_LEUTHEN_CHORALE
				update_attack1()
				update_attack2()
				return
			}
		}

		throw new Error("no handler for taking dice from other card")
	}
}

function resume_attack() {
	pay_for_action(game.selected)

	if (game.hits === 1) {
		if (game.self > 0)
			log(">1 hit. " + game.self + " self.")
		else
			log(">1 hit.")
	} else {
		if (game.hits > 0 && game.self > 0)
			log(">" + game.hits + " hits. " + game.self + " self.")
		else if (game.hits > 0)
			log(">" + game.hits + " hits.")
		else if (game.self > 0)
			log(">" + game.self + " self.")
	}

	if (game.hits > 0)
		remove_sticks(game.target, game.hits)
	if (game.self > 0)
		remove_sticks(game.selected, game.self)

	if (game.target2 >= 0 && game.hits2 > 0) {
		log(">" + game.hits2 + " hits on C" + game.target2)
		remove_sticks(game.target2, game.hits2)
	}

	if (game.scenario === S39_MARSAGLIA) {
		// remove after using
		if (game.target2 === S39_BAYONETS) {
			remove_card(S39_BAYONETS)
		}
	}

	if (game.scenario === S44_HOHENFRIEDBERG) {
		// remove after first attack
		if (game.selected === S44_BAYREUTH_DRAGOONS) {
			remove_card(S44_BAYREUTH_DRAGOONS)
		}
	}

	if (game.scenario === S49_LEUTHEN) {
		// remove after using
		if (game.target2 === S49_THE_LEUTHEN_CHORALE) {
			remove_card(S49_THE_LEUTHEN_CHORALE)
		}
	}

	end_action_phase()
}

// === COMMAND ===

function goto_command() {

	if (game.scenario === S37_INKERMAN && game.selected === S37_THE_FOG) {
		log("The Fog Lifts...")
		remove_cubes(S37_THE_FOG, 1)
		remove_dice(game.selected)
		end_action_phase()
		return
	}

	if (game.scenario === S38_FLEURUS && game.selected === S38_WALDECK) {
		remove_cubes(S38_RETREAT_TO_NIVELLES, 1)
		remove_dice(game.selected)
		if (!has_any_cubes_on_card(S38_RETREAT_TO_NIVELLES)) {
			game.selected = S38_RETREAT_TO_NIVELLES
			game.state = "s38_retreat_to_nivelles_1"
			game.starting_on_your_next_turn = 0
			return
		}
		end_action_phase()
		return
	}

	game.state = "command"
}

states.command = {
	inactive: "command",
	prompt() {
		let list = find_all_targets_of_command(game.selected, current_action())
		view.prompt = "Bring " + list.map(c => card_name(c)).join(" and ") + " out of reserve."
		for (let t of list)
			gen_action_card(t)
	},
	card(c) {
		log("Command\nC" + game.selected + "\nC" + c)

		if (game.scenario === S4_BOSWORTH_FIELD) {
			if (c === S4_THE_STANLEYS) {
				if (is_card_in_play_or_reserve(S4_NORTHUMBERLAND)) {
					log("The Stanleys rout Northumberland.")
					set_sticks(S4_NORTHUMBERLAND, 0)
				}
			}
		}

		if (game.scenario === S13_EDGECOTE_MOOR) {
			if (game.reserve[0].length === 2 && game.reserve[1].length === 1) {
				let p = player_index()
				log("Gained a second morale cube.")
				game.morale[p] += 1
			}
		}

		if (game.scenario === S37_INKERMAN) {
			if (c === S37_PAULOFFS_LEFT) {
				log("Morale Cube added to Russian side.")
				game.morale[0] += 1
			}
		}

		let p = player_index()
		set_delete(game.reserve[p], c)
		set_add(game.front[p], c)

		if (game.scenario === S44_HOHENFRIEDBERG) {
			// one at a time
			if (game.selected === S44_CHARLES || game.selected === S44_FREDERICK_II) {
				pay_for_action(game.selected)
				end_action_phase()
				return
			}
		}

		if (find_first_target_of_command(game.selected, current_action()) < 0) {
			pay_for_action(game.selected)
			end_action_phase()
		}
	},
}

states.s38_retreat_to_nivelles_1 = {
	inactive: "remove cards",
	prompt() {
		view.prompt = "Retreat to Nivelles: Remove all in-play Grand Alliance cards!"
		for (let c of game.front[1])
			if (c !== S38_RETREAT_TO_NIVELLES)
				gen_action_card(c)
	},
	card(c) {
		move_all_sticks(c, S38_RETREAT_TO_NIVELLES)
		remove_card(c)
		if (game.front[1].length === 1)
			game.state = "s38_retreat_to_nivelles_2"
	},
}

states.s38_retreat_to_nivelles_2 = {
	inactive: "enter reserves",
	prompt() {
		view.prompt = "Retreat to Nivelles: Bring van Aylva & van Weibnom out of reserve."
		for (let c of game.reserve[1])
			gen_action_card(c)
	},
	card(c) {
		bring_out_of_reserve(c)
		if (game.reserve[1].length === 0)
			end_action_phase()
	},
}

// === REACTION ===

function can_opponent_react() {

	if (game.scenario === S39_MARSAGLIA) {
		if (game.selected === S39_CATINAT && game.target2 === S39_BAYONETS)
			return false
	}

	if (game.scenario === S14_BARNET) {
		if (game.selected === S14_TREASON)
			return false
	}

	let p = 1 - player_index()
	let wild = side_has_wild_die(p)
	for (let c of game.front[p])
		if (can_card_react(c, wild))
			return true
	return false
}

function can_card_react(c, wild) {
	let has_dice = has_any_dice_on_card(c)
	let has_cube = has_any_cubes_on_card(c)
	if (has_dice || has_cube) {
		if (data.cards[c].actions.length >= 1)
			if (is_reaction(c, data.cards[c].actions[0]))
				if (can_take_reaction(c, data.cards[c].actions[0], wild))
					return true
		if (data.cards[c].actions.length >= 2)
			if (is_reaction(c, data.cards[c].actions[1]))
				if (can_take_reaction(c, data.cards[c].actions[1], wild))
					return true
	}
	return false
}

function can_take_reaction(c, a, wild) {
	switch (a.type) {
	default:
		throw new Error("invalid reaction: " + a.type)

	case "Screen":
		// if a friendly formation is attacked by a listed enemy formation
		// ... or a listed formation is attacked (Wheatfield Road Artillery, etc)
		if (!a.target_list.includes(game.selected) && !a.target_list.includes(game.target))
			return false
		break

	case "Counterattack":
		// if this formation is attacked
		if (game.target !== c)
			return false
		// ... by one of the listed targets
		if (a.choice) {
			// if "any" or "or" choice
			if (!a.target_list.includes(game.selected))
				return false
		} else {
			// if strict order
			if (find_target_of_counterattack(a) !== game.selected)
				return false
		}
		break

	case "Absorb":
		// if attack target is listed on absorb action
		if (a.choice) {
			if (!a.target_list.includes(game.target))
				return false
		} else {
			if (find_target_of_absorb(a) !== game.target)
				return false
		}
		break
	}

	if (game.scenario === S7_THE_DUNES) {
		if (has_any_cubes_on_card(S7_THE_ENGLISH_FLEET)) {
			if (c === S7_DON_JUAN_JOSE)
				return false
			if (c === S7_SPANISH_RIGHT_CAVALRY)
				return false
		}
	}

	if (game.scenario === S15_TEWKESBURY) {
		if (c === S15_WENLOCK) {
			if (is_removed_from_play(S15_SOMERSET))
				return false
		}
	}

	if (game.scenario === S31_NEWBURY_1ST) {
		if (c === S31_GERARD) {
			// London Trained Bands enter play when Skippon routs
			if (is_removed_from_play(S31_SKIPPON))
				return false
		}
	}

	if (game.scenario === S43_DENAIN) {
		if (c === S43_DUTCH_HORSE) {
			// May only screen Villars's Left if Broglie has routed
			if (game.selected === S43_VILLARS_LEFT)
				if (is_card_in_play(S43_BROGLIE))
					return false
		}
	}

	if (data.cards[c].special)
		return check_cube_requirement(c, a.requirement)
	else
		return check_dice_requirement(c, a.requirement, wild)
}

function take_wild_die_if_needed_for_reaction(c, ix) {
	let w = side_get_wild_die_card(player_index())
	if (w >= 0) {
		let a = data.cards[c].actions[ix]
		if (!can_take_reaction(c, a, false)) {
			take_wild_die(w, c)
		}
	}
}

states.react = {
	inactive: "react",
	prompt() {
		view.prompt = card_name(game.selected) + " attacks " + card_name(game.target) + "!"
		let voluntary = true
		let p = player_index()
		let wild = side_has_wild_die(p)
		for (let c of game.front[p]) {
			let has_dice = has_any_dice_on_card(c)
			let has_cube = has_any_cubes_on_card(c)
			if (has_dice || has_cube) {
				for (let i = 0; i < data.cards[c].actions.length; ++i) {
					let a = data.cards[c].actions[i]
					if (is_reaction(c, a)) {
						let must = false
						let may = false
						if (is_mandatory_reaction(c, a)) {
							must = can_take_reaction(c, a, false)
							if (!must && wild && can_take_reaction(c, a, true))
								may = true
						} else {
							may = can_take_reaction(c, a, wild)
						}
						if (must)
							voluntary = false
						if (must || may) {
							if (i === 0) gen_action_action1(c)
							if (i === 1) gen_action_action2(c)
						}
					}
				}
			}
		}
		if (voluntary)
			view.actions.pass = 1
	},
	a1(c) {
		push_undo()
		take_wild_die_if_needed_for_reaction(c, 0)
		goto_take_reaction(c, 0)
	},
	a2(c) {
		push_undo()
		take_wild_die_if_needed_for_reaction(c, 1)
		goto_take_reaction(c, 1)
	},
	pass() {
		set_opponent_active()
		resume_attack()
	},
}

function goto_take_reaction(c, ix) {
	let a = data.cards[c].actions[ix]
	switch (a.type) {
		case "Screen":
			goto_screen(c, a)
			break
		case "Absorb":
			goto_absorb(c, a)
			break
		case "Counterattack":
			goto_counterattack(c, a)
			break
	}
}

function end_reaction() {
	clear_undo()
	set_opponent_active()
	resume_attack()
}

// === SCREEN ===

function goto_screen(c, a) {
	game.reacted = player_index()

	game.target = c

	update_attack1()

	switch (a.effect)
	{
	default:
		throw new Error("invalid screen effect: " + a.effect)
	case undefined:
		game.hits = 0
		game.self = 0
		break
	case "If either Chariot formation is screened, it suffers 1 hit!":
		game.hits = 0
		if (card_has_rule(game.selected, "is_chariot"))
			game.self = 1
		else
			game.self = 0
		break
	}

	update_attack2()

	game.state = "screen"
}

states.screen = {
	inactive: "screen",
	prompt() {
		view.prompt = "Screen attack from " + card_name(game.selected) + "."
		view.actions.screen = 1
		gen_action_card(game.selected)
	},
	screen() {
		log("Screen\nC" + game.target)

		pay_for_action(game.target)

		if (card_has_rule(game.target, "remove_after_screen"))
			remove_card(game.target)

		end_reaction()
	},
	card(_) {
		this.screen()
	},
}

// === ABSORB ===

function goto_absorb(c, a) {
	game.reacted = player_index()

	if (game.scenario === S29_GETTYS_2ND) {
		if (c === S29_MEADE) {
			game.state = "s29_meade"
			return
		}
	}

	game.target = c

	update_attack1(true)

	switch (a.effect)
	{
	default:
		throw new Error("invalid absorb effect: " + a.effect)
	case "Suffers hits.":
		break
	case "Suffers 1 hit only.":
		game.hits = 1
		break
	case "Suffers 1 less hit.":
		game.hits = Math.max(0, game.hits - 1)
		break
	case "Suffers 1 less hit per die.":
		game.hits = Math.max(0, game.hits - count_dice_on_card(c))
		break
	}

	update_attack2()

	game.state = "absorb"
}

states.s29_meade = {
	inactive: "absorb",
	prompt() {
		view.prompt = "Choose any friendly Formation except Little Round Top to absorb the hits instead."
		let p = player_index()
		for (let c of game.front[p]) {
			if (c !== S29_MEADE && c !== S29_LITTLE_ROUND_TOP && c !== game.target)
				gen_action_card(c)
		}
	},
	card(c) {
		remove_dice(S29_MEADE)
		game.target = c
		update_attack1()
		update_attack2()
		game.state = "absorb"
	}
}

states.absorb = {
	inactive: "absorb",
	prompt() {
		view.prompt = "Absorb attack from " + card_name(game.selected) + "."
		view.actions.absorb = 1
		gen_action_card(game.selected)
	},
	absorb() {
		log("Absorb\nC" + game.target)

		pay_for_action(game.target)
		end_reaction()
	},
	card(_) {
		this.absorb()
	},
}

// === COUNTERATTACK ===

function goto_counterattack(c, a) {
	game.reacted = player_index()

	update_attack1()

	switch (a.effect)
	{
	default:
		throw new Error("invalid counterattack effect: " + a.effect)
	case "1 hit.":
		game.self += 1
		break
	case "1 hit per die.":
		game.self += count_dice_on_card(c)
		break
	case "1 hit. Suffers 1 hit only.":
		game.self += 1
		game.hits = 1
		break
	case "Suffers 1 less hit.":
		game.hits = Math.max(0, game.hits - 1)
		break
	case "1 hit. Suffers 1 less hit.":
		game.self += 1
		game.hits = Math.max(0, game.hits - 1)
		break
	case "1 hit. Suffers 1 less hit per die.":
		game.self += 1
		game.hits = Math.max(0, game.hits - count_dice_on_card(c))
		break
	case "1 hit. Suffers 1 less hit and never more than 1.":
		game.self += 1
		game.hits = Math.max(0, Math.min(1, game.hits - 1))
		break
	case "1 hit. Suffers 2 less hits and never more than 1.":
		game.self += 1
		game.hits = Math.max(0, Math.min(1, game.hits - 2))
		break
	}

	if (game.scenario === S48_BRESLAU) {
		if (player_index() === 1) {
			if (has_any_dice_on_card(S48_BEVERN))
				game.self += 1
		}
	}

	update_attack2()

	game.state = "counterattack"
}

states.counterattack = {
	inactive: "counterattack",
	prompt() {
		view.prompt = "Counterattack " + card_name(game.selected) + "."
		view.actions.counterattack = 1
		gen_action_card(game.selected)
	},
	counterattack() {
		log("Counterattack\nC" + game.target)

		pay_for_action(game.target)
		end_reaction()
	},
	card(_) {
		this.counterattack()
	},
}

// === ROUTING/PURSUIT/REMOVE/FORCE-RETIRE ===

function find_card_owner(c) {
	if (set_has(game.front[0], c) || set_has(game.reserve[0], c))
		return 0
	if (set_has(game.front[1], c) || set_has(game.reserve[1], c))
		return 1
	throw new Error("card not found in any player area")
}

function should_rout_card(c) {
	if (!data.cards[c].special) {
		if (get_sticks(c) === 0)
			return true
	}

	let rout_with = card_has_rule(c, "rout_with")
	if (rout_with) {
		for (let other of rout_with)
			if (!is_removed_from_play(other))
				return false
		return true
	}

	return false
}

function should_pursue(c) {
	let pursuit = data.cards[c].pursuit
	if (pursuit !== undefined)
		return !set_has(game.front[0], pursuit) && !set_has(game.front[1], pursuit)
	return false
}

function should_remove_card(c) {
	let remove_with = card_has_rule(c, "remove_with")
	if (remove_with) {
		for (let other of remove_with)
			if (!is_removed_from_play(other))
				return false
		return true
	}
	return false
}

function should_retire_card(c) {
	let retire_with = card_has_rule(c, "retire_with")
	if (retire_with) {
		for (let other of retire_with)
			if (!is_removed_from_play(other))
				return false
		return true
	}

	if (game.scenario === S25_WHEATFIELD) {
		if (c === S25_ZOOK || c === S25_KELLY) {
			if (is_card_in_play(S25_WOFFORD))
				return true
		}
	}

	return false
}

function goto_routing() {
	game.routed = [ 0, 0 ]

	if (game.scenario === S2_MARSTON_MOOR) {
		if (is_card_in_play(S2_RUPERTS_LIFEGUARD)) {
			if (is_card_in_play(S2_NORTHERN_HORSE) && should_rout_card(S2_NORTHERN_HORSE)) {
				game.state = "s2_ruperts_lifeguard"
				game.selected = S2_NORTHERN_HORSE
				return
			}
			if (is_card_in_play(S2_BYRON) && should_rout_card(S2_BYRON)) {
				game.state = "s2_ruperts_lifeguard"
				game.selected = S2_BYRON
				return
			}
		}
	}

	resume_routing()
}

states.s2_ruperts_lifeguard = {
	inactive: "remove cards",
	prompt() {
		view.prompt = "Rupert's Lifeguard: Add the Lifeguard's Unit to " + card_name(game.selected)
		gen_action_card(S2_RUPERTS_LIFEGUARD)
	},
	card(c) {
		log(`Move\nC${S2_RUPERTS_LIFEGUARD}\nC${game.selected}\n1 stick.`)
		move_all_sticks(S2_RUPERTS_LIFEGUARD, game.selected)
		remove_card(S2_RUPERTS_LIFEGUARD)
		game.selected = -1
		resume_routing()
	}

}

function resume_routing() {
	game.state = "routing"

	for (let p = 0; p <= 1; ++p) {
		for (let c of game.front[p])
			if (should_rout_card(c) || should_remove_card(c) || should_retire_card(c) || should_pursue(c))
				return
		for (let c of game.reserve[p])
			if (should_rout_card(c) || should_remove_card(c) || should_retire_card(c))
				return
	}

	end_routing()
}

states.routing = {
	inactive: "remove routing and pursuing cards",
	prompt() {
		view.prompt = "Routing: Remove routing and pursuing cards from play!"
		for (let p = 0; p <= 1; ++p) {
			for (let c of game.front[p])
				if (should_rout_card(c) || should_remove_card(c) || should_retire_card(c) || should_pursue(c))
					gen_action_card(c)
			for (let c of game.reserve[p])
				if (should_rout_card(c) || should_remove_card(c) || should_retire_card(c))
					gen_action_card(c)
		}
	},
	card(c) {
		if (should_rout_card(c)) {
			let p = find_card_owner(c)
			game.routed[p] += data.cards[c].morale
			rout_card(c)
		} else if (should_retire_card(c)) {
			log("Retire\nC" + c)
			retire_card(c)
		} else if (should_pursue(c)) {
			log("Pursuit\nC" + c)
			pursue_card(c)
		} else {
			remove_card(c)
		}
		resume_routing()
	},
}

function end_routing() {
	// Normal morale loss and gain
	if (game.morale[0] > 0 && game.morale[1] > 0) {
		if ((game.routed[0] > 0 && !game.routed[1]) || (game.routed[1] > 0 && !game.routed[0])) {
			if (game.routed[0]) {
				game.routed[0] = Math.min(game.routed[0], game.morale[0])
				game.morale[0] -= game.routed[0]
				// do not gain for special scenarios
				if (game.morale[1] > 0)
					game.morale[1] += game.routed[0]
			} else {
				game.routed[1] = Math.min(game.routed[1], game.morale[1])
				game.morale[1] -= game.routed[1]
				// do not gain for special scenarios
				if (game.morale[0] > 0)
					game.morale[0] += game.routed[1]
			}
		}
		if (check_victory())
			return
	} else {
		// SPECIAL: S3 - Plains of Abraham
		// SPECIAL: S34 - Tippermuir - Royalists
		// SPECIAL: S35 - Auldearn - Royalists

		// Instant loss if any card routs for side at 0 morale (S3, S34, S35).
		if (game.morale[0] === 0 && game.routed[0])
			return goto_game_over(P2, player_name(0) + " routed!")
		if (game.morale[1] === 0 && game.routed[1])
			return goto_game_over(P1, player_name(1) + " routed!")

		// Remove instead of take cubes for side at 0 morale
		if (game.routed[0]) {
			game.morale[0] -= Math.min(game.routed[0], game.morale[0])
			if (game.morale[0] === 0)
				return goto_game_over(P2, player_name(0) + " ran out of morale!")
		}
		if (game.routed[1]) {
			game.morale[1] -= Math.min(game.routed[1], game.morale[1])
			if (game.morale[1] === 0)
				return goto_game_over(P1, player_name(1) + " ran out of morale!")
		}
	}

	game.routed = null

	goto_reserve()
}

// === RESERVE ===

function should_enter_reserve(c) {
	let reserve = data.cards[c].reserve

	if (game.scenario === S37_INKERMAN) {
		if (c === S37_BRITISH_TROOPS)
			return get_cubes(S37_THE_FOG) === 1
		if (c === S37_FRENCH_TROOPS)
			return get_cubes(S37_THE_FOG) === 0
	}

	if (Array.isArray(reserve)) {
		for (let t of reserve) {
			if (is_removed_from_play(t))
				return true
		}
	}

	if (game.scenario === S30_EDGEHILL) {
		if (c === S30_BALFOUR || c === S30_STAPLETON) {
			return is_removed_from_play(S30_RUPERT) && is_removed_from_play(S30_WILMOT)
		}
	}

	return false
}

function goto_reserve() {
	resume_reserve()
}

function resume_reserve() {
	game.state = "reserve"
	for (let p = 0; p <= 1; ++p)
		for (let c of game.reserve[p])
			if (should_enter_reserve(c))
				return
	end_reserve()
}

function end_reserve() {
	goto_roll_phase()
}

function bring_out_of_reserve(c) {
	log("Reserve\nC" + c)
	let p = find_card_owner(c)
	set_delete(game.reserve[p], c)
	set_add(game.front[p], c)
}

states.reserve = {
	inactive: "enter reserves",
	prompt() {
		view.prompt = "Enter reserves!"
		for (let p = 0; p <= 1; ++p)
			for (let c of game.reserve[p])
				if (should_enter_reserve(c))
					gen_action_card(c)
	},
	card(c) {
		bring_out_of_reserve(c)
		resume_reserve()
	},
}

// === COMMON LIBRARY ===

function gen_action(action, argument) {
	if (!(action in view.actions))
		view.actions[action] = [ argument ]
	else
		set_add(view.actions[action], argument)
}

function gen_action_dice_on_card(c) {
	for (let d = 0; d < 12; ++d) {
		if (get_dice_location(d) === c) {
			gen_action_die(d)
			return
		}
	}
}

function gen_action_card(c) {
	gen_action("card", c)
}

function gen_action_die(d) {
	gen_action("die", d)
}

function gen_action_action1(c) {
	gen_action("a1", c)
}

function gen_action_action2(c) {
	gen_action("a2", c)
}

function gen_action_null1(c) {
	gen_action("n1", c)
}

function gen_action_null2(c) {
	gen_action("n2", c)
}

function gen_action_retire(c) {
	gen_action("retire", c)
}

function log(msg) {
	game.log.push(msg)
}

function clear_undo() {
	if (game.undo) {
		game.undo.length = 0
	}
}

function push_undo() {
	if (game.undo) {
		let copy = {}
		for (let k in game) {
			let v = game[k]
			if (k === "undo")
				continue
			else if (k === "log")
				v = v.length
			else if (typeof v === "object" && v !== null)
				v = object_copy(v)
			copy[k] = v
		}
		game.undo.push(copy)
	}
}

function pop_undo() {
	if (game.undo) {
		let save_log = game.log
		let save_undo = game.undo
		game = save_undo.pop()
		save_log.length = game.log
		game.log = save_log
		game.undo = save_undo
	}
}

function random(range) {
	// Largest MLCG that will fit its state in a double.
	// Uses BigInt for arithmetic, so is an order of magnitude slower.
	// https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf
	// m = 2**53 - 111
	return (game.seed = Number(BigInt(game.seed) * 5667072534355537n % 9007199254740881n)) % range
}

// Fast deep copy for objects without cycles
function object_copy(original) {
	if (Array.isArray(original)) {
		let n = original.length
		let copy = new Array(n)
		for (let i = 0; i < n; ++i) {
			let v = original[i]
			if (typeof v === "object" && v !== null)
				copy[i] = object_copy(v)
			else
				copy[i] = v
		}
		return copy
	} else {
		let copy = {}
		for (let i in original) {
			let v = original[i]
			if (typeof v === "object" && v !== null)
				copy[i] = object_copy(v)
			else
				copy[i] = v
		}
		return copy
	}
}

// Array remove and insert (faster than splice)

function array_remove(array, index) {
	let n = array.length
	for (let i = index + 1; i < n; ++i)
		array[i - 1] = array[i]
	array.length = n - 1
}

function array_insert(array, index, item) {
	for (let i = array.length; i > index; --i)
		array[i] = array[i - 1]
	array[index] = item
}

function array_remove_pair(array, index) {
	let n = array.length
	for (let i = index + 2; i < n; ++i)
		array[i - 2] = array[i]
	array.length = n - 2
}

function array_insert_pair(array, index, key, value) {
	for (let i = array.length; i > index; i -= 2) {
		array[i] = array[i-2]
		array[i+1] = array[i-1]
	}
	array[index] = key
	array[index+1] = value
}

// Set as plain sorted array

function set_has(set, item) {
	let a = 0
	let b = set.length - 1
	while (a <= b) {
		let m = (a + b) >> 1
		let x = set[m]
		if (item < x)
			b = m - 1
		else if (item > x)
			a = m + 1
		else
			return true
	}
	return false
}

function set_add(set, item) {
	let a = 0
	let b = set.length - 1
	while (a <= b) {
		let m = (a + b) >> 1
		let x = set[m]
		if (item < x)
			b = m - 1
		else if (item > x)
			a = m + 1
		else
			return
	}
	array_insert(set, a, item)
}

function set_delete(set, item) {
	let a = 0
	let b = set.length - 1
	while (a <= b) {
		let m = (a + b) >> 1
		let x = set[m]
		if (item < x)
			b = m - 1
		else if (item > x)
			a = m + 1
		else {
			array_remove(set, m)
			return
		}
	}
}

// Map as plain sorted array of key/value pairs

function map_clear(map) {
	map.length = 0
}

function map_has(map, key) {
	let a = 0
	let b = (map.length >> 1) - 1
	while (a <= b) {
		let m = (a + b) >> 1
		let x = map[m<<1]
		if (key < x)
			b = m - 1
		else if (key > x)
			a = m + 1
		else
			return true
	}
	return false
}

function map_get(map, key, missing) {
	let a = 0
	let b = (map.length >> 1) - 1
	while (a <= b) {
		let m = (a + b) >> 1
		let x = map[m<<1]
		if (key < x)
			b = m - 1
		else if (key > x)
			a = m + 1
		else
			return map[(m<<1)+1]
	}
	return missing
}

function map_set(map, key, value) {
	let a = 0
	let b = (map.length >> 1) - 1
	while (a <= b) {
		let m = (a + b) >> 1
		let x = map[m<<1]
		if (key < x)
			b = m - 1
		else if (key > x)
			a = m + 1
		else {
			map[(m<<1)+1] = value
			return
		}
	}
	array_insert_pair(map, a<<1, key, value)
}

function map_delete(map, item) {
	let a = 0
	let b = (map.length >> 1) - 1
	while (a <= b) {
		let m = (a + b) >> 1
		let x = map[m<<1]
		if (item < x)
			b = m - 1
		else if (item > x)
			a = m + 1
		else {
			array_remove_pair(map, m<<1)
			return
		}
	}
}