"use strict" // TODO: execute enemy heirs during supply phase // TODO: reuse supply and goes-home states for pretender and king // TODO: mulligan rule exports.scenarios = [ "Wars of the Roses", "Kingmaker", "Richard III", ] exports.roles = [ "York", "Lancaster" ] const { CARDS, BLOCKS, AREAS, BORDERS, block_index, area_index } = require('./data') const first_area = 6 // first real area (skip pools and seas) const last_area = AREAS.length - 4 // skip layout areas at end const block_count = BLOCKS.length // roles const LANCASTER = "Lancaster" const YORK = "York" const ENEMY = { Lancaster: "York", York: "Lancaster" } const OBSERVER = "Observer" const BOTH = "Both" const PLAYER_ID = { "": 0, Lancaster: 1, York: 2 } const ID_PLAYER = [ "", "Lancaster", "York" ] // areas const NOWHERE = 0 const POOL = 1 const MINOR = 2 const IRISH_SEA = area_index["Irish Sea"] const NORTH_SEA = area_index["North Sea"] const ENGLISH_CHANNEL = area_index["English Channel"] const IRELAND = area_index["Ireland"] const SCOTLAND = area_index["Scotland"] const FRANCE = area_index["France"] const CALAIS = area_index["Calais"] const CAERNARVON = area_index["Caernarvon"] const PEMBROKE = area_index["Pembroke"] const POWYS = area_index["Powys"] const GLAMORGAN = area_index["Glamorgan"] const CORNWALL = area_index["Cornwall"] const EAST_YORKS = area_index["East Yorks"] const HEREFORD = area_index["Hereford"] const ISLE_OF_MAN = area_index["Isle of Man"] const MIDDLESEX = area_index["Middlesex"] const NORTH_YORKS = area_index["North Yorks"] const RUTLAND = area_index["Rutland"] const SOUTH_YORKS = area_index["South Yorks"] // blocks const NOBODY = -1 const B_YORK = block_index["York"] const B_MARCH = block_index["March"] const B_RUTLAND = block_index["Rutland"] const B_CLARENCE_Y = block_index["Clarence/Y"] const B_GLOUCESTER = block_index["Gloucester"] const B_EXETER_Y = block_index["Exeter/Y"] const B_WARWICK_Y = block_index["Warwick/Y"] const B_KENT_Y = block_index["Kent/Y"] const B_SALISBURY_Y = block_index["Salisbury/Y"] const B_IRISH_MERCENARY = block_index["Irish Mercenary"] const B_BURGUNDIAN_MERCENARY = block_index["Burgundian Mercenary"] const B_CALAIS_MERCENARY = block_index["Calais Mercenary"] const B_HENRY_VI = block_index["Henry VI"] const B_PRINCE_EDWARD = block_index["Prince Edward"] const B_EXETER_L = block_index["Exeter/L"] const B_SOMERSET = block_index["Somerset"] const B_RICHMOND = block_index["Richmond"] const B_WARWICK_L = block_index["Warwick/L"] const B_KENT_L = block_index["Kent/L"] const B_SALISBURY_L = block_index["Salisbury/L"] const B_CLARENCE_L = block_index["Clarence/L"] const B_SCOTS_MERCENARY = block_index["Scots Mercenary"] const B_WELSH_MERCENARY = block_index["Welsh Mercenary"] const B_FRENCH_MERCENARY = block_index["French Mercenary"] const B_REBEL = block_index["Rebel"] // serif cirled numbers const DIE_HIT = [ 0, '\u2776', '\u2777', '\u2778', '\u2779', '\u277A', '\u277B' ] const DIE_MISS = [ 0, '\u2460', '\u2461', '\u2462', '\u2463', '\u2464', '\u2465' ] const ATTACK_MARK = "*" const RESERVE_MARK = "" let states = {} let game = null function random(n) { if (game.rng === 1) return Math.floor(((game.seed = game.seed * 48271 % 0x7fffffff) / 0x7fffffff) * n) return (game.seed = game.seed * 200105 % 34359738337) % n } function logbr() { if (game.log.length > 0 && game.log[game.log.length-1] !== "") game.log.push("") } function log_battle(s) { game.log.push(game.active[0] + ": " + s) } function logp(s) { game.log.push(game.active + " " + s) } function logi(s) { game.log.push(">" + s) } function log(s) { game.log.push(s) } function log_move_start(from) { game.turn_buf = [ "#" + from ] } function log_move_continue(to, mark) { if (mark) game.turn_buf.push("#" + to + mark) else game.turn_buf.push("#" + to) } function log_move_end() { if (game.turn_buf) { game.turn_log.push(game.turn_buf) delete game.turn_buf } } function print_turn_log_no_count(text) { log(text) if (game.turn_log.length > 0) { game.turn_log.sort() for (let entry of game.turn_log) logi(entry.join(" \u2192 ")) } else { logi("nothing.") } delete game.turn_log } function print_turn_log(text) { function print_move(last) { logi(n + " " + last.join(" \u2192 ")) } game.turn_log.sort() let last = game.turn_log[0] log(text) let n = 0 for (let entry of game.turn_log) { if (entry.toString() !== last.toString()) { print_move(last) n = 0 } ++n last = entry } if (n > 0) print_move(last) else logi("nothing.") delete game.turn_log } function is_inactive_player(current) { return current === OBSERVER || (game.active !== current && game.active !== BOTH) } function remove_from_array(array, item) { let i = array.indexOf(item) if (i >= 0) array.splice(i, 1) } function gen_action_undo(view) { if (!view.actions) view.actions = {} if (game.undo && game.undo.length > 0) view.actions.undo = 1 else view.actions.undo = 0 } function gen_action(view, action, argument) { if (!view.actions) view.actions = {} if (argument !== undefined) { if (!(action in view.actions)) { view.actions[action] = [ argument ] } else { if (!set_has(view.actions[action], argument)) set_add(view.actions[action], argument) } } else { view.actions[action] = 1 } } function roll_d6() { return random(6) + 1 } function shuffle_deck() { let deck = [] for (let c = 1; c <= 25; ++c) deck.push(c) return deck } function deal_cards(deck, n) { let hand = [] for (let i = 0; i < n; ++i) { let k = random(deck.length) hand.push(deck[k]) deck.splice(k, 1) } return hand } function count_ap(hand) { let count = 0 for (let c of hand) count += CARDS[c].actions return count } function is_pretender_heir(who) { return (block_owner(who) === block_owner(game.pretender) && block_type(who) === 'heir') } function is_royal_heir(who) { return (block_owner(who) === block_owner(game.king) && block_type(who) === 'heir') } function is_dead(who) { return game.location[who] === NOWHERE } function is_shield_area_for(where, who, combat) { let haystack = AREAS[where].shields let needle = BLOCKS[who].shield // Nevilles going to exile in Calais if (where === CALAIS) { if (who === B_WARWICK_L || who === B_KENT_L || who === B_SALISBURY_L) return false if (count_blocks_exclude_mercenaries(CALAIS) < 4) { if (who === B_KENT_Y) return is_area_friendly_to(EAST_YORKS, LANCASTER) if (who === B_SALISBURY_Y) return is_area_friendly_to(NORTH_YORKS, LANCASTER) } } // Exeter and Clarence as enemy nobles if (who === B_EXETER_Y) return where === CORNWALL if (who === B_CLARENCE_L) return (where === SOUTH_YORKS || where === RUTLAND || where === HEREFORD) // Everyone can always use their own shield if (haystack && haystack.includes(needle)) return true // Nevilles can use each other's shields if their owner is dead if (is_neville(who)) { if (is_dead(B_WARWICK_L) && is_dead(B_WARWICK_Y) && haystack.includes("Warwick")) return true if (is_dead(B_KENT_L) && is_dead(B_KENT_Y) && haystack.includes("Kent")) return true if (is_dead(B_SALISBURY_L) && is_dead(B_SALISBURY_Y) && haystack.includes("Salisbury")) return true } // York heirs can use any York shield if (is_heir(who) && block_owner(who) === YORK) { if (haystack.includes("York")) return !combat || find_senior_heir_in_area(YORK, where) === who } // Lancaster heirs can use each other's specific shields if their owner is dead if (is_heir(who) && block_owner(who) === LANCASTER) { let available = false if (haystack.includes("Lancaster")) available = true if (is_dead(B_EXETER_L) && is_dead(B_EXETER_Y) && haystack.includes("Exeter")) available = true if (is_dead(B_SOMERSET) && haystack.includes("Somerset")) available = true if (is_dead(B_RICHMOND) && haystack.includes("Richmond")) available = true if (available) return !combat || find_senior_heir_in_area(LANCASTER, where) === who } return false } function is_at_home(who) { let where = game.location[who] if (where === NOWHERE || where === MINOR || where === POOL) return true if (is_pretender_heir(who)) return is_exile_area(where) if (is_royal_heir(who)) return is_shield_area_for(where, who, false) || is_crown_area(where) if (block_type(who) === 'nobles') return is_shield_area_for(where, who, false) if (block_type(who) === 'church') return has_cathedral(where) === block_home(who) return true } function is_in_exile(who) { return is_exile_area(game.location[who]) } function is_home_for(where, who) { if (is_pretender_heir(who)) return is_shield_area_for(where, who, false) if (is_royal_heir(who)) return is_crown_area(where) || is_shield_area_for(where, who, false) if (block_type(who) === 'nobles') return is_shield_area_for(where, who, false) if (block_type(who) === 'church') return block_home(who) === has_cathedral(where) return false } function is_available_home_for(where, who) { if (who === B_CLARENCE_L) return is_home_for(where, who) && is_vacant_area(where) return is_home_for(where, who) && is_friendly_or_vacant_area(where) } function count_available_homes(who) { let count = 0 for (let where = first_area; where <= last_area; ++where) if (is_available_home_for(where, who)) ++count return count } function available_home(who) { for (let where = first_area; where <= last_area; ++where) if (is_available_home_for(where, who)) return where } function pretender_go_home_if_possible(who) { if (!is_in_exile(who)) { // pretender heirs must go home! if (is_heir(who)) return true let n = count_available_homes(who) if (n === 0) { game.turn_log.push([block_name(who), "Pool"]) disband(who) } else if (n === 1) { let home = available_home(who) if (game.location[who] !== home) { game.location[who] = home game.turn_log.push([block_name(who), "#" + game.location[who]]) // TODO: "Home"? } } else { return true } } return false } function king_go_home_if_possible(who) { if (!is_in_exile(who)) { let n = count_available_homes(who) if (n === 0) { game.turn_log.push([block_name(who), "Pool"]) disband(who) } else if (n === 1) { let home = available_home(who) if (game.location[who] !== home) { game.location[who] = home game.turn_log.push([block_name(who), "#" + game.location[who]]) // TODO: "Home"? } } else { return true } } return false } function is_on_map_not_in_exile_or_man(who) { let where = game.location[who] return where !== NOWHERE && where !== MINOR && where !== POOL && where !== ISLE_OF_MAN && !is_exile_area(where) } function is_land_area(where) { return where !== NOWHERE && where !== MINOR && where !== POOL && !is_sea_area(where) } function is_area_friendly_to(where, owner) { let save_active = game.active game.active = owner let result = is_friendly_area(where) game.active = save_active return result } function is_london_friendly_to(owner) { return is_area_friendly_to(MIDDLESEX, owner) } function count_lancaster_nobles_and_heirs() { let count = 0 for (let b = 0; b < block_count; ++b) if (block_owner(b) === LANCASTER && (block_type(b) === 'nobles' || block_type(b) === 'church' || block_type(b) === 'heir')) if (is_on_map_not_in_exile_or_man(b)) ++count if (is_london_friendly_to(LANCASTER)) ++count return count } function count_york_nobles_and_heirs() { let count = 0 for (let b = 0; b < block_count; ++b) if (block_owner(b) === YORK && (block_type(b) === 'nobles' || block_type(b) === 'church' || block_type(b) === 'heir')) if (is_on_map_not_in_exile_or_man(b)) ++count if (is_london_friendly_to(YORK)) ++count return count } function block_name(who) { return BLOCKS[who].name } function block_type(who) { return BLOCKS[who].type } function block_home(who) { return BLOCKS[who].home } function block_owner(who) { if (who === B_REBEL) { if (game.pretender !== NOBODY) return block_owner(game.pretender) else if (game.king !== NOBODY) return ENEMY[block_owner(game.king)] else return YORK // whatever... they're both dead } return BLOCKS[who].owner } function block_initiative(who) { if (block_type(who) === 'bombard') return game.battle_round <= 1 ? 'A' : 'D' return BLOCKS[who].initiative } function block_printed_fire_power(who) { return BLOCKS[who].fire_power } function block_fire_power(who, where) { let combat = block_printed_fire_power(who) if (is_defender(who)) { if (is_heir(who) && is_shield_area_for(where, who, true)) ++combat if (is_crown_area(where) && is_senior_royal_heir_in(who, where)) ++combat if (is_noble(who) && is_shield_area_for(where, who, true)) ++combat if (is_church(who) && block_home(who) === has_cathedral(where)) ++combat if (is_levy(who) && block_home(who) === has_city(where)) ++combat if (who === B_WELSH_MERCENARY && is_wales(where)) ++combat } return combat } function is_mercenary(who) { return BLOCKS[who].type === 'mercenaries' } function is_heir(who) { return BLOCKS[who].type === 'heir' } function is_noble(who) { return BLOCKS[who].type === 'nobles' } function is_church(who) { return BLOCKS[who].type === 'church' } function is_levy(who) { return BLOCKS[who].type === 'levies' } function is_rose_noble(who) { return BLOCKS[who].type === 'nobles' && !BLOCKS[who].loyalty } function is_neville(who) { let name = block_name(who) return name === "Warwick" || name === "Kent" || name === "Salisbury" } function block_loyalty(source, target) { let source_name = source !== NOBODY ? block_name(source) : "Event" if (source_name === "Warwick") { let target_name = block_name(target) if (target_name === "Kent" || target_name === "Salisbury") return 1 if (target_name === "Northumberland" || target_name === "Westmoreland") return 0 } return BLOCKS[target].loyalty } function can_defect(source, target) { // Clarence and Exeter can't defect if they are the king or pretender if (target === game.king || target === game.pretender) return false return block_loyalty(source, target) > 0 && !set_has(game.defected, target) } function can_attempt_treason_event() { if (game.treason === game.attacker[game.where]) { for (let b = 0; b < block_count; ++b) if (is_defender(b) && can_defect(NOBODY, b)) return true } else { for (let b = 0; b < block_count; ++b) if (is_attacker(b) && can_defect(NOBODY, b)) return true } return false } function treachery_tag(who) { if (who === game.king) return 'King' if (who === game.pretender) return 'Pretender' if (who === B_WARWICK_L || who === B_WARWICK_Y) return 'Warwick' return game.active } function can_attempt_treachery(who) { let once = treachery_tag(who) if (set_has(game.battle_list, who) && !set_has(game.treachery, once)) { for (let b = 0; b < block_count; ++b) { if (game.active === game.attacker[game.where]) { if (is_defender(b) && can_defect(who, b)) return true } else { if (is_attacker(b) && can_defect(who, b)) return true } } } return false } function block_max_steps(who) { return BLOCKS[who].steps } function can_activate(who) { return block_owner(who) === game.active && !set_has(game.moved, who) && !set_has(game.dead, who) } function is_area_on_map(location) { return location !== NOWHERE && location !== MINOR && location !== POOL } function is_block_on_map(b) { return is_area_on_map(game.location[b]) } function is_block_alive(b) { return is_area_on_map(game.location[b]) && !set_has(game.dead, b) } function border_id(a, b) { return (a < b) ? a * 100 + b : b * 100 + a } function border_was_last_used_by_enemy(from, to) { return map_get(game.last_used, border_id(from, to), 0) === PLAYER_ID[ENEMY[game.active]] } function border_was_last_used_by_active(from, to) { return map_get(game.last_used, border_id(from, to), 0) === PLAYER_ID[game.active] } function border_type(a, b) { return BORDERS[border_id(a,b)] } function border_limit(a, b) { return map_get(game.border_limit, border_id(a,b), 0) } function set_border_limit(a, b, n) { map_set(game.border_limit, border_id(a,b), n) } function reset_border_limits() { game.border_limit.length = 0 } function count_friendly(where) { let p = game.active let count = 0 for (let b = 0; b < block_count; ++b) if (game.location[b] === where && block_owner(b) === p && !set_has(game.dead, b)) ++count return count } function count_enemy(where) { let p = ENEMY[game.active] let count = 0 for (let b = 0; b < block_count; ++b) if (game.location[b] === where && block_owner(b) === p && !set_has(game.dead, b)) ++count return count } function count_enemy_excluding_reserves(where) { let p = ENEMY[game.active] let count = 0 for (let b = 0; b < block_count; ++b) if (game.location[b] === where && block_owner(b) === p) if (!set_has(game.reserves, b)) ++count return count } function is_friendly_area(where) { return is_land_area(where) && count_friendly(where) > 0 && count_enemy(where) === 0 } function is_enemy_area(where) { return is_land_area(where) && count_friendly(where) === 0 && count_enemy(where) > 0 } function is_vacant_area(where) { return is_land_area(where) && count_friendly(where) === 0 && count_enemy(where) === 0 } function is_contested_area(where) { return is_land_area(where) && count_friendly(where) > 0 && count_enemy(where) > 0 } function is_friendly_or_vacant_area(where) { return is_friendly_area(where) || is_vacant_area(where) } function has_city(where) { return AREAS[where].city } function has_cathedral(where) { return AREAS[where].cathedral } function is_crown_area(where) { return AREAS[where].crown } function is_major_port(where) { return AREAS[where].major_port } function is_sea_area(where) { return where === IRISH_SEA || where === NORTH_SEA || where === ENGLISH_CHANNEL } function is_wales(where) { return where === CAERNARVON || where === PEMBROKE || where === POWYS || where === GLAMORGAN } function is_lancaster_exile_area(where) { return where === FRANCE || where === SCOTLAND } function is_york_exile_area(where) { return where === CALAIS || where === IRELAND } function is_exile_area(where) { return is_lancaster_exile_area(where) || is_york_exile_area(where) } function is_friendly_exile_area(where) { return (game.active === LANCASTER) ? is_lancaster_exile_area(where) : is_york_exile_area(where) } function is_enemy_exile_area(where) { return (game.active === YORK) ? is_lancaster_exile_area(where) : is_york_exile_area(where) } function is_pretender_exile_area(where) { return (block_owner(game.pretender) === LANCASTER) ? is_lancaster_exile_area(where) : is_york_exile_area(where) } function can_recruit_to(who, to) { if (who === B_WELSH_MERCENARY) return is_wales(to) && is_friendly_or_vacant_area(to) switch (block_type(who)) { case 'heir': // Not in rulebook, but they can be disbanded to the pool during exile limit check... // Use same rules as entering a minor noble. if (block_owner(who) === block_owner(game.king)) return is_crown_area(to) && is_friendly_or_vacant_area(to) else return is_pretender_exile_area(to) case 'nobles': return is_shield_area_for(to, who, false) && is_friendly_or_vacant_area(to) case 'church': return block_home(who) === has_cathedral(to) && is_friendly_or_vacant_area(to) case 'levies': return block_home(who) === has_city(to) && is_friendly_or_vacant_area(to) case 'bombard': return has_city(to) && is_friendly_area(to) case 'rebel': return !is_exile_area(to) && is_vacant_area(to) } return false } function can_recruit(who) { // Move one group events: if (game.active === game.force_march) return false if (game.active === game.surprise) return false if (game.active === game.treason) return false // Must use AP for sea moves: if (game.active === game.piracy) return false if (can_activate(who) && game.location[who] === POOL) for (let to = first_area; to <= last_area; ++to) if (can_recruit_to(who, to)) return true return false } function have_contested_areas() { for (let where = first_area; where <= last_area; ++where) if (is_area_on_map(where) && is_contested_area(where)) return true return false } function count_pinning(where) { return count_enemy_excluding_reserves(where) } function count_pinned(where) { let count = 0 for (let b = 0; b < block_count; ++b) if (game.location[b] === where && block_owner(b) === game.active) if (!set_has(game.reserves, b)) ++count return count } function is_pinned(who, from) { if (game.active === game.p2) { if (count_pinned(from) <= count_pinning(from)) return true } return false } function can_block_sea_move_to(who, from, to) { if (is_enemy_exile_area(to)) return false if (game.active === game.force_march) return false if (who === B_REBEL || who === B_SCOTS_MERCENARY || who === B_WELSH_MERCENARY) return false if (block_type(who) === 'bombard' || block_type(who) === 'levies') return false if (border_type(from, to) === 'sea') return true return false } function can_block_sea_move(who) { if (can_activate(who)) { let from = game.location[who] if (from !== NOWHERE) { if (is_pinned(who, from)) return false for (let to of AREAS[from].exits) if (can_block_sea_move_to(who, from, to)) return true } } return false } function can_block_use_border(who, from, to) { if (game.active === game.surprise) { switch (border_type(from, to)) { case 'major': return border_limit(from, to) < 5 case 'river': return border_limit(from, to) < 4 case 'minor': return border_limit(from, to) < 3 case 'sea': return false } } else { switch (border_type(from, to)) { case 'major': return border_limit(from, to) < 4 case 'river': return border_limit(from, to) < 3 case 'minor': return border_limit(from, to) < 2 case 'sea': return false } } } function count_borders_crossed(to) { let count = 0 for (let from of AREAS[to].exits) if (border_was_last_used_by_active(from, to)) ++count return count } function can_block_land_move_to(who, from, to) { if (is_enemy_exile_area(to)) return false if (game.active === game.piracy) return false if (can_block_use_border(who, from, to)) { // limit number of borders used to attack/reinforce let contested = is_contested_area(to) if (contested && !border_was_last_used_by_active(from, to)) { // p1 or p2 attacking if (game.attacker[to] === game.active) { if (count_borders_crossed(to) >= 3) return false } if (game.active === game.p2) { // p2 reinforcing battle started by p1 if (game.attacker[to] === game.p1) { if (count_borders_crossed(to) >= 2) return false } } } if (count_pinning(from) > 0) if (border_was_last_used_by_enemy(from, to)) return false return true } return false } function can_block_land_move(who) { if (can_activate(who)) { let from = game.location[who] if (from !== NOWHERE) { if (is_pinned(who, from)) return false for (let to of AREAS[from].exits) if (can_block_land_move_to(who, from, to)) return true } } return false } function can_block_continue(who, from, to) { if (is_contested_area(to)) return false if (border_type(from, to) === 'minor') return false if (game.active === game.force_march) { if (game.distance >= 3) return false } else { if (game.distance >= 2) return false } if (to === game.last_from) return false return true } function can_block_retreat_to(who, to) { if (is_enemy_exile_area(to)) return false if (is_friendly_area(to) || is_vacant_area(to)) { let from = game.location[who] if (can_block_use_border(who, from, to)) { if (border_was_last_used_by_enemy(from, to)) return false return true } } return false } function can_block_regroup_to(who, to) { if (is_enemy_exile_area(to)) return false if (is_friendly_area(to) || is_vacant_area(to)) { let from = game.location[who] if (can_block_use_border(who, from, to)) return true } return false } function can_block_regroup(who) { if (block_owner(who) === game.active) { let from = game.location[who] for (let to of AREAS[from].exits) if (can_block_regroup_to(who, to)) return true } return false } function can_block_muster_via(who, from, next, muster) { if (can_block_land_move_to(who, from, next) && is_friendly_or_vacant_area(next)) { if (next === muster) return true if (border_type(from, next) !== 'minor') { if (set_has(AREAS[next].exits, muster)) if (can_block_land_move_to(who, next, muster)) return true } } return false } function can_block_muster(who, muster) { let from = game.location[who] if (from === muster) return false if (can_activate(who) && is_block_on_map(who)) { if (is_pinned(who, from)) return false for (let next of AREAS[from].exits) if (can_block_muster_via(who, from, next, muster)) return true } return false } function can_muster_to(muster) { for (let b = 0; b < block_count; ++b) if (can_block_muster(b, muster)) return true return false } function is_battle_reserve(who) { return set_has(game.reserves, who) } function is_attacker(who) { if (game.location[who] === game.where && block_owner(who) === game.attacker[game.where] && !set_has(game.dead, who)) return !set_has(game.reserves, who) return false } function is_defender(who) { if (game.location[who] === game.where && block_owner(who) !== game.attacker[game.where] && !set_has(game.dead, who)) return !set_has(game.reserves, who) return false } function swap_blocks(a) { let b = BLOCKS[a].enemy game.location[b] = game.location[a] game.steps[b] = game.steps[a] game.location[a] = NOWHERE game.steps[a] = block_max_steps(a) return b } function disband(who) { game.location[who] = POOL game.steps[who] = block_max_steps(who) } function check_instant_victory() { // Check Clarence/Y and Exeter/L specifically (they're not heirs if converted) if (is_dead(B_YORK) && is_dead(B_MARCH) && is_dead(B_RUTLAND) && is_dead(B_CLARENCE_Y) && is_dead(B_GLOUCESTER)) { log("All York heirs were eliminated!") game.victory = "Lancaster won by eliminating all enemy heirs!" game.result = LANCASTER } if (is_dead(B_HENRY_VI) && is_dead(B_PRINCE_EDWARD) && is_dead(B_EXETER_L) && is_dead(B_SOMERSET) && is_dead(B_RICHMOND)) { log("All Lancaster heirs were eliminated!") game.victory = "York won by eliminating all enemy heirs!" game.result = YORK } } function eliminate_block(who) { log(block_name(who) + " was eliminated.") game.flash += " " + block_name(who) + " was eliminated." if (who === B_EXETER_Y) { game.location[who] = NOWHERE ++game.killed_heirs[LANCASTER] return check_instant_victory() } if (who === B_CLARENCE_L) { game.location[who] = NOWHERE ++game.killed_heirs[YORK] return check_instant_victory() } if (is_heir(who)) { game.location[who] = NOWHERE ++game.killed_heirs[block_owner(who)] if (who === game.pretender) game.pretender = find_senior_heir(block_owner(game.pretender)) // A new King is only crowned in the supply phase. return check_instant_victory() } if (is_rose_noble(who) || is_neville(who)) { game.location[who] = NOWHERE return } if (is_mercenary(who)) { switch (who) { case B_WELSH_MERCENARY: game.location[who] = POOL; break case B_IRISH_MERCENARY: game.location[who] = IRELAND; break case B_BURGUNDIAN_MERCENARY: game.location[who] = CALAIS; break case B_CALAIS_MERCENARY: game.location[who] = CALAIS; break case B_SCOTS_MERCENARY: game.location[who] = SCOTLAND; break case B_FRENCH_MERCENARY: game.location[who] = FRANCE; break } game.steps[who] = block_max_steps(who) set_add(game.dead, who) return } game.location[who] = POOL game.steps[who] = block_max_steps(who) set_add(game.dead, who) } function reduce_block(who) { if (game.steps[who] === 1) { eliminate_block(who) } else { --game.steps[who] } } function count_attackers() { let count = 0 for (let b = 0; b < block_count; ++b) if (is_attacker(b)) ++count return count } function count_defenders() { let count = 0 for (let b = 0; b < block_count; ++b) if (is_defender(b)) ++count return count } function count_blocks_exclude_mercenaries(where) { let count = 0 for (let b = 0; b < block_count; ++b) if (game.location[b] === where && !is_mercenary(b) && !(game.reduced && set_has(game.reduced, b))) ++count return count } function count_blocks(where) { let count = 0 for (let b = 0; b < block_count; ++b) if (game.location[b] === where && !(game.reduced && set_has(game.reduced, b))) ++count return count } function add_blocks_exclude_mercenaries(list, where) { for (let b = 0; b < block_count; ++b) if (game.location[b] === where && !is_mercenary(b) && !(game.reduced && set_has(game.reduced, b))) set_add(list, b) } function add_blocks(list, where) { for (let b = 0; b < block_count; ++b) if (game.location[b] === where && !(game.reduced && set_has(game.reduced, b))) set_add(list, b) } function check_supply_penalty() { game.supply = [] for (let where = first_area; where <= last_area; ++where) { if (is_friendly_area(where)) { if (where === CALAIS || where === FRANCE) { if (count_blocks_exclude_mercenaries(where) > 4) add_blocks_exclude_mercenaries(game.supply, where) } else if (where === IRELAND || where === SCOTLAND) { if (count_blocks_exclude_mercenaries(where) > 2) add_blocks_exclude_mercenaries(game.supply, where) } else if (has_city(where)) { if (count_blocks(where) > 5) add_blocks(game.supply, where) } else { if (count_blocks(where) > 4) add_blocks(game.supply, where) } } } return game.supply.length > 0 } function check_exile_limits() { game.exiles = [] for (let where = first_area; where <= last_area; ++where) { if (is_friendly_area(where)) { if (where === CALAIS || where === FRANCE) { if (count_blocks_exclude_mercenaries(where) > 4) add_blocks_exclude_mercenaries(game.exiles, where) } else if (where === IRELAND || where === SCOTLAND) { if (count_blocks_exclude_mercenaries(where) > 2) add_blocks_exclude_mercenaries(game.exiles, where) } } } if (game.exiles.length > 0) return true delete game.exiles return false } // SETUP function find_block(owner, name) { if (name in block_index) return block_index[name] name = name + "/" + owner[0] if (name in block_index) return block_index[name] throw new Error("Block not found: " + name) } function deploy(who, where) { if (where === "Enemy") return if (!(where in area_index)) throw new Error("Area not found: " + where) game.location[who] = area_index[where] game.steps[who] = BLOCKS[who].steps } function deploy_lancaster(name, where) { deploy(find_block(LANCASTER, name), where) } function deploy_york(name, where) { deploy(find_block(YORK, name), where) } function reset_blocks() { for (let b = 0; b < block_count; ++b) { game.location[b] = NOWHERE game.steps[b] = block_max_steps(b) } } function setup_game() { reset_blocks() game.campaign = 1 game.end_campaign = 3 game.pretender = B_YORK game.king = B_HENRY_VI deploy_lancaster("Henry VI", "Middlesex") deploy_lancaster("Somerset", "Dorset") deploy_lancaster("Exeter", "Cornwall") deploy_lancaster("Devon", "Cornwall") deploy_lancaster("Pembroke", "Pembroke") deploy_lancaster("Wiltshire", "Wilts") deploy_lancaster("Oxford", "Essex") deploy_lancaster("Beaumont", "Lincoln") deploy_lancaster("Clifford", "North Yorks") deploy_lancaster("French Mercenary", "France") deploy_lancaster("Scots Mercenary", "Scotland") deploy_lancaster("Buckingham", "Pool") deploy_lancaster("Northumberland", "Pool") deploy_lancaster("Shrewsbury", "Pool") deploy_lancaster("Westmoreland", "Pool") deploy_lancaster("Rivers", "Pool") deploy_lancaster("Stanley", "Pool") deploy_lancaster("Bristol (levy)", "Pool") deploy_lancaster("Coventry (levy)", "Pool") deploy_lancaster("Newcastle (levy)", "Pool") deploy_lancaster("York (levy)", "Pool") deploy_lancaster("York (church)", "Pool") deploy_lancaster("Bombard", "Pool") deploy_lancaster("Welsh Mercenary", "Pool") deploy_lancaster("Prince Edward", "Minor") deploy_lancaster("Richmond", "Minor") deploy_lancaster("Canterbury (church)", "Enemy") deploy_lancaster("Clarence", "Enemy") deploy_lancaster("Warwick", "Enemy") deploy_lancaster("Salisbury", "Enemy") deploy_lancaster("Kent", "Enemy") deploy_york("York", "Ireland") deploy_york("Rutland", "Ireland") deploy_york("Irish Mercenary", "Ireland") deploy_york("March", "Calais") deploy_york("Warwick", "Calais") deploy_york("Salisbury", "Calais") deploy_york("Kent", "Calais") deploy_york("Calais Mercenary", "Calais") deploy_york("Burgundian Mercenary", "Calais") deploy_york("Norfolk", "Pool") deploy_york("Suffolk", "Pool") deploy_york("Arundel", "Pool") deploy_york("Essex", "Pool") deploy_york("Worcester", "Pool") deploy_york("Hastings", "Pool") deploy_york("Herbert", "Pool") deploy_york("Canterbury (church)", "Pool") deploy_york("London (levy)", "Pool") deploy_york("Norwich (levy)", "Pool") deploy_york("Salisbury (levy)", "Pool") deploy_york("Bombard", "Pool") deploy_york("Rebel", "Pool") deploy_york("Clarence", "Minor") deploy_york("Gloucester", "Minor") deploy_york("Exeter", "Enemy") deploy_york("Buckingham", "Enemy") deploy_york("Northumberland", "Enemy") deploy_york("Westmoreland", "Enemy") deploy_york("Shrewsbury", "Enemy") deploy_york("Rivers", "Enemy") deploy_york("Stanley", "Enemy") deploy_york("York (church)", "Enemy") } function setup_kingmaker() { reset_blocks() game.campaign = 2 game.end_campaign = 2 game.pretender = B_HENRY_VI game.king = B_MARCH deploy_york("March", "Middlesex") deploy_york("Gloucester", "South Yorks") deploy_york("Buckingham", "Warwick") deploy_york("Norfolk", "East Anglia") deploy_york("Suffolk", "East Anglia") deploy_york("Arundel", "Sussex") deploy_york("Essex", "Essex") deploy_york("Hastings", "Leicester") deploy_york("Rivers", "Leicester") deploy_york("Stanley", "Lancaster") deploy_york("Irish Mercenary", "Ireland") deploy_york("Calais Mercenary", "Calais") deploy_york("Burgundian Mercenary", "Calais") deploy_york("Northumberland", "Pool") deploy_york("Westmoreland", "Pool") deploy_york("Canterbury (church)", "Pool") deploy_york("Bombard", "Pool") deploy_york("London (levy)", "Pool") deploy_york("Norwich (levy)", "Pool") deploy_york("Salisbury (levy)", "Pool") deploy_york("Warwick", "Enemy") deploy_york("Clarence", "Enemy") deploy_york("Shrewsbury", "Enemy") deploy_york("York (church)", "Enemy") deploy_york("Exeter", "Enemy") deploy_lancaster("Henry VI", "Middlesex") deploy_lancaster("Prince Edward", "France") deploy_lancaster("Exeter", "France") deploy_lancaster("Warwick", "France") deploy_lancaster("Clarence", "France") deploy_lancaster("Oxford", "France") deploy_lancaster("French Mercenary", "France") deploy_lancaster("Scots Mercenary", "Scotland") deploy_lancaster("Pembroke", "Pool") deploy_lancaster("Shrewsbury", "Pool") deploy_lancaster("York (church)", "Pool") deploy_lancaster("Welsh Mercenary", "Pool") deploy_lancaster("Bombard", "Pool") deploy_lancaster("Bristol (levy)", "Pool") deploy_lancaster("Coventry (levy)", "Pool") deploy_lancaster("Newcastle (levy)", "Pool") deploy_lancaster("York (levy)", "Pool") deploy_lancaster("Rebel", "Pool") deploy_lancaster("Richmond", "Minor") deploy_lancaster("Buckingham", "Enemy") deploy_lancaster("Northumberland", "Enemy") deploy_lancaster("Rivers", "Enemy") deploy_lancaster("Westmoreland", "Enemy") deploy_lancaster("Stanley", "Enemy") deploy_lancaster("Canterbury (church)", "Enemy") // Prisoner! set_add(game.dead, B_HENRY_VI) } function setup_richard_iii() { reset_blocks() game.campaign = 3 game.end_campaign = 3 game.pretender = B_RICHMOND game.king = B_GLOUCESTER deploy_york("Gloucester", "Middlesex") deploy_york("Norfolk", "East Anglia") deploy_york("Suffolk", "East Anglia") deploy_york("Arundel", "Sussex") deploy_york("Essex", "Essex") deploy_york("Northumberland", "Northumbria") deploy_york("Stanley", "Lancaster") deploy_york("Irish Mercenary", "Ireland") deploy_york("Calais Mercenary", "Calais") deploy_york("Burgundian Mercenary", "Calais") deploy_york("Westmoreland", "Pool") deploy_york("Canterbury (church)", "Pool") deploy_york("York (church)", "Pool") deploy_york("Bombard", "Pool") deploy_york("London (levy)", "Pool") deploy_york("Norwich (levy)", "Pool") deploy_york("Salisbury (levy)", "Pool") deploy_york("Buckingham", "Enemy") deploy_york("Shrewsbury", "Enemy") deploy_york("Rivers", "Enemy") deploy_lancaster("Richmond", "France") deploy_lancaster("Oxford", "France") deploy_lancaster("Pembroke", "France") deploy_lancaster("French Mercenary", "France") deploy_lancaster("Scots Mercenary", "Scotland") deploy_lancaster("Buckingham", "Glamorgan") deploy_lancaster("Rivers", "Leicester") deploy_lancaster("Shrewsbury", "Pool") deploy_lancaster("Welsh Mercenary", "Pool") deploy_lancaster("Bombard", "Pool") deploy_lancaster("Bristol (levy)", "Pool") deploy_lancaster("Coventry (levy)", "Pool") deploy_lancaster("Newcastle (levy)", "Pool") deploy_lancaster("York (levy)", "Pool") deploy_lancaster("Rebel", "Pool") deploy_lancaster("Northumberland", "Enemy") deploy_lancaster("Westmoreland", "Enemy") deploy_lancaster("Stanley", "Enemy") deploy_lancaster("Canterbury (church)", "Enemy") deploy_lancaster("York (church)", "Enemy") } // Kingmaker scenario special rule function free_henry_vi() { if (set_has(game.dead, B_HENRY_VI)) { if ((game.active === LANCASTER && is_friendly_area(MIDDLESEX)) || (game.active === YORK && is_enemy_area(MIDDLESEX))) { log("Henry VI rescued!") set_delete(game.dead, B_HENRY_VI) } } } // GAME TURN function start_campaign() { logbr() log(".h1 Campaign " + game.campaign) logbr() // TODO: Use board game mulligan rules instead of automatically redealing? do { let deck = shuffle_deck() game.l_hand = deal_cards(deck, 7) game.y_hand = deal_cards(deck, 7) } while (count_ap(game.l_hand) <= 13 || count_ap(game.y_hand) <= 13) start_game_turn() } function start_game_turn() { game.turn = 8 - game.l_hand.length logbr() log(".h1 Turn " + game.turn + " of campaign " + game.campaign + ".") logbr() // Reset movement and attack tracking state reset_border_limits() game.last_used = [] game.attacker = {} set_clear(game.reserves) set_clear(game.moved) goto_card_phase() } function end_game_turn() { delete game.force_march delete game.piracy delete game.is_pirate delete game.surprise delete game.treason if (game.l_hand.length > 0) start_game_turn() else goto_political_turn() } // CARD PHASE function goto_card_phase() { game.l_card = 0 game.y_card = 0 game.show_cards = false game.state = 'play_card' game.active = BOTH } function resume_play_card() { if (game.l_card > 0 && game.y_card > 0) reveal_cards() else if (game.l_card > 0) game.active = YORK else if (game.y_card > 0) game.active = LANCASTER else game.active = BOTH } states.play_card = { prompt: function (view, current) { if (current === OBSERVER) return view.prompt = "Waiting for players to play a card." if (current === LANCASTER) { if (game.l_card) { view.prompt = "Waiting for York to play a card." } else { view.prompt = "Play a card." for (let c of game.l_hand) gen_action(view, 'play', c) } } if (current === YORK) { if (game.y_card) { view.prompt = "Waiting for Lancaster to play a card." } else { view.prompt = "Play a card." for (let c of game.y_hand) gen_action(view, 'play', c) } } }, play: function (card, current) { if (current === LANCASTER) { remove_from_array(game.l_hand, card) game.l_card = card } if (current === YORK) { remove_from_array(game.y_hand, card) game.y_card = card } resume_play_card() }, undo: function (_, current) { if (current === LANCASTER) { game.l_hand.push(game.l_card) game.l_card = 0 } if (current === YORK) { game.y_hand.push(game.y_card) game.y_card = 0 } resume_play_card() } } function reveal_cards() { logbr() log("Lancaster played " + CARDS[game.l_card].name + ".") log("York played " + CARDS[game.y_card].name + ".") game.show_cards = true let pretender = block_owner(game.pretender) let lc = CARDS[game.l_card] let yc = CARDS[game.y_card] let lp = (lc.event ? 10 : 0) + lc.actions * 2 + (pretender === LANCASTER ? 1 : 0) let yp = (yc.event ? 10 : 0) + yc.actions * 2 + (pretender === YORK ? 1 : 0) if (lp > yp) { game.p1 = LANCASTER game.p2 = YORK } else { game.p1 = YORK game.p2 = LANCASTER } game.active = game.p1 start_player_turn() } function start_player_turn() { logbr() log(".h2 " + game.active) reset_border_limits() let lc = CARDS[game.l_card] let yc = CARDS[game.y_card] if (game.active === LANCASTER && lc.event) goto_event_card(lc.event) else if (game.active === YORK && yc.event) goto_event_card(yc.event) else if (game.active === LANCASTER) goto_action_phase(lc.actions) else if (game.active === YORK) goto_action_phase(yc.actions) } function end_player_turn() { game.moves = 0 game.activated = null game.move_port = null game.main_border = null // Remove "Surprise" road limit bonus for retreats and regroups. delete game.surprise if (game.active === game.p2) { goto_battle_phase() } else { game.active = game.p2 start_player_turn() } } // EVENTS function goto_event_card(event) { switch (event) { case 'force_march': game.force_march = game.active goto_action_phase(1) break case 'muster': goto_muster_event() break case 'piracy': game.piracy = game.active game.is_pirate = [] goto_action_phase(2) break case 'plague': game.state = 'plague_event' break case 'surprise': game.surprise = game.active goto_action_phase(1) break case 'treason': game.treason = game.active goto_action_phase(1) break } } states.plague_event = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Plague: Waiting for " + game.active + " to choose a city." view.prompt = "Plague: Choose an enemy city area." gen_action(view, 'pass') for (let where = first_area; where <= last_area; ++where) if (is_enemy_area(where) && has_city(where)) gen_action(view, 'area', where) }, area: function (where) { log("Plague ravaged " + has_city(where) + "!") game.where = where game.plague = [] for (let b = 0; b < block_count; ++b) if (game.location[b] === where) set_add(game.plague, b) game.active = ENEMY[game.active] game.state = 'apply_plague' }, pass: function () { end_player_turn() } } states.apply_plague = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Plague: Waiting for " + game.active + " to reduce blocks in " + has_city(game.where) + "." view.prompt = "Plague: Reduce blocks in " + has_city(game.where) + "." for (let b of game.plague) gen_action(view, 'block', b) }, block: function (b) { reduce_block(b) set_delete(game.plague, b) if (game.plague.length === 0) { delete game.plague game.active = ENEMY[game.active] game.where = NOWHERE end_player_turn() } }, } function goto_muster_event() { game.state = 'muster_event' game.turn_log = [] clear_undo() } states.muster_event = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Muster: Waiting for " + game.active + " to muster." view.prompt = "Muster: Choose one friendly or vacant muster area." gen_action_undo(view) gen_action(view, 'end_action_phase') for (let where = first_area; where <= last_area; ++where) { if (is_friendly_or_vacant_area(where)) if (can_muster_to(where)) gen_action(view, 'area', where) } }, area: function (where) { push_undo() game.where = where game.state = 'muster_who' }, end_action_phase: function () { clear_undo() print_turn_log(game.active + " mustered:") end_player_turn() }, undo: pop_undo, } states.muster_who = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Muster: Waiting for " + game.active + " to muster." view.prompt = "Muster: Move blocks to the designated muster area." gen_action_undo(view) gen_action(view, 'end_action_phase') for (let b = 0; b < block_count; ++b) if (can_block_muster(b, game.where)) gen_action(view, 'block', b) }, block: function (who) { push_undo() game.who = who game.state = 'muster_move_1' }, end_action_phase: function () { game.where = NOWHERE clear_undo() print_turn_log(game.active + " mustered:") end_player_turn() }, undo: pop_undo, } states.muster_move_1 = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Muster: Waiting for " + game.active + " to muster." view.prompt = "Muster: Move " + block_name(game.who) + " to the designated muster area." gen_action_undo(view) gen_action(view, 'block', game.who) let from = game.location[game.who] for (let to of AREAS[from].exits) { if (can_block_muster_via(game.who, from, to, game.where)) gen_action(view, 'area', to) } }, area: function (to) { let from = game.location[game.who] log_move_start(from) log_move_continue(to) move_block(game.who, from, to) if (to === game.where) { log_move_end() set_add(game.moved, game.who) game.who = NOBODY game.state = 'muster_who' } else { game.state = 'muster_move_2' } }, block: pop_undo, undo: pop_undo, } states.muster_move_2 = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Muster: Waiting for " + game.active + " to muster." view.prompt = "Muster: Move " + block_name(game.who) + " to the designated muster area." gen_action_undo(view) gen_action(view, 'area', game.where) }, area: function (to) { log_move_continue(to) log_move_end() move_block(game.who, game.location[game.who], to) set_add(game.moved, game.who) game.who = NOBODY game.state = 'muster_who' }, undo: pop_undo, } // ACTION PHASE function use_border(from, to) { set_border_limit(from, to, border_limit(from, to) + 1) } function move_block(who, from, to) { game.location[who] = to use_border(from, to) game.distance ++ if (is_contested_area(to)) { map_set(game.last_used, border_id(from, to), PLAYER_ID[game.active]) if (!game.attacker[to]) { game.attacker[to] = game.active map_set(game.main_border, to, from) } else { if (game.attacker[to] !== game.active || map_get(game.main_border, to, 0) !== from) { set_add(game.reserves, who) return RESERVE_MARK } } return ATTACK_MARK } return "" } function goto_action_phase(moves) { game.state = 'action_phase' game.moves = moves game.activated = [] game.move_port = [] game.main_border = [] game.turn_log = [] game.recruit_log = [] clear_undo() } states.action_phase = { prompt: function (view, current) { if (is_inactive_player(current)) { if (game.active === game.piracy) return view.prompt = "Piracy: Waiting for " + game.active + "." if (game.active === game.force_march) return view.prompt = "Force March: Waiting for " + game.active + "." if (game.active === game.surprise) return view.prompt = "Surprise: Waiting for " + game.active + "." if (game.active === game.treason) return view.prompt = "Treason: Waiting for " + game.active + "." else return view.prompt = "Action Phase: Waiting for " + game.active + "." } if (game.active === game.piracy) { view.prompt = "Piracy: Choose an army to sea move. Attacking is allowed. " + game.moves + "AP left." } else if (game.active === game.force_march) { view.prompt = "Force March: Move one group. Blocks can move up to 3 areas and may attack." } else if (game.active === game.surprise) { view.prompt = "Surprise: Move one group. Border limit is +1 to cross all borders." } else if (game.active === game.treason) { view.prompt = "Treason: Move one group." } else { view.prompt = "Action Phase: Choose an army to move or recruit. " + game.moves + "AP left." } gen_action_undo(view) gen_action(view, 'end_action_phase') for (let b = 0; b < block_count; ++b) { let from = game.location[b] if (can_recruit(b)) { if (game.moves > 0) gen_action(view, 'block', b) } if (can_block_land_move(b)) { if (game.moves === 0) { if (set_has(game.activated, from)) gen_action(view, 'block', b) } else { gen_action(view, 'block', b) } } if (can_block_sea_move(b)) { if (game.moves === 0) { if (map_has(game.move_port, game.location[b])) gen_action(view, 'block', b) } else { gen_action(view, 'block', b) } } } }, block: function (who) { push_undo() game.who = who game.origin = game.location[who] if (game.origin === POOL) { game.state = 'recruit_where' } else { game.distance = 0 game.last_from = NOWHERE game.state = 'move_to' } }, end_action_phase: function () { if (game.moves > 0 && game.turn_log.length === 0 && game.recruit_log.length === 0) logp("did nothing.") if (game.turn_log.length > 0) { print_turn_log(game.active + " moved:") } game.turn_log = game.recruit_log if (game.turn_log.length > 0) { print_turn_log(game.active + " recruited:") } game.turn_log = null game.recruit_log = null clear_undo() game.moves = 0 end_player_turn() }, undo: pop_undo, } states.recruit_where = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Action Phase: Waiting for " + game.active + "." view.prompt = "Recruit " + block_name(game.who) + " where?" gen_action_undo(view) gen_action(view, 'block', game.who) for (let to = first_area; to <= last_area; ++to) if (can_recruit_to(game.who, to)) gen_action(view, 'area', to) }, area: function (to) { game.recruit_log.push(["#" + to]) --game.moves game.location[game.who] = to set_add(game.moved, game.who) end_action() }, block: pop_undo, undo: pop_undo, } states.move_to = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Action Phase: Waiting for " + game.active + "." view.prompt = "Move " + block_name(game.who) + "." gen_action_undo(view) gen_action(view, 'block', game.who) let from = game.location[game.who] if (game.distance > 0) gen_action(view, 'area', from) for (let to of AREAS[from].exits) { if (to !== game.last_from && can_block_land_move_to(game.who, from, to)) gen_action(view, 'area', to) else if (game.distance === 0 && can_block_sea_move_to(game.who, from, to)) { let has_destination_port = false if (game.moves === 0) { for (let port of AREAS[to].exits) if (map_get(game.move_port, game.origin) === port) has_destination_port = true } else { if (game.active === game.piracy) has_destination_port = true else for (let port of AREAS[to].exits) if (port !== game.origin && is_friendly_or_vacant_area(port)) has_destination_port = true } if (has_destination_port) gen_action(view, 'area', to) } } }, block: function () { if (game.distance === 0) pop_undo() else end_move() }, area: function (to) { let from = game.location[game.who] if (to === from) { end_move() return } if (game.distance === 0) log_move_start(from) game.last_from = from if (is_sea_area(to)) { // XXX use_border(from, to) // if displaying sea moves log_move_continue(to) game.location[game.who] = to game.state = 'sea_move_to' } else { let mark = move_block(game.who, from, to) log_move_continue(to, mark) if (!can_block_continue(game.who, from, to)) end_move() } }, undo: pop_undo, } states.sea_move_to = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Action Phase: Waiting for " + game.active + "." if (game.active === game.piracy) { view.prompt = "Piracy: Sea Move " + block_name(game.who) + " to a coastal area." } else { view.prompt = "Sea Move " + block_name(game.who) + " to a friendly or vacant coastal area." } gen_action_undo(view) for (let to of AREAS[game.location[game.who]].exits) { if (to === game.last_from) continue if (is_enemy_exile_area(to)) continue if (is_friendly_or_vacant_area(to)) { if (game.moves === 0) { if (map_get(game.move_port, game.origin) === to) gen_action(view, 'area', to) } else { gen_action(view, 'area', to) } } else if (game.active === game.piracy && game.moves > 0) { // Can attack with piracy, but no port-to-port bonus. gen_action(view, 'area', to) } } }, area: function (to) { // XXX use_border(game.location[game.who], to) // if displaying sea moves game.location[game.who] = to set_add(game.moved, game.who) if (game.active === game.piracy && is_contested_area(to)) { // Can attack with piracy, but no port-to-port bonus. log_move_continue(to, ATTACK_MARK) set_add(game.is_pirate, game.who) if (!game.attacker[to]) game.attacker[to] = game.active logp("sea moved.") --game.moves } else { // Can sea move two blocks between same major ports for 1 AP. log_move_continue(to) if (map_get(game.move_port, game.origin) === to) { map_delete(game.move_port, game.origin) } else { logp("sea moved.") --game.moves if (is_major_port(game.origin) && is_major_port(to)) map_set(game.move_port, game.origin, to) } } log_move_end() end_action() }, undo: pop_undo, } function end_move() { if (game.distance > 0) { log_move_end() if (!set_has(game.activated, game.origin)) { logp("activated #" + game.origin + ".") set_add(game.activated, game.origin) game.moves -- } set_add(game.moved, game.who) } game.last_from = NOWHERE end_action() } function end_action() { free_henry_vi() game.who = NOBODY game.distance = 0 game.origin = NOWHERE game.state = 'action_phase' } // BATTLE PHASE function goto_battle_phase() { reset_border_limits() if (have_contested_areas()) { game.active = game.p1 game.state = 'battle_phase' } else { goto_supply_phase() } } states.battle_phase = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for " + game.active + " to choose a battle." view.prompt = "Choose the next battle to fight!" for (let where = first_area; where <= last_area; ++where) if (is_area_on_map(where) && is_contested_area(where)) gen_action(view, 'area', where) }, area: function (where) { start_battle(where) }, } function start_battle(where) { game.flash = "" logbr() log(".h3 Battle in #" + where) game.where = where game.battle_round = 0 game.defected = [] game.treachery = [] if (game.treason && can_attempt_treason_event()) { game.active = game.treason game.state = 'treason_event' } else { game.state = 'battle_round' start_battle_round() } } function resume_battle() { if (game.result) return goto_game_over() game.who = NOBODY game.state = 'battle_round' pump_battle_round() } function end_battle() { if (game.turn_log && game.turn_log.length > 0) print_turn_log("Retreated from #" + game.where + ":") free_henry_vi() game.flash = "" game.battle_round = 0 reset_border_limits() set_clear(game.moved) game.defected = [] game.treachery = [] goto_regroup() } states.treason_event = { show_battle: true, prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Treason: Waiting for " + game.active + " to choose a target." view.prompt = "Treason: Choose a target or pass." gen_action(view, 'pass') for (let b = 0; b < block_count; ++b) { if (game.active === game.attacker[game.where]) { if (is_defender(b) && can_defect(NOBODY, b)) { gen_action(view, 'battle_treachery', b) gen_action(view, 'block', b) } } else { if (is_attacker(b) && can_defect(NOBODY, b)) { gen_action(view, 'battle_treachery', b) gen_action(view, 'block', b) } } } }, battle_treachery: function (target) { delete game.treason attempt_treachery(NOBODY, target) game.state = 'battle_round' start_battle_round() }, block: function (target) { delete game.treason attempt_treachery(NOBODY, target) game.state = 'battle_round' start_battle_round() }, pass: function () { game.state = 'battle_round' start_battle_round() } } function bring_on_reserves(owner, moved) { for (let b = 0; b < block_count; ++b) { if (block_owner(b) === owner && game.location[b] === game.where) { set_delete(game.reserves, b) if (moved) set_add(game.moved, b) else set_delete(game.moved, b) } } } function start_battle_round() { if (++game.battle_round <= 4) { if (game.turn_log && game.turn_log.length > 0) print_turn_log("Retreated from #" + game.where + ":") game.turn_log = [] log(".h4 Battle Round " + game.battle_round) reset_border_limits() set_clear(game.moved) if (game.battle_round > 1) { bring_on_reserves(LANCASTER, false) bring_on_reserves(YORK, false) } pump_battle_round() } else { end_battle() } } function pump_battle_round() { if (is_friendly_area(game.where) || is_enemy_area(game.where)) { end_battle() return } if (count_attackers() === 0 || count_defenders() === 0) { // Deploy reserves immediately if all blocks on one side are eliminated. if (count_attackers() === 0) { log("Attacking main force was eliminated.") bring_on_reserves(game.attacker[game.where], true) } else if (count_defenders() === 0) { log("Defending main force was eliminated.") bring_on_reserves(ENEMY[game.attacker[game.where]], true) if (game.battle_round === 1) { log("Battlefield control changed.") game.attacker[game.where] = ENEMY[game.attacker[game.where]] } } } function filter_battle_blocks(ci, is_candidate) { let output = null for (let b = 0; b < block_count; ++b) { if (is_candidate(b) && !set_has(game.moved, b) && !set_has(game.dead, b)) { if (block_initiative(b) === ci) { if (!output) output = [] set_add(output, b) } } } return output } function battle_step(active, initiative, candidate) { game.battle_list = filter_battle_blocks(initiative, candidate) if (game.battle_list) { game.active = active return true } return false } let attacker = game.attacker[game.where] let defender = ENEMY[attacker] if (battle_step(defender, 'A', is_defender)) return if (battle_step(attacker, 'A', is_attacker)) return if (battle_step(defender, 'B', is_defender)) return if (battle_step(attacker, 'B', is_attacker)) return if (battle_step(defender, 'C', is_defender)) return if (battle_step(attacker, 'C', is_attacker)) return if (battle_step(defender, 'D', is_defender)) return if (battle_step(attacker, 'D', is_attacker)) return start_battle_round() } function pass_with_block(b) { game.flash = block_name(b) + " passed." log_battle(game.flash) set_add(game.moved, b) resume_battle() } function can_retreat_with_block(who) { if (game.location[who] === game.where) { if (game.battle_round > 1) { if (game.active === game.piracy && set_has(game.is_pirate, who)) { return true } else { for (let to of AREAS[game.where].exits) if (can_block_retreat_to(who, to)) return true } } } return false } function must_retreat_with_block(who) { if (game.location[who] === game.where) if (game.battle_round === 4) return (block_owner(who) === game.attacker[game.where]) return false } function retreat_with_block(who) { if (can_retreat_with_block(who)) { game.who = who game.state = 'retreat_in_battle' } else { eliminate_block(who) resume_battle() } } function roll_attack(active, b, verb) { game.hits = 0 let fire = block_fire_power(b, game.where) let printed_fire = block_printed_fire_power(b) let rolls = [] let steps = game.steps[b] let name = block_name(b) + " " + BLOCKS[b].combat if (fire > printed_fire) name += "+" + (fire - printed_fire) for (let i = 0; i < steps; ++i) { let die = roll_d6() if (die <= fire) { rolls.push(DIE_HIT[die]) ++game.hits } else { rolls.push(DIE_MISS[die]) } } game.flash = name + " " + verb + " " + rolls.join(" ") + " " if (game.hits === 0) game.flash += "and missed." else if (game.hits === 1) game.flash += "and scored 1 hit." else game.flash += "and scored " + game.hits + " hits." log(active[0] + ": " + name + " " + verb + " " + rolls.join("") + ".") } function fire_with_block(b) { set_add(game.moved, b) roll_attack(game.active, b, "fired") if (game.hits > 0) { game.active = ENEMY[game.active] goto_battle_hits() } else { resume_battle() } } function attempt_treachery(source, target) { if (source !== NOBODY) { let once = treachery_tag(source) set_add(game.treachery, once) set_add(game.moved, source) } let n = block_loyalty(source, target) let rolls = [] let result = true for (let i = 0; i < n; ++i) { let die = roll_d6() if ((die & 1) === 1) { rolls.push(DIE_MISS[die]) result = false } else { rolls.push(DIE_HIT[die]) } } if (source !== NOBODY) game.flash = block_name(source) + " treachery " + rolls.join(" ") else game.flash = "Treason event " + rolls.join(" ") if (result) { game.flash += " converted " + block_name(target) + "!" target = swap_blocks(target) set_add(game.defected, target) set_add(game.reserves, target) } else { game.flash += " failed to convert " + block_name(target) + "." } log_battle(game.flash) } function charge_with_block(heir, target) { let n set_add(game.moved, heir) roll_attack(game.active, heir, "charged " + block_name(target)) n = Math.min(game.hits, game.steps[target]) if (n === game.steps[target]) { eliminate_block(target) } else { while (n-- > 0) reduce_block(target) let charge_flash = game.flash roll_attack(ENEMY[game.active], target, "counter-attacked") n = Math.min(game.hits, game.steps[heir]) while (n-- > 0) reduce_block(heir) game.flash = charge_flash + " " + game.flash } resume_battle() } function can_block_fire(who) { if (is_attacker(who)) return game.battle_round < 4 if (is_defender(who)) return true return false } function find_minor_heir(owner) { let candidate = NOBODY for (let b = 0; b < block_count; ++b) { if (block_owner(b) === owner && block_type(b) === 'heir' && game.location[b] === MINOR) if (candidate === NOBODY || BLOCKS[b].heir < BLOCKS[candidate].heir) candidate = b } return candidate } function find_senior_heir(owner) { let candidate = NOBODY for (let b = 0; b < block_count; ++b) if (block_owner(b) === owner && block_type(b) === 'heir' && !is_dead(b)) if (candidate === NOBODY || BLOCKS[b].heir < BLOCKS[candidate].heir) candidate = b return candidate } function find_next_king(owner) { let candidate = NOBODY for (let b = 0; b < block_count; ++b) if (block_owner(b) === owner && block_type(b) === 'heir' && game.location[b] !== NOWHERE) if (candidate === NOBODY || BLOCKS[b].heir < BLOCKS[candidate].heir) candidate = b return candidate } function find_senior_heir_in_area(owner, where) { let candidate = NOBODY for (let b = 0; b < block_count; ++b) { if (block_owner(b) === owner && block_type(b) === 'heir' && game.location[b] === where) { if (is_battle_reserve(b)) continue if (candidate === NOBODY || BLOCKS[b].heir < BLOCKS[candidate].heir) candidate = b } } return candidate } function is_senior_royal_heir_in(who, where) { return find_senior_heir_in_area(block_owner(game.king), where) === who } function can_heir_charge() { let heir = find_senior_heir_in_area(game.active, game.where) if (heir !== NOBODY && !set_has(game.moved, heir)) { if (is_attacker(heir)) return game.battle_round < 4 ? heir : NOBODY if (is_defender(heir)) return heir } return NOBODY } states.battle_round = { show_battle: true, prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for " + game.active + " to choose a combat action." view.prompt = "Battle: Choose a combat action with an army." let can_fire = false let can_retreat = false let must_retreat = false let can_pass = false if (game.active === game.attacker[game.where]) { if (game.battle_round < 4) can_fire = true if (game.battle_round > 1) can_retreat = true if (game.battle_round < 4) can_pass = true if (game.battle_round === 4) must_retreat = true } else { can_fire = true if (game.battle_round > 1) can_retreat = true can_pass = true } for (let b of game.battle_list) { if (can_fire) gen_action(view, 'battle_fire', b) if (must_retreat || (can_retreat && can_retreat_with_block(b))) gen_action(view, 'battle_retreat', b) if (can_pass) gen_action(view, 'battle_pass', b) gen_action(view, 'block', b) } if (!must_retreat) { let heir = can_heir_charge() if (heir !== NOBODY && set_has(game.battle_list, heir)) gen_action(view, 'battle_charge', heir) if (can_attempt_treachery(game.king)) gen_action(view, 'battle_treachery', game.king) if (can_attempt_treachery(game.pretender)) gen_action(view, 'battle_treachery', game.pretender) if (can_attempt_treachery(B_WARWICK_L)) gen_action(view, 'battle_treachery', B_WARWICK_L) if (can_attempt_treachery(B_WARWICK_Y)) gen_action(view, 'battle_treachery', B_WARWICK_Y) } }, battle_fire: function (who) { fire_with_block(who) }, battle_retreat: function (who) { retreat_with_block(who) }, battle_pass: function (who) { pass_with_block(who) }, battle_charge: function (who) { game.who = who game.state = 'battle_charge' }, battle_treachery: function (who) { game.who = who game.state = 'battle_treachery' }, block: function (who) { if (can_block_fire(who)) fire_with_block(who) else if (can_retreat_with_block(who)) retreat_with_block(who) else if (must_retreat_with_block(who)) { eliminate_block(who) resume_battle() } else pass_with_block(who) }, } states.battle_charge = { show_battle: true, prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Heir Charge: Waiting for " + game.active + " to choose a target." view.prompt = "Heir Charge: Choose a target." gen_action(view, 'undo') for (let b = 0; b < block_count; ++b) { if (game.active === game.attacker[game.where]) { if (is_defender(b)) { gen_action(view, 'battle_charge', b) gen_action(view, 'block', b) } } else { if (is_attacker(b)) { gen_action(view, 'battle_charge', b) gen_action(view, 'block', b) } } } }, battle_charge: function (target) { charge_with_block(game.who, target) }, block: function (target) { charge_with_block(game.who, target) }, undo: function () { resume_battle() } } states.battle_treachery = { show_battle: true, prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Treachery: Waiting for " + game.active + " to choose a target." view.prompt = "Treachery: Choose a target." gen_action(view, 'undo') for (let b = 0; b < block_count; ++b) { if (game.active === game.attacker[game.where]) { if (is_defender(b) && can_defect(game.who, b)) { gen_action(view, 'battle_treachery', b) gen_action(view, 'block', b) } } else { if (is_attacker(b) && can_defect(game.who, b)) { gen_action(view, 'battle_treachery', b) gen_action(view, 'block', b) } } } }, battle_treachery: function (target) { attempt_treachery(game.who, target) resume_battle() }, block: function (target) { attempt_treachery(game.who, target) resume_battle() }, undo: function () { resume_battle() } } function goto_battle_hits() { game.battle_list = list_victims(game.active) if (game.battle_list.length === 0) resume_battle() else game.state = 'battle_hits' } function apply_hit(who) { let n = Math.min(game.hits, game.steps[who]) if (n === 1) game.flash = block_name(who) + " took " + n + " hit." else game.flash = block_name(who) + " took " + n + " hits." log_battle(game.flash) while (n-- > 0) { reduce_block(who) game.hits-- } game.battle_list = list_victims(game.active) if (game.battle_list.length > 0) { if (game.hits === 1) game.flash += " 1 hit left." else if (game.hits > 1) game.flash += " " + game.hits + " hits left." } if (game.hits === 0) resume_battle() else goto_battle_hits() } function list_victims(p) { let is_candidate = (p === game.attacker[game.where]) ? is_attacker : is_defender let max = 0 for (let b = 0; b < block_count; ++b) if (is_candidate(b) && game.steps[b] > max) max = game.steps[b] let list = [] for (let b = 0; b < block_count; ++b) if (is_candidate(b) && game.steps[b] === max) set_add(list, b) return list } states.battle_hits = { show_battle: true, prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for " + game.active + " to assign hits." view.prompt = "Assign " + game.hits + (game.hits !== 1 ? " hits" : " hit") + " to your armies." for (let b of game.battle_list) { gen_action(view, 'battle_hit', b) gen_action(view, 'block', b) } }, battle_hit: function (who) { apply_hit(who) }, block: function (who) { apply_hit(who) }, } states.retreat_in_battle = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for " + game.active + " to retreat." gen_action(view, 'undo') gen_action(view, 'block', game.who) if (game.active === game.piracy && set_has(game.is_pirate, game.who)) { view.prompt = "Retreat: Move the army to a friendly or vacant areas in the same sea zone." for (let to of AREAS[game.where].exits) if (is_sea_area(to)) gen_action(view, 'area', to) } else { view.prompt = "Retreat: Move the army to a friendly or vacant area." for (let to of AREAS[game.where].exits) if (can_block_retreat_to(game.who, to)) gen_action(view, 'area', to) } }, area: function (to) { if (is_sea_area(to)) { game.location[game.who] = to game.state = 'sea_retreat_to' } else { game.flash = block_name(game.who) + " retreated." log_battle(game.flash) game.turn_log.push([game.active, "#" + to]) use_border(game.where, to) game.location[game.who] = to resume_battle() } }, eliminate: function () { game.flash = "" eliminate_block(game.who) resume_battle() }, block: function () { resume_battle() }, undo: function () { resume_battle() } } states.sea_retreat_to = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for " + game.active + " to retreat." view.prompt = "Retreat: Move the army to a friendly or vacant area in the same sea zone." // TODO: only eliminate if no retreat is possible gen_action(view, 'eliminate') let from = game.location[game.who] for (let to of AREAS[from].exits) if (is_friendly_or_vacant_area(to)) gen_action(view, 'area', to) }, area: function (to) { let sea = game.location[game.who] game.turn_log.push([game.active, "#" + sea, "#" + to]) game.flash = block_name(game.who) + " retreated." log_battle(game.flash) game.location[game.who] = to resume_battle() }, eliminate: function () { game.flash = "" eliminate_block(game.who) resume_battle() }, undo: function () { game.location[game.who] = game.where resume_battle() } } function goto_regroup() { game.active = game.attacker[game.where] if (is_enemy_area(game.where)) game.active = ENEMY[game.active] log(game.active + " won the battle in #" + game.where + "!") game.state = 'regroup' game.turn_log = [] clear_undo() } function is_attacker_with_piracy() { game.active === game.piracy && game.active === game.attacker[game.where] } states.regroup = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for " + game.active + " to regroup." view.prompt = "Regroup: Choose an army to move." gen_action_undo(view) gen_action(view, 'end_regroup') for (let b = 0; b < block_count; ++b) { if (game.location[b] === game.where) { if (is_attacker_with_piracy()) { if (set_has(game.is_pirate, b)) gen_action(view, 'block', b) } else { if (can_block_regroup(b)) gen_action(view, 'block', b) } } } }, block: function (who) { push_undo() game.who = who game.state = 'regroup_to' }, end_regroup: function () { game.where = NOWHERE clear_undo() print_turn_log(game.active + " regrouped:") goto_battle_phase() }, undo: pop_undo, } states.regroup_to = { prompt: function (view, current) { if (is_attacker_with_piracy() && set_has(game.is_pirate, game.who)) { if (is_inactive_player(current)) return view.prompt = "Waiting for " + game.active + " to regroup." view.prompt = "Regroup: Move the army to a friendly or vacant area in the same sea zone." gen_action_undo(view) gen_action(view, 'block', game.who) for (let to of AREAS[game.where].exits) if (is_sea_area(to)) gen_action(view, 'area', to) } else { if (is_inactive_player(current)) return view.prompt = "Waiting for " + game.active + " to regroup." view.prompt = "Regroup: Move the army to a friendly or vacant area." gen_action_undo(view) gen_action(view, 'block', game.who) for (let to of AREAS[game.where].exits) if (can_block_regroup_to(game.who, to)) gen_action(view, 'area', to) } }, area: function (to) { if (is_sea_area(to)) { log_move_start(game.where) log_move_continue(to) game.location[game.who] = to game.state = 'sea_regroup_to' } else { game.turn_log.push(["#"+game.where, "#"+to]) move_block(game.who, game.where, to) game.who = NOBODY game.state = 'regroup' } }, block: pop_undo, undo: pop_undo, } states.sea_regroup_to = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for " + game.active + " to regroup." view.prompt = "Regroup: Move the army to a friendly or vacant area in the same sea zone." gen_action_undo(view) let from = game.location[game.who] for (let to of AREAS[from].exits) if (is_friendly_or_vacant_area(to)) gen_action(view, 'area', to) }, area: function (to) { log_move_continue(to) log_move_end() game.location[game.who] = to game.who = NOBODY game.state = 'regroup' }, undo: pop_undo, } // SUPPLY PHASE function goto_supply_phase() { set_clear(game.moved) if (game.location[game.king] === NOWHERE) { game.king = find_next_king(block_owner(game.king)) log("The King is dead; long live the king!") if (game.location[game.king] === MINOR) log("The new King is a minor.") else log("The new King is in #" + game.location[game.king] + ".") } goto_execute_clarence() } function goto_execute_clarence() { if (is_block_alive(B_CLARENCE_L)) { game.active = LANCASTER game.state = 'execute_clarence' game.who = B_CLARENCE_L } else { goto_execute_exeter() } } states.execute_clarence = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for " + game.active + " to execute Clarence." view.prompt = "Supply Phase: Execute enemy heir Clarence?" gen_action(view, 'execute_clarence') gen_action(view, 'pass') }, execute_clarence: function () { logp("executed Clarence.") eliminate_block(B_CLARENCE_L) game.who = NOBODY if (game.result) return goto_game_over() goto_execute_exeter() }, pass: function () { game.who = NOBODY goto_execute_exeter() } } function goto_execute_exeter() { if (is_block_alive(B_EXETER_Y)) { game.active = YORK game.state = 'execute_exeter' game.who = B_EXETER_Y } else { goto_enter_pretender_heir() } } states.execute_exeter = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for " + game.active + " to execute Exeter." view.prompt = "Supply Phase: Execute enemy heir Exeter?" gen_action(view, 'execute_exeter') gen_action(view, 'pass') }, execute_exeter: function () { logp("executed Exeter.") eliminate_block(B_EXETER_Y) game.who = NOBODY if (game.result) return goto_game_over() goto_enter_pretender_heir() }, pass: function () { game.who = NOBODY goto_enter_pretender_heir() } } // PRETENDER SUPPLY PHASE function goto_enter_pretender_heir() { game.active = block_owner(game.pretender) let n = game.killed_heirs[game.active] if (n > 0 && (game.who = find_minor_heir(game.active)) !== NOBODY) game.state = 'enter_pretender_heir' else goto_supply_limits_pretender() } states.enter_pretender_heir = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for " + game.active + " to enter pretender heirs." view.prompt = "Death of an Heir: Enter " + block_name(game.who) + " in an exile area." for (let where = first_area; where <= last_area; ++where) if (is_pretender_exile_area(where)) gen_action(view, 'area', where) }, area: function (to) { logbr() log(block_name(game.who) + " came of age in #" + to + ".") --game.killed_heirs[game.active] game.location[game.who] = to game.who = NOBODY goto_enter_pretender_heir() }, } function goto_supply_limits_pretender() { game.reduced = [] game.active = block_owner(game.pretender) if (check_supply_penalty()) { game.state = 'supply_limits_pretender' game.turn_log = [] clear_undo() } else { delete game.supply delete game.reduced goto_enter_royal_heir() } } states.supply_limits_pretender = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for " + game.active + " to check supply limits." view.prompt = "Supply Phase: Reduce blocks in over-stacked areas." gen_action_undo(view) if (game.supply.length === 0) gen_action(view, 'end_supply_phase') for (let b of game.supply) gen_action(view, 'block', b) }, block: function (who) { push_undo() game.turn_log.push(["#" + game.location[who]]) set_add(game.reduced, who) reduce_block(who) check_supply_penalty() }, end_supply_phase: function () { delete game.supply delete game.reduced clear_undo() print_turn_log(game.active + " reduced:") if (game.result) return goto_game_over() goto_enter_royal_heir() }, undo: pop_undo, } // KING SUPPLY PHASE function goto_enter_royal_heir() { game.active = block_owner(game.king) let n = game.killed_heirs[game.active] if (n > 0 && (game.who = find_minor_heir(game.active)) !== NOBODY) game.state = 'enter_royal_heir' else goto_supply_limits_king() } states.enter_royal_heir = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for " + game.active + " to enter royal heirs." view.prompt = "Death of an Heir: Enter " + block_name(game.who) + " in a Crown area." let can_enter = false for (let where = first_area; where <= last_area; ++where) { if (is_crown_area(where) && is_friendly_or_vacant_area(where)) { gen_action(view, 'area', where) can_enter = true } } if (!can_enter) gen_action(view, 'pass') }, area: function (to) { logbr() log(block_name(game.who) + " came of age in #" + to + ".") --game.killed_heirs[game.active] game.location[game.who] = to game.who = NOBODY goto_enter_royal_heir() }, pass: function () { game.who = NOBODY goto_supply_limits_king() } } function goto_supply_limits_king() { game.reduced = [] game.active = block_owner(game.king) if (check_supply_penalty()) { game.state = 'supply_limits_king' game.turn_log = [] clear_undo() } else { delete game.supply delete game.reduced end_game_turn() } } states.supply_limits_king = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for " + game.active + " to check supply limits." view.prompt = "Supply Phase: Reduce blocks in over-stacked areas." gen_action_undo(view) if (game.supply.length === 0) gen_action(view, 'end_supply_phase') for (let b of game.supply) gen_action(view, 'block', b) }, block: function (who) { push_undo() game.turn_log.push(["#" + game.location[who]]) set_add(game.reduced, who) reduce_block(who) check_supply_penalty() }, end_supply_phase: function () { delete game.supply delete game.reduced clear_undo() print_turn_log(game.active + " reduced:") if (game.result) return goto_game_over() end_game_turn() }, undo: pop_undo, } // POLITICAL TURN function goto_political_turn() { logbr() log(".h1 Political Turn") logbr() game.turn_log = [] // Levies disband for (let b = 0; b < block_count; ++b) { if (!is_land_area(game.location[b])) continue switch (block_type(b)) { case 'bombard': case 'levies': case 'rebel': game.turn_log.push(["#" + game.location[b]]) disband(b) break case 'mercenaries': switch (b) { case B_WELSH_MERCENARY: game.turn_log.push(["#" + game.location[b]]) disband(b) break case B_IRISH_MERCENARY: if (game.location[b] !== IRELAND) { game.turn_log.push(["#" + game.location[b], "#" + IRELAND]) game.location[b] = IRELAND } break case B_BURGUNDIAN_MERCENARY: case B_CALAIS_MERCENARY: if (game.location[b] !== CALAIS) { game.turn_log.push(["#" + game.location[b], "#" + CALAIS]) game.location[b] = CALAIS } break case B_SCOTS_MERCENARY: if (game.location[b] !== SCOTLAND) { game.turn_log.push(["#" + game.location[b], "#" + SCOTLAND]) game.location[b] = SCOTLAND } break case B_FRENCH_MERCENARY: if (game.location[b] !== FRANCE) { game.turn_log.push(["#" + game.location[b], "#" + FRANCE]) game.location[b] = FRANCE } break } break } } print_turn_log("Levies disbanded:") // Usurpation let l_count = count_lancaster_nobles_and_heirs() let y_count = count_york_nobles_and_heirs() logbr() log("Lancaster controlled " + l_count + " nobles.") log("York controlled " + y_count + " nobles.") if (l_count > y_count && block_owner(game.king) === YORK) { game.king = find_senior_heir(LANCASTER) game.pretender = find_senior_heir(YORK) log(block_name(game.king) + " usurped the throne!") } else if (y_count > l_count && block_owner(game.king) === LANCASTER) { game.king = find_senior_heir(YORK) game.pretender = find_senior_heir(LANCASTER) log(block_name(game.king) + " usurped the throne!") } else { log(block_name(game.king) + " remained king.") } // Game ends after last Usurpation check if (game.campaign === game.end_campaign) return goto_game_over() logbr() goto_pretender_goes_home() } // PRETENDER GOES HOME function goto_pretender_goes_home() { game.active = block_owner(game.pretender) game.state = 'pretender_goes_home' game.turn_log = [] let choices = false for (let b = 0; b < block_count; ++b) if (block_owner(b) === game.active && is_block_on_map(b)) if (pretender_go_home_if_possible(b)) choices = true if (!choices) { print_turn_log_no_count("Pretender went home:") goto_exile_limits_pretender() } else { clear_undo() } } states.pretender_goes_home = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for the Pretender to go to exile." gen_action_undo(view) let done = true for (let b = 0; b < block_count; ++b) { if (block_owner(b) === game.active && is_block_on_map(b) && !set_has(game.moved, b)) { if (!is_in_exile(b)) { if (is_heir(b)) { done = false gen_action(view, 'block', b) } else if (!is_at_home(b)) { done = false let n = count_available_homes(b) if (n > 1) gen_action(view, 'block', b) } } } } if (done) { view.prompt = "Pretender Goes Home: You may move nobles to another home." for (let b = 0; b < block_count; ++b) { if (block_owner(b) === game.active && is_block_on_map(b) && !set_has(game.moved, b)) { if (!is_in_exile(b)) { if (is_at_home(b)) { let n = count_available_homes(b) if (n > 1) gen_action(view, 'block', b) } } } } gen_action(view, 'end_political_turn') } else { view.prompt = "Pretender Goes Home: Move the pretender and his heirs to exile, and nobles to home." } }, block: function (who) { push_undo() game.who = who game.state = 'pretender_goes_home_to' }, end_political_turn: function () { clear_undo() print_turn_log_no_count("Pretender went home:") goto_exile_limits_pretender() }, undo: pop_undo, } states.pretender_goes_home_to = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for the Pretender to go to exile." if (is_heir(game.who)) view.prompt = "Pretender Goes Home: Move " + block_name(game.who) + " to exile." else view.prompt = "Pretender Goes Home: Move " + block_name(game.who) + " to home." gen_action(view, 'block', game.who) for (let where = first_area; where <= last_area; ++where) { if (where !== game.location[game.who]) { if (is_heir(game.who)) { if (is_friendly_exile_area(where)) gen_action(view, 'area', where) } else if (is_available_home_for(where, game.who)) { gen_action(view, 'area', where) } } } }, area: function (to) { if (is_exile_area(to)) game.turn_log.push([block_name(game.who), "#" + to]) // TODO: "Exile"? else game.turn_log.push([block_name(game.who), "#" + to]) // TODO: "Home"? set_add(game.moved, game.who) game.location[game.who] = to game.who = NOBODY game.state = 'pretender_goes_home' }, block: pop_undo, undo: pop_undo, } function goto_exile_limits_pretender() { set_clear(game.moved) game.active = block_owner(game.pretender) if (check_exile_limits()) { game.state = 'exile_limits_pretender' clear_undo() } else { goto_king_goes_home() } } states.exile_limits_pretender = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for " + game.active + " to check exile limits." view.prompt = "Campaign Reset: Disband one block in each over-stacked exile area." gen_action_undo(view) if (game.exiles.length === 0) gen_action(view, 'end_exile_limits') for (let b of game.exiles) gen_action(view, 'block', b) }, block: function (who) { push_undo() let where = game.location[who] logp("disbanded in #" + where + ".") game.exiles = game.exiles.filter(b => game.location[b] !== where) disband(who) }, end_exile_limits: function () { goto_king_goes_home() }, undo: pop_undo, } // KING GOES HOME function goto_king_goes_home() { game.active = block_owner(game.king) game.state = 'king_goes_home' game.turn_log = [] let choices = false for (let b = 0; b < block_count; ++b) if (block_owner(b) === game.active && is_block_on_map(b)) if (king_go_home_if_possible(b)) choices = true if (!choices) { print_turn_log_no_count("King went home:") goto_exile_limits_king() } else { clear_undo() } } states.king_goes_home = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for the King to go home." gen_action_undo(view) let done = true for (let b = 0; b < block_count; ++b) { if (block_owner(b) === game.active && is_block_on_map(b) && !set_has(game.moved, b)) { if (!is_in_exile(b)) { if (!is_at_home(b)) { done = false let n = count_available_homes(b) if (n > 1) gen_action(view, 'block', b) } } } } if (done) { view.prompt = "King Goes Home: You may move nobles and heirs to another home." for (let b = 0; b < block_count; ++b) { if (block_owner(b) === game.active && is_block_on_map(b) && !set_has(game.moved, b)) { if (!is_in_exile(b)) { if (is_at_home(b)) { let n = count_available_homes(b) if (n > 1) gen_action(view, 'block', b) } } } } gen_action(view, 'end_political_turn') } else { view.prompt = "King Goes Home: Move the King, the royal heirs, and nobles to home." } }, block: function (who) { push_undo() game.who = who game.state = 'king_goes_home_to' }, end_political_turn: function () { clear_undo() print_turn_log_no_count("King went home:") goto_exile_limits_king() }, undo: pop_undo, } states.king_goes_home_to = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for the King to go home." view.prompt = "King Goes Home: Move " + block_name(game.who) + " to home." gen_action(view, 'block', game.who) for (let where = first_area; where <= last_area; ++where) if (where !== game.location[game.who]) if (is_available_home_for(where, game.who)) gen_action(view, 'area', where) }, area: function (to) { game.turn_log.push([block_name(game.who), "#" + to]) // TODO: "Home"? set_add(game.moved, game.who) game.location[game.who] = to game.who = NOBODY game.state = 'king_goes_home' }, block: pop_undo, undo: pop_undo, } function goto_exile_limits_king() { set_clear(game.moved) game.active = block_owner(game.king) if (check_exile_limits()) { game.state = 'exile_limits_king' clear_undo() } else { end_political_turn() } } states.exile_limits_king = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for " + game.active + " to check exile limits." view.prompt = "Campaign Reset: Disband one block in each over-stacked exile area." gen_action_undo(view) if (game.exiles.length === 0) gen_action(view, 'end_exile_limits') for (let b of game.exiles) gen_action(view, 'block', b) }, block: function (who) { push_undo() let where = game.location[who] logp("disbanded in #" + where + ".") game.exiles = game.exiles.filter(b => game.location[b] !== where) disband(who) }, end_exile_limits: function () { end_political_turn() }, undo: pop_undo, } function end_political_turn() { // Campaign reset set_clear(game.dead) for (let b = 0; b < block_count; ++b) game.steps[b] = block_max_steps(b) ++game.campaign start_campaign() } // GAME OVER function goto_game_over() { game.active = "None" game.state = 'game_over' if (!game.result) { game.result = block_owner(game.king) game.victory = game.result + " won!" } logbr() log(game.victory) } states.game_over = { prompt: function (view) { view.prompt = game.victory } } function make_battle_view() { let battle = { round: game.battle_round, LF: [], LR: [], YF: [], YR: [], flash: game.flash } battle.title = game.attacker[game.where] + " attacks " + AREAS[game.where].name battle.title += " \u2014 round " + game.battle_round + " of 4" function fill_cell(cell, owner, fn) { for (let b = 0; b < block_count; ++b) if (game.location[b] === game.where & block_owner(b) === owner && !set_has(game.dead, b) && fn(b)) cell.push(b) } fill_cell(battle.LR, LANCASTER, b => is_battle_reserve(b)) fill_cell(battle.LF, LANCASTER, b => !is_battle_reserve(b)) fill_cell(battle.YR, YORK, b => is_battle_reserve(b)) fill_cell(battle.YF, YORK, b => !is_battle_reserve(b)) return battle } exports.setup = function (seed, scenario, options) { game = { seed: seed, log: [], undo: [], state: null, active: null, turn: 0, moves: 0, who: NOBODY, where: NOWHERE, show_cards: false, killed_heirs: { Lancaster: 0, York: 0 }, king: NOBODY, pretender: NOBODY, location: [], steps: [], moved: [], dead: [], reserves: [], attacker: {}, border_limit: [], last_used: [], main_border: [], } // Old RNG for ancient replays if (options.rng) game.rng = options.rng if (scenario === "Wars of the Roses") setup_game() else if (scenario === "Kingmaker") setup_kingmaker() else if (scenario === "Richard III") setup_richard_iii() else throw new Error("Unknown scenario:", scenario) logbr() log(".h1 " + scenario) logbr() start_campaign() return game } exports.action = function (state, current, action, arg) { game = state let S = states[game.state] if (action in S) S[action](arg, current) else throw new Error("Invalid action: " + action) return game } function observer_hand() { let hand = [] hand.length = Math.max(game.l_hand.length, game.y_hand.length) hand.fill(0) return hand } exports.view = function(state, current) { game = state let view = { log: game.log, campaign: game.campaign + " of " + game.end_campaign, active: game.active, king: game.king, pretender: game.pretender, l_card: (game.show_cards || current === LANCASTER) ? game.l_card : 0, y_card: (game.show_cards || current === YORK) ? game.y_card : 0, hand: (current === LANCASTER) ? game.l_hand : (current === YORK) ? game.y_hand : observer_hand(), who: (game.active === current) ? game.who : NOBODY, where: game.where, location: game.location, steps: game.steps, moved: game.moved, dead: game.dead, last_used: game.last_used, border_limit: game.border_limit, battle: null, prompt: null, } if (game.main_border && game.main_border.length > 0) { view.main_border = [] for (let i = 0; i < game.main_border.length; i += 2) set_add(view.main_border, border_id(game.main_border[i+0], game.main_border[i+1])) } states[game.state].prompt(view, current) if (states[game.state].show_battle) view.battle = make_battle_view() return view } // === COMMON LIBRARY === // remove item at index (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 return array } 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 } // insert item at index (faster than splice) function array_insert(array, index, item) { for (let i = array.length; i > index; --i) array[i] = array[i - 1] array[index] = item return array } 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 } function set_clear(set) { set.length = 0 } 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 set } 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 return array_remove(set, m) } return set } 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 } } } // 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 } } function clear_undo() { if (game.undo.length > 0) game.undo = [] } function push_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() { 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 }