"use strict" let states = {} let game = null let view = null /* DATA */ const data = require("./data.js") const SPACES = data.spaces const SPACE_NAME = data.space_name const CITIES = data.city function get_space_id(name) { return SPACE_NAME.indexOf(name); } const S_ANDHRA = get_space_id("Andhra") const S_BENGAL = get_space_id("Bengal") const S_GONDWANA = get_space_id("Gondwana") const S_GUJARAT = get_space_id("Gujarat") const S_JAUNPUR = get_space_id("Jaunpur") const S_KARNATAKA = get_space_id("Karnataka") const S_MADHYADESH = get_space_id("Madhyadesh") const S_MAHARASHTRA = get_space_id("Maharashtra") const S_MALWA = get_space_id("Malwa") const S_ORISSA = get_space_id("Orissa") const S_RAJPUT_KINGDOMS = get_space_id("Rajput Kingdoms") const S_SINDH = get_space_id("Sindh") const S_TAMILAKAM = get_space_id("Tamilakam") const S_DELHI = get_space_id("Delhi") const S_MOUNTAIN_PASSES = get_space_id("Mountain Passes") const S_PUNJAB = get_space_id("Punjab") const S_MONGOL_INVADERS = 16 const S_DS_AVAILABLE = 17 const S_BK_AVAILABLE = 18 const S_VE_AVAILABLE = 19 const S_BK_INF_2 = 20 const S_BK_INF_4 = 21 const S_VE_INF_1 = 22 const S_VE_INF_2 = 23 const S_VE_INF_3 = 24 const S_VE_INF_4 = 25 const C_CHITTOR = 0 const C_DEVAGIRI = 1 const C_GOA = 2 const C_GULBARGA = 3 const C_VIJAYANAGARA = 4 const C_WARANGAL = 5 const SINGLE_EVENTS = [26] const first_piece = data.first_piece const last_piece = data.last_piece const all_first_piece = 0 const all_last_piece = 103 const first_space = S_ANDHRA const last_space = S_PUNJAB const last_province = S_TAMILAKAM const faction_name = [ "Delhi Sultanate", "Bahmani Kingdom", "Vijayanagara Empire", "Mongol Invaders" ] const faction_short = [ "Sultanate", "Bahmani", "Vijayanagara", "Mongols" ] const faction_acronyms = [ "DS", "BK", "VE", "MI" ] const faction_flags = [ "FDS", "FBK", "FVE", "FMI" ] const AD = [ "A0", "A1", "A2", "A3", "A4", "A5", "A6" ] const DD = [ "D0", "D1", "D2", "D3", "D4", "D5", "D6" ] exports.scenarios = [ "Standard" ] exports.roles = function (scenario, _options) { if (scenario === "Solo") return [ NAME_SOLO ] return [ NAME_DS, NAME_BK, NAME_VE ] } function is_current_role(role) { if (role === NAME_SOLO) return true if (role === NAME_DS) return game.current === DS if (role === NAME_BK) return game.current === BK if (role === NAME_VE) return game.current === VE return false } function change_current(f) { if (game.current != f) { clear_undo() game.current = f } } function load_game(state) { game = state } function save_game() { if (game.solo) { game.active = NAME_SOLO return game } if (game.current === DS) game.active = NAME_DS if (game.current === BK) game.active = NAME_BK if (game.current === VE) game.active = NAME_VE return game } exports.view = function (state, role) { load_game(state) let this_card = game.deck[0] | 0 let deck_size = Math.max(0, game.deck.length - 1) let card_back = next_card_back() view = { state: game.state, prompt: null, actions: null, log: game.log, current: game.current, marked: game.marked, succ: game.succ, vp: game.vp, resources: game.resources, inf: game.inf, deck: [ this_card, deck_size, card_back, game.of_gods_and_kings ], discard: game.discard, cavalry: game.cavalry, cylinder: game.cylinder, pieces: game.pieces, tributary: game.tributary, control: game.control, rebel: game.rebel, order: game.order, campaign_spaces: [], who: {}, dice: game.dice, } if (game.cmd && game.cmd.attacker !== undefined) { view.attack = { where: game.cmd.where, attacker: game.cmd.attacker, target: game.cmd.target } if (game.cmd.n_units !== undefined) view.attack.n_units = game.cmd.n_units } if (game.result) { view.prompt = game.victory } else if (!is_current_role(role)) { let inactive = states[game.state].inactive if (!inactive) { if (game.vm) inactive = "Event" else inactive = game.state } view.prompt = `Waiting for ${faction_name[game.current]} \u2014 ${inactive}.` } else { view.actions = {} if (states[game.state]) states[game.state].prompt() else view.prompt = "Unknown state: " + game.state if (!globalThis.RTT_FUZZER) if (!states[game.state].disable_negotiation) { view.actions.ask_resources = 1 if (game.resources[game.current] > 0) view.actions.transfer_resources = 1 else view.actions.transfer_resources = 0 if (n_cavalry(game.current) > 0) view.actions.transfer_cavalry = 1 else view.actions.transfer_cavalry = 0 if (can_ask_cavalry()) view.actions.ask_cavalry = 1 else view.actions.ask_cavalry = 0 view.actions.ping = 1 } else { view.actions.ask_resources = 0 view.actions.transfer_resources = 0 view.actions.ask_cavalry = 0 view.actions.transfer_cavalry = 0 view.actions.ping = 1 } if (view.actions.undo === undefined) { if (game.undo && game.undo.length > 0) view.actions.undo = 1 else view.actions.undo = 0 } } save_game() return view } exports.action = function (state, role, action, arg) { load_game(state) let S = states[game.state] if (S && action in S) { S[action](arg) } else { if (action === "undo" && game.undo && game.undo.length > 0) pop_undo() else if (action === "ask_resources") action_ask_resources() else if (action === "ask_cavalry") action_ask_cavalry() else if (action === "transfer_resources") action_transfer_resources() else if (action === "transfer_cavalry") action_transfer_cavalry() else if (action === "ping") action_ping() else throw new Error("Invalid action: " + action) } return save_game() } /* SETUP */ exports.setup = function (seed, scenario, _options) { game = { seed, log: [], undo: [], active: null, current: 0, state: null, succ: 0, marked: 0, cylinder: [ ELIGIBLE, ELIGIBLE, ELIGIBLE ], resources: [ 12, 6, 7 ], vp: [ 18, 0, 0 ], prosperity: [ 0, 0, 0 ], inf: [null, 0, 0], tributary: 8191, // all 13 provinces control: [0, 0, 0], rebel: [null, 0, 0], // amir/raja rebel status pieces: Array(104).fill(AVAILABLE), // piece locations cavalry: Array(10).fill(AVAILABLE), deck: [], of_gods_and_kings: [null, null, 0], discard: [], order: [], cmd: { type: null, limited: null, free: null, spaces: [], pieces: [], where: null, who: null, event: 0 }, cav: null, inf_shift: null, decree: null, vm: null, dice: [0, 0, 0, 0, 0, 0] } if (scenario === "Solo") game.solo = 1 // Setup setup_standard() update_control() setup_deck() goto_card() return save_game() } function shuffle_cards(a, b) { let deck = [] for (let i = a; i <= b; ++i) deck.push(i) shuffle(deck) return deck } function setup_deck() { let cards = shuffle_cards(1, 36) let mi_cards = shuffle_cards(37, 44).slice(0, 6).concat([48, 49]) let deck = [] let pile let i = 0 for (let p = 0; p < 4; ++p) { pile = cards.slice(i, i + 6) pile = pile.concat(mi_cards.slice(p*2, p*2 + 2)) i += 6 shuffle(pile) deck = deck.concat(pile) if (p<3) deck = deck.concat([45+p]) } game.of_gods_and_kings[0] = cards[cards.length-1] game.deck = deck } function setup_standard() { setup_piece(DS, DISC, 1, S_ANDHRA) setup_piece(DS, ELITE, 1, S_MALWA) setup_piece(DS, TROOPS, 4, S_DELHI) setup_piece(DS, TROOPS, 4, S_PUNJAB) setup_piece(DS, TROOPS, 3, S_MALWA) setup_piece(DS, TROOPS, 2, S_JAUNPUR) setup_piece(DS, TROOPS, 2, S_MADHYADESH) setup_piece(DS, TROOPS, 2, S_MOUNTAIN_PASSES) setup_piece(DS, TROOPS, 1, S_ANDHRA) setup_piece(DS, TROOPS, 1, S_GUJARAT) setup_piece(DS, TROOPS, 1, S_RAJPUT_KINGDOMS) setup_piece(DS, TROOPS, 1, S_SINDH) setup_piece(DS, TROOPS, 1, S_TAMILAKAM) setup_piece(BK, ELITE, 1, S_GONDWANA) setup_piece(BK, ELITE, 1, S_GUJARAT) setup_piece(BK, ELITE, 2, S_MADHYADESH) setup_piece(BK, ELITE, 4, S_MAHARASHTRA) setup_piece(BK, ELITE, 2, S_BK_INF_2) setup_piece(BK, ELITE, 2, S_BK_INF_4) setup_piece(VE, ELITE, 1, S_TAMILAKAM) setup_piece(VE, ELITE, 2, S_ANDHRA) setup_piece(VE, ELITE, 3, S_KARNATAKA) setup_piece(VE, ELITE, 2, S_VE_INF_1) setup_piece(VE, ELITE, 2, S_VE_INF_2) setup_piece(VE, ELITE, 2, S_VE_INF_3) setup_piece(VE, ELITE, 2, S_VE_INF_4) // Two cavalry to DS set_cavalry_faction(0, DS) set_cavalry_faction(1, DS) } function setup_piece(faction, type, count, where) { for (let p = first_piece[faction][type]; count > 0; ++p) { if (piece_space(p) < 0) { set_piece_space(p, where) --count } } } // === SEQUENCE OF PLAY === function this_card() { return game.deck[0] } function next_card_back() { if (game.deck.length > 1) { if (game.deck[1] >= 45 && game.deck[1] <= 47) return game.deck[1] - 44 } return 0 } function goto_card() { log_h1("C" + this_card()) if (this_card() >= 37 && this_card() <= 40) { goto_mongol_invaders(BK) } else if (this_card() >= 41 && this_card() <= 44) { goto_mongol_invaders(VE) } else if (this_card() >= 45 && this_card() <= 47) { succession() } else if (this_card() >= 48 && this_card() <= 49) { succession() } else { adjust_eligibility(DS) adjust_eligibility(BK) adjust_eligibility(VE) game.marked = 0 resume_event_card() } } function resume_event_card() { clear_undo() if (game.cylinder.includes(ELIGIBLE)) goto_eligible() else end_card() } function end_card() { clear_undo() update_discard() array_remove(game.deck, 0) goto_card() } function update_discard() { if ("discard" in game === false) { game.discard = [] } if (this_card() === 45) game.discard = [45] else if (this_card() === 46) game.discard = [45, 46] else if (this_card() === 47) game.discard = [45, 46, 47] else if (this_card() > 36) game.discard.push(this_card()) } function goto_eligible() { change_current(next_eligible_faction()) if (game.current < 0) { end_card() } else { game.state = "eligible" game.decree = {} reset_cmd() } } function reset_cmd() { game.cmd = { limited: 0, free: 0, spaces: [], pieces: [], selected: [], where: -1, pass: 1 } } function is_eligible(faction) { return game.cylinder[faction] === ELIGIBLE } function did_option(e) { return ( game.cylinder[DS] === e || game.cylinder[BK] === e || game.cylinder[VE] === e ) } function next_eligible_faction() { game.order = data.card_order[this_card()] for (let faction of game.order) if (is_eligible(faction)) return faction return -1 } function adjust_eligibility(faction) { if (game.cylinder[faction] === INELIGIBLE || game.cylinder[faction] === SOP_PASS || game.cylinder[faction] === SOP_LIMITED_COMMAND) game.cylinder[faction] = ELIGIBLE else if (game.cylinder[faction] !== ELIGIBLE) game.cylinder[faction] = INELIGIBLE if (game.marked & (16 << faction)) game.cylinder[faction] = ELIGIBLE } function goto_pass() { push_undo() game.cmd = 0 game.sa = 0 game.cylinder[game.current] = SOP_PASS log_h2(faction_short[game.current] + " - Pass") if (game.current === DS) { log_resources(game.current, 3) add_resources(game.current, 3) } else { log_resources(game.current, 1) add_resources(game.current, 1) } game.state = "pass" } states.pass = { prompt() { view.prompt = "Pass: Done." view.actions.end_of_turn = 1 }, end_of_turn: resume_event_card, } function goto_advance() { init_command("Advance") game.cmd.free -= 1 game.state = "advance" } function goto_amass() { init_command("Amass") game.cmd.free -= 1 game.state = "amass" } function goto_cavalry(n, next) { if (typeof n !== "undefined") { game.cav = { "n": n, "next": next } } push_summary() game.state = "cavalry" // Removing automatic cavalry attribution // while (game.cav.n > 0 && n_available_cavalry() > 0) { // let c = find_cavalry(AVAILABLE) // log_summary_cavalry(c) // set_cavalry_faction(c, game.current) // game.cav.n -= 1 // } // if (game.cav.n !== 0) // game.state = "cavalry" // else { // end_cavalry() // } } function end_cavalry() { if (game.vm && is_succession()) pop_summary() else if (game.vm) upop_summary() else if (game.decree && game.decree.type === "Compel") pop_summary() else upop_summary() log_br() if (game.vm) { vm_next() } else if (game.cav.next) { if (game.cav.next === "end_card") end_card() else if (game.cav.next === "end_decree") end_decree() else game.state = game.cav.next } else { goto_eligible() } } function goto_compromising_gifts() { if (game.inf[game.current] === 0) game.state = "end_mongol_invasion" else game.state = "compromising_gifts" } function goto_demand() { init_decree("Demand Obedience") game.state = "demand" } function goto_march() { init_command("March") game.cmd.pieces = [] game.state = "march" } function goto_march_space() { push_summary() game.cmd_count = 0 game.state = "march_space" } function end_march_space() { pop_summary() game.state = "march" } function goto_mi_attack() { init_command("Attack & Plunder") game.cmd.free -= 1 game.cmd.selected = [] game.cmd.cavalry = false game.state = "mi_attack" } function goto_mongol_invaders(faction) { game.cmd = { pieces: [], spaces: [], limited: 0, free: 2, sa_faction: faction } change_current(faction) goto_strategic_assistance() } function goto_strategic_assistance() { if (game.cmd.free > 0) { game.state = "strategic_assistance" } else { game.cmd.free = 1 goto_compromising_gifts() } } function goto_rebel() { init_command("Rebel") game.state = "rebel" } function goto_tax() { init_decree("Tax") game.decree.count = 1 game.state = "tax" } /* SUCCESSION */ function succession() { change_current(DS) if (game.succ > 0) game.succ += 1 goto_vm(game.succ * 2 + 74) } function is_succession() { return (game.vm && game.vm.fp > 73) } function is_timurid() { return (game.vm && game.vm.fp > 78) } /* STATES */ states.eligible = { disable_negotiation: false, inactive: "Eligible Faction", prompt() { if (!did_option(SOP_COMMAND_DECREE) && !did_option(SOP_EVENT_OR_COMMAND)) { view.prompt = `${data.card_title[this_card()]}: Event or Command & Decree.` view.actions.command_decree = 1 view.actions.event_command = 1 } else if (!did_option(SOP_COMMAND_DECREE)) { view.prompt = `${data.card_title[this_card()]}: Command & Decree.` view.actions.command_decree = 1 } else if (!did_option(SOP_EVENT_OR_COMMAND)) { view.prompt = `${data.card_title[this_card()]}: Event or Command.` view.actions.event_command = 1 } else view.prompt = `${data.card_title[this_card()]}: Limited Command.` view.actions.lim_command = 1 view.actions.pass = 1 view.actions.undo = 0 }, pass: goto_pass, command_decree() { push_undo() game.cylinder[game.current] = SOP_COMMAND_DECREE game.decree.n = 1 game.state = "main_phase" }, event_command() { push_undo() game.cylinder[game.current] = SOP_EVENT_OR_COMMAND game.cmd.event = 1 game.state = "main_phase" }, lim_command() { push_undo() game.cylinder[game.current] = SOP_LIMITED_COMMAND game.cmd.limited = 1 game.state = "main_phase" }, } states.main_phase = { inactive: "Eligible Faction", prompt() { if (game.cmd && game.cmd.limited === 1) { view.prompt = "Select a Limited Command." gen_any_command() } else if (game.cmd && game.decree && game.decree.n === 1) { view.prompt = "Select a Command and a Decree." gen_any_command() gen_any_decree() } else if (game.cmd && game.cmd.event === 1) { view.prompt = "Select a Command or the Event." gen_any_command() gen_any_event() } else if (game.cmd) { view.prompt = "Select a Command." gen_any_command() } else if (game.decree && game.decree.n === 1) { view.prompt = "Select a Decree." gen_any_decree() } else { switch (game.cylinder[game.current]) { case SOP_COMMAND_DECREE: view.prompt = "Command and Decree: Done." break case SOP_EVENT_OR_COMMAND: view.prompt = "Event or Command: Done." break case SOP_LIMITED_COMMAND: view.prompt = "Limited Command: Done." break } view.actions.end_of_turn = 1 } }, attack: goto_attack, build: goto_build, campaign: goto_campaign, collect: goto_collect, compel: goto_compel, conscript: goto_conscript, conspire: goto_conspire, demand: goto_demand, govern: goto_govern, march: goto_march, migrate: goto_migrate, rally: goto_rally, rebel: goto_rebel, tax: goto_tax, trade: goto_trade, event() { goto_event(0) }, unshaded() { goto_event(0) }, shaded() { goto_event(1) }, gk_unshaded() { goto_gk_event(0) }, gk_shaded() { goto_gk_event(1) }, end_of_turn: resume_event_card, } states.cavalry = { inactive: "Gain Cavalry", prompt() { if (game.cav.n > 0) { view.prompt = `Gain Cavalry: Take ${game.cav.n} Cavalry token${game.cav.n > 1 ? "s" : ""}.` let can_take = gen_take_cavalry(game.current) if (can_take) view.prompt = `Gain Cavalry: Take ${game.cav.n} Cavalry token${game.cav.n > 1 ? "s" : ""}.` else { view.prompt = "Gain Cavalry: No more Cavalry available." view.actions.end_cavalry = 1 } } else { view.prompt = "Gain Cavalry: Done." view.actions.end_cavalry = 1 } }, token(c) { push_undo() game.cav.n -= 1 log_summary_cavalry(c) set_cavalry_faction(c, game.current) if (game.cav.n === 0) end_cavalry() }, end_cavalry: end_cavalry } states.compromising_gifts = { inactive: "Compromising Gifts", prompt() { view.prompt = "Compromising Gifts: Reduce your Influence by one to gain two Resources and two Cavalry." gen_action_influence(game.current) view.actions.end_gifts = 1 }, influence(f) { push_undo() log_h2(faction_short[f] + " - Compromising Gifts") log_resources(f, 2) add_resources(f, 2) game.inf_shift = { "next": "goto_cavalry", } game.cav = { "n": 2, "next": "end_mongol_invasion" } remove_influence(f) }, end_gifts() { game.state = "end_mongol_invasion" } } states.demand = { inactive: "Demand Obedience", prompt() { view.prompt = "Demand Obedience: Select a Controlled province with a Governor." if (can_demand()) for (let s = first_space; s <= last_space; ++s) if (!is_selected_decree_space(s) && can_demand_in_space(s)) gen_action_space(s) if (game.decree.spaces.length > 0) view.actions.end_demand = 1 if (!view.actions.space) view.prompt = "Demand Obedience: Done." }, space(s) { push_undo() select_decree_space(s) demand_obedience_in_space(s) }, end_demand: end_decree, } function demand_obedience_in_space(s) { add_resources(DS, SPACES[s].pop) add_tributary(s) to_obedient_space(s) log_space(s, "Demand Obedience") logi_resources(DS, SPACES[s].pop) } states.march = { inactive: "March", prompt() { view.prompt = "March: Select a space to move into." if (can_select_cmd_space(1)) { for (let s = first_space; s <= last_space; ++s) { if (!is_selected_cmd_space(s) && can_march_in_space(s)) gen_action_space(s) } } view.actions.end_march = prompt_end_cmd(1) }, space(s) { push_undo() select_cmd_space(s, 1) log_space(game.cmd.where, "March") goto_march_space() }, end_march: end_command } states.march_space = { inactive: "March", prompt() { view.prompt = "March: Move pieces from adjacent spaces." view.where = game.cmd.where for_each_movable(DS, p => { if ( set_has(SPACES[game.cmd.where].adjacent, piece_space(p)) && !set_has(game.cmd.pieces, p) ) gen_action_piece(p) }) if (game.cmd.pieces.length > 0) view.actions.next = 1 }, piece(p) { log_summary_move_from(p) set_add(game.cmd.pieces, p) place_piece(p, game.cmd.where) }, next() { end_march_space() } } states.rebel = { inactive: "Rebel", prompt() { view.prompt = "Rebel: Select a Province where your pieces are in majority." if (can_select_cmd_space(1)) { for (let s = first_space; s <= last_space; ++s) { if (!is_selected_cmd_space(s) && can_rebel_in_space(s)) gen_action_space(s) } } view.actions.end_rebel = prompt_end_cmd(1) }, space(s) { push_undo() select_cmd_space(s, 1) remove_tributary(s) to_rebel_space(s, game.current) log_space(s, "Rebel") }, end_rebel: end_command, } states.strategic_assistance = { inactive: "Mongol Invasion", prompt() { let n_command = (game.cmd.free === 2) ? "two Mongol Commands" : "one Mongol Command" view.prompt = `Strategic Assistance: Carry out ${n_command}.` gen_mi_command() }, amass: goto_amass, advance: goto_advance, mi_attack: goto_mi_attack, } states.end_mongol_invasion = { inactive: "Mongol Invasion", prompt() { view.prompt = "Mongol Invasion: Done." view.actions.end_of_turn = 1 }, end_of_turn() { end_card() }, } states.tax = { inactive: "Tax", prompt() { view.prompt = `Tax: Collect ${tax_count()} Resources from Prosperity and Temples in your Controlled Provinces.` if (game.decree.count > 0) gen_action_resources(VE) view.actions.end_tax = prompt_end_decree() }, resources(f) { let t = tax_count() add_resources(game.current, t) log_resources(VE, t) game.decree.count = 0 }, end_tax: end_decree } /* COMMANDS */ function init_command(type) { push_undo() if (game.cmd.limited) log_h2(faction_short[game.current] + " - Limited " + type) else log_h2(faction_short[game.current] + " - " + type) game.cmd.type = type } function init_vm_command(type, free, limited) { push_undo() if (limited) log_h2(faction_short[game.current] + " - Limited " + type) else log_h2(faction_short[game.current] + " - " + type) game.cmd = { type: type, limited: limited, free: free, spaces: [], selected: [], pieces: [], where: -1, } } function init_free_command_in_space(type, s) { log_space(s, type) game.cmd = { type: type, limited: s >= 0 ? 1 : 0, free: 1, spaces: [], selected: [], pieces: [], where: s, } if (s >= 0) set_add(game.cmd.spaces, s) } function gen_any_command() { if (game.current === DS) { view.actions.conscript = can_conscript() ? 1 : 0 view.actions.govern = can_govern() ? 1 : 0 view.actions.march = can_march() ? 1 : 0 view.actions.attack = can_attack() ? 1 : 0 } else if (game.current === BK || game.current === VE) { view.actions.rally = can_rally() ? 1 : 0 view.actions.migrate = can_migrate() ? 1 : 0 view.actions.rebel = can_rebel() ? 1 : 0 view.actions.attack = can_attack() ? 1 : 0 } } function can_select_cmd_space(cost) { if (!game.cmd.free && game.resources[game.current] < cost) return false if (game.cmd.limited) return game.cmd.spaces.length === 0 return true } function end_command() { if (game.summary && game.summary.length > 0) { pop_summary() } else if (game.summary) { game.summary = null } log_br() game.cmd = null if (game.vm) vm_next() else game.state = "main_phase" } function end_decree() { if (game.summary && game.summary.length > 0) { pop_summary() } log_br() game.decree = null if (game.vm) vm_next() else game.state = "main_phase" } function prompt_end_cmd(cost) { if (!view.actions.space) { if (game.cmd.limited && game.cmd.spaces && game.cmd.spaces.length > 0) view.prompt = game.cmd.type + ": Done." else if (!game.cmd.free && game.resources[game.current] < cost) view.prompt = game.cmd.type + ": No Resources." else view.prompt = game.cmd.type + ": Done." } return true } function select_cmd_space(s, cost) { set_add(game.cmd.spaces, s) if (!game.cmd.free) game.resources[game.current] -= cost game.cmd.where = s } function select_decree_space(s) { set_add(game.decree.spaces, s) game.decree.where = s } function can_march() { for (let s = first_space; s <= last_space; ++s) if (can_march_in_space(s)) return true return false } function can_march_in_space(s) { for (let adj of SPACES[s].adjacent) if (has_unmoved_piece(adj, DS)) return true return false } function can_rebel() { if (game.succ === 0) return false for (let s = first_space; s <= last_space; ++s) { if (can_rebel_in_space(s)) return true } return false } function can_rebel_in_space(s) { if (is_tributary(s) && has_majority(s) === game.current) return true return false } /* SHARED COMMANDS */ function can_attack() { for (let s = first_space; s <= last_space; ++s) if (can_attack_in_space(s)) return true return false } function can_attack_in_space(s) { if (!has_valid_attackers(s, game.current)) return false if (game.current === DS) { if ( can_attack_rebel_in_space(s, BK) || can_attack_rebel_in_space(s, VE) || has_piece(s, MI, TROOPS) ) return true } else if (game.current === BK) { if (count_faction_pieces(s, DS) > 0 || count_faction_pieces(s, VE) > 0) return true } else if (game.current === VE) { if (count_faction_pieces(s, DS) > 0 || count_faction_pieces(s, BK) > 0) return true } return false } function can_attack_rebel_in_space(s, faction) { if ( count_pieces(s, faction, DISC) === 1 && count_faction_pieces(s, faction) === 1 ) return true if (has_rebel(s, faction)) return true return false } function goto_attack() { init_command("Attack") game.cmd.attacker = game.current game.cmd.support_space = null game.state = "attack" } function goto_attack_select() { game.cmd.target = [0, 0, 0, 0] if (game.current === DS) { game.cmd.target[BK] += can_attack_rebel_in_space(game.cmd.where, BK) game.cmd.target[VE] += can_attack_rebel_in_space(game.cmd.where, VE) game.cmd.target[MI] += has_piece(game.cmd.where, MI, TROOPS) } else if (game.current === BK) { game.cmd.target[DS] += count_faction_pieces(game.cmd.where, DS) > 0 game.cmd.target[VE] += count_faction_pieces(game.cmd.where, VE) > 0 } else if (game.current === VE) { game.cmd.target[DS] += count_faction_pieces(game.cmd.where, DS) > 0 game.cmd.target[BK] += count_faction_pieces(game.cmd.where, BK) > 0 } let n = game.cmd.target.reduce((a, c) => a + c, 0); if (n === 1) { game.cmd.target = game.cmd.target.indexOf(1) goto_attack_space() } else { game.state = "attack_select" } } function roll_attack() { for (let d = 0; d < 6; ++d) game.dice[d] = random(6) + 1 if (game.cmd.target === DS && game.cmd.attacker === MI && game.cmd.where === S_DELHI) game.dice[3] = 0 let is_shaded_28 = (game.vm && game.vm.fp === 57) let is_event_30 = (game.vm && game.vm.fp === 60) let is_shaded_30 = (game.vm && game.vm.fp === 61) if (game.cmd.target === BK && has_piece(game.cmd.where, BK, DISC) && !is_shaded_30) game.dice[3] = 0 if (is_shaded_28 || is_event_30 || is_shaded_30) { game.dice[4] = 0 game.dice[5] = 0 } if (game.cmd.n_units[1] === 0) { game.dice[4] = 0 game.dice[5] = 0 } } function attack_use_cavalry(d, f) { use_cavalry(f) let is_attacker = (f === game.cmd.attacker) let is_a_die = (d < 4) if (is_a_die && is_attacker) { charge_die(d) } else if (!is_a_die && is_attacker) { screen_die(d) } else if (is_a_die && !is_attacker) { screen_die(d) } else if (!is_a_die && !is_attacker) { charge_die(d) } } function charge_die(d) { if (d < 4) { logi(`${AD[game.dice[d]]} -> ${AD[game.dice[d]-1]} charged`) } else { logi(`${DD[game.dice[d]]} -> ${DD[game.dice[d]-1]} charged`) } game.dice[d] -= 1 } function screen_die(d) { if (d < 4) { logi(`${AD[game.dice[d]]} screened`) } else { logi(`${DD[game.dice[d]]} screened`) } game.dice[d] = 0 } states.attack = { inactive: "Attack", prompt() { if (can_select_cmd_space(1) && can_attack()) { if (game.current === DS) view.prompt = "Attack: Select spaces with Rebels or Mongol Invaders." else view.prompt = "Attack: Select Provinces with opposing pieces." for (let s = first_space; s <= last_space; ++s) { if (!is_selected_cmd_space(s) && can_attack_in_space(s)) gen_action_space(s) } } view.actions.end_attack = prompt_end_cmd(1) }, space(s) { push_undo() game.cmd.selected = [] game.cmd.where = s select_cmd_space(game.cmd.where, 1) set_add(game.cmd.spaces, game.cmd.where) log_space(game.cmd.where, "Attack") goto_attack_select() }, end_attack: end_command } states.attack_select = { inactive: "Attack", prompt() { view.prompt = "Attack: Choose a Faction to attack." for (let [f, t] of game.cmd.target.entries()) if (t != 0) gen_faction_in_space(game.cmd.where, f) }, piece(p) { let f = piece_faction(p) game.cmd.target = f goto_attack_space() } } function goto_attack_space() { game.cmd.n_units = [0, 0] game.cmd.cavalry = false for_each_movable(game.cmd.target, p => { if (piece_space(p) === game.cmd.where) { set_add(game.cmd.selected, p) set_add(game.cmd.pieces, p) game.cmd.n_units[1] += 1 if (game.cmd.attacker === DS && game.cmd.target !== MI) to_rebel(p) } }) for_each_piece(game.cmd.target, DISC, p => { if (piece_space(p) === game.cmd.where) set_add(game.cmd.selected, p) }) for_each_piece(game.cmd.attacker, DISC, p => { if (piece_space(p) === game.cmd.where) set_add(game.cmd.selected, p) }) for_each_movable(game.cmd.attacker, p => { if (piece_space(p) === game.cmd.where) { if (game.cmd.attacker === MI || !set_has(game.cmd.pieces, p)) { set_add(game.cmd.selected, p) set_add(game.cmd.pieces, p) game.cmd.n_units[0] += 1 } if ((game.cmd.attacker === BK || game.cmd.attacker === VE) && game.cmd.target === DS) to_rebel(p) } }) game.cmd.n_adj = (game.current === VE) ? 1 : 2 if (has_valid_support_attackers(game.cmd.where, game.cmd.attacker)) game.state = "attack_adjacent_support" else game.state = "attack_space" } states.attack_adjacent_support = { inactive: "Attack", prompt() { view.who = game.cmd.selected if (game.current === DS) view.prompt = "Attack: Join with up to two Units from one adjacent space with a Qasbah." else if (game.current === BK) view.prompt = "Attack: Join with up to two Amirs from one adjacent Province with a Fort." else if (game.current === VE) view.prompt = "Attack: Join with up to one Raja from one adjacent Province with a Temple." if (game.cmd.support_space === null) { for (let s = first_space; s <= last_space; ++s) if (is_adjacent(game.cmd.where, s) && can_support_from(s, game.cmd.attacker)) gen_action_space(s) } else { if (game.cmd.n_adj > 0) for_each_movable(game.cmd.attacker, p => { if ( piece_space(p) === game.cmd.support_space && !set_has(game.cmd.selected, p) && !set_has(game.cmd.pieces, p) ) gen_action_piece(p) }) } if (game.cmd.n_units[0] > 0) view.actions.next = 1 }, space(s) { push_undo() game.cmd.support_space = s }, piece(p) { push_undo() set_add(game.cmd.selected, p) set_add(game.cmd.pieces, p) game.cmd.n_adj -= 1 game.cmd.n_units[0] += 1 if (game.cmd.target === DS) to_rebel(p) }, next() { game.state = "attack_space" } } states.attack_space = { inactive: "Attack", prompt() { view.prompt = "Attack: Roll the dice!" view.who = game.cmd.selected view.actions.roll = 1 }, roll() { clear_undo() roll_attack() log(">" + game.dice.slice(0,4).filter(d => d > 0).map(d => AD[d]).join(" ")) log(">" + game.dice.slice(4).filter(d => d > 0).map(d => DD[d]).join(" ")) log_br() game.cmd.step = 0 next_attack_cavalry_step() } } function next_attack_cavalry_step() { game.cmd.cavalry = false if (game.cmd.step === 0) { goto_attack_cavalry(game.cmd.attacker) } else if (game.cmd.step === 1) { clear_undo() goto_attack_cavalry(game.cmd.target) } else { game.cmd.a_hit = game.dice.slice(0,4).filter(d => d > 0 && d <= game.cmd.n_units[0] && d < 6).length game.cmd.d_hit = game.dice.slice(4).filter(d => d > 0 && d <= game.cmd.n_units[1] && d < 6).length game.cmd.victor = get_attack_victor() log_br() log(`${faction_flags[game.cmd.attacker]} scored ${game.cmd.a_hit} hits.`) log(`${faction_flags[game.cmd.target]} scored ${game.cmd.d_hit} hits.`) log_br() goto_attack_casualties() } } function goto_attack_cavalry(curr) { if (curr === MI) { let u = curr === game.cmd.attacker ? 0 : 1 let dices = game.dice.slice( curr === game.cmd.attacker ? 0 : 4, curr === game.cmd.attacker ? 4 : 6, ) let d_hit = dices.findIndex(d => (d - 1 === game.cmd.n_units[u] || (d === 6 && game.cmd.n_units[u] >= 5)) ) if (d_hit !== -1) { log(`${faction_flags[curr]} used CAV`) attack_use_cavalry(d_hit + (curr === game.cmd.attacker ? 0 : 4), MI) } else { let d_hit2 = dices.findIndex(d => d === 2) if (d_hit2 !== -1 && curr === game.cmd.attacker) { log(`${faction_flags[curr]} used CAV`) attack_use_cavalry(d_hit2 + (curr === game.cmd.attacker ? 0 : 4), MI) } } game.cmd.step += 1 next_attack_cavalry_step() } else { change_current(curr) game.state = "attack_cavalry" } } states.attack_cavalry = { inactive: "Attack", prompt() { let curr_die = [0, 1, 2, 3, 4, 5] view.prompt = "Attack: Use cavalry to Charge your dice or Screen your opponent's dice." if (has_cavalry(game.current)) for (let d of curr_die) if (can_use_cavalry_on_d(d)) gen_action_die(d) view.actions.next = 1 view.who = game.cmd.selected }, die(d) { push_undo() if (!game.cmd.cavalry) { log_br() log(`${faction_flags[game.current]} used CAV`) game.cmd.cavalry = true } attack_use_cavalry(d, game.current) }, next() { game.cmd.step += 1 next_attack_cavalry_step() } } function can_use_cavalry_on_d(d) { if (game.dice[d] <= 1) return false if (game.current === game.cmd.target && d < 4 && game.dice[d] === 6) return false if (game.current === game.cmd.target && d < 4 && game.dice[d] > game.cmd.n_units[0]) return false return true } function get_attack_victor() { if (game.cmd.a_hit > game.cmd.d_hit) return game.cmd.attacker else if (game.cmd.a_hit < game.cmd.d_hit) return game.cmd.target else return -1 } function goto_attack_casualties() { let curr game.dice = [0, 0, 0, 0, 0, 0] if (game.cmd.step === 2) { curr = game.cmd.target game.cmd.count = game.cmd.a_hit } else if (game.cmd.step === 3) { curr = game.cmd.attacker game.cmd.count = game.cmd.d_hit } // Auto-remove MI casualties if (curr === MI) { remove_mi_casualties(game.cmd.where) end_attack_casualties() } else { change_current(curr) if (game.cmd.count > 0) { push_summary() game.state = "attack_casualties" } else end_attack_casualties() } } function remove_mi_casualties(s) { push_summary() while ((game.cmd.count > 0) && (count_pieces(s, MI, TROOPS) > 0)) { let p = find_piece(s, MI, TROOPS) log_summary_remove(p) remove_piece(p) game.cmd.count -= 1 } upop_summary() } states.attack_casualties = { inactive: "Attack", prompt() { if ( game.cmd.count > 0 && game.cmd.selected.filter(p => piece_faction(p) === game.current).length > 0 ) { view.prompt = `Attack: Remove ${game.cmd.count} piece${game.cmd.count > 1 ? "s" : ""} as casualties.` // Adding all units in space as targets if (game.cmd.step === 3) { for_each_movable(game.cmd.attacker, p => { if (piece_space(p) === game.cmd.where) { set_add(game.cmd.selected, p) } }) } for (let p of game.cmd.selected) if (piece_faction(p) === game.current) gen_action_piece(p) } else { view.prompt = "Attack: No more casualties to remove." view.actions.next = 1 } }, piece(p) { push_undo() if (piece_space(p) === game.cmd.where) log_summary_remove(p) else log_summary_remove_from(p) if (piece_type(p) === DISC) game.cmd.n_units[game.cmd.step === 2 ? 1 : 0] += 1 remove_piece(p) set_delete(game.cmd.selected, p) game.cmd.count -= 1 }, next() { end_attack_casualties() } } function end_attack_casualties() { if (game.summary) upop_summary("No casualty.") clear_undo() if (game.cmd.step === 2) { game.cmd.step += 1 goto_attack_casualties() } else goto_attack_resolution() } function goto_attack_resolution() { game.cmd.support_space = null if (is_rebel_faction(game.cmd.target) && is_rebel_faction(game.cmd.attacker)) attack_influence_shift() else if (game.cmd.attacker === MI && !is_timurid()) { goto_plunder() } else if (game.vm) { vm_next() } else { game.state = "attack" } } function attack_influence_shift() { let up = null, down = null let score_a = Math.min(game.cmd.a_hit, game.cmd.n_units[1]) let score_d = Math.min(game.cmd.d_hit, game.cmd.n_units[0]) if (score_a > score_d) { up = game.cmd.attacker down = game.cmd.target } else if (score_a < score_d) { down = game.cmd.attacker up = game.cmd.target } if (up) { if (game.vm) game.inf_shift = { "next": "vm_next" } else game.inf_shift = { "next": "attack" } log_br() add_influence(up) remove_influence(down) } else { if (game.vm) { vm_next() } else { game.state = "attack" } } } /* DELHI SULTANATE COMMANDS */ function can_conscript() { return has_piece(AVAILABLE, DS, TROOPS) } function can_conscript_in_space(s) { if (s === S_DELHI || has_piece(s, DS, DISC) || is_tributary(s)) return true } function conscript_count() { if (game.cmd.where === S_DELHI) return 5 if (has_piece(game.cmd.where, DS, DISC)) { return 2 } return 1 } function goto_conscript() { init_command("Conscript") game.state = "conscript" } function goto_conscript_space() { push_summary() if (conscript_count() === 1) { let p = find_piece(AVAILABLE, game.current, TROOPS) log_summary_place(p) place_piece(p, game.cmd.where) end_conscript_space() } else { game.cmd.count = 0 game.state = "conscript_space" } } states.conscript = { inactive: "Conscript", prompt() { view.prompt = "Conscript: Select Tributaries, Qasbah or Delhi to place troops." if (can_select_cmd_space(1) && can_conscript()) { for (let s = first_space; s <= last_space; ++s) { if (!is_selected_cmd_space(s) && can_conscript_in_space(s)) gen_action_space(s) } } view.actions.end_conscript = prompt_end_cmd(1) }, space(s) { push_undo() select_cmd_space(s, 1) goto_conscript_space() }, end_conscript: end_command, } states.conscript_space = { inactive: "Conscript", prompt() { view.prompt = `Conscript: Place up to ${conscript_count()} Troops.` view.where = game.cmd.where view.actions.next = 1 gen_place_piece(DS, TROOPS) }, piece(p) { log_summary_place(p) place_piece(p, game.cmd.where) if (++game.cmd.count >= conscript_count()) end_conscript_space() }, next() { end_conscript_space() } } function end_conscript_space() { log_space(game.cmd.where, "Conscript") pop_summary() game.state = "conscript" } function can_govern() { for (let s = first_space; s <= last_space; ++s) if (can_govern_in_space(s)) return true return false } function can_govern_in_space(s) { let is_space_agnostic = (game.vm && "ignore_loc_req" in game.vm && game.vm.ignore_loc_req) let is_space = (is_tributary(s) || s === S_DELHI || s === S_MOUNTAIN_PASSES || s === S_PUNJAB || is_faction_control(s, DS)) if (!is_space_agnostic && !is_space) return false if (has_piece(AVAILABLE, DS, ELITE)) return true if (has_piece(s, DS, ELITE) && has_piece(AVAILABLE, DS, DISC) && !has_piece(s, DS, DISC)) return true if (has_piece(s, DS, ELITE) && has_units_enemy_faction(s, DS)) return true return false } function goto_govern() { init_command("Govern") game.state = "govern" } function goto_govern_space() { push_summary() if (count_pieces(game.cmd.where, DS, ELITE) === 0) { let p = find_piece(AVAILABLE, DS, ELITE) log_summary_place(p) place_piece(p, game.cmd.where) end_govern_space() } else { game.cmd.count = [0, 0] game.state = "govern_space" } } states.govern = { inactive: "Govern", prompt() { if (game.vm && "ignore_loc_req" in game.vm && game.vm.ignore_loc_req) view.prompt = "Govern: Select any space, ignoring location requirements." else view.prompt = "Govern: Select a Tributary, Sultanate Controlled Province, Mongol Invasion Region, or Delhi." if (can_select_cmd_space(1) && can_govern()) for (let s = first_space; s <= last_space; ++s) if (!is_selected_cmd_space(s) && can_govern_in_space(s)) gen_action_space(s) view.actions.end_govern = prompt_end_cmd(1) }, space(s) { push_undo() select_cmd_space(s, 1) log_space(game.cmd.where, "Govern") goto_govern_space() }, end_govern: end_command, } states.govern_space = { inactive: "Govern", prompt() { view.prompt = "Govern: Place a Governor OR place a Qasbah and/or remove two Obedient Units." view.where = game.cmd.where view.actions.next = 1 let can_place_qasbah = (has_piece(AVAILABLE, DS, DISC) && has_piece(game.cmd.where, DS, ELITE) && !has_piece(game.cmd.where, DS, DISC)) let can_place_governor = (has_piece(AVAILABLE, DS, ELITE) && game.cmd.count[0] + game.cmd.count[1] === 0) let can_remove = (has_piece(game.cmd.where, DS, ELITE) && game.cmd.count[1] < 2 && has_units_enemy_faction(game.cmd.where, DS)) if (can_place_governor) gen_place_piece(DS, ELITE) if (can_place_qasbah) gen_place_piece(DS, DISC) if (can_remove) { gen_action_obedient_in_space(game.cmd.where, BK) gen_action_obedient_in_space(game.cmd.where, VE) } if (!can_place_governor && !can_place_qasbah && !can_remove) view.prompt = "Govern: Done." }, piece(p) { if (piece_faction(p) === DS) { log_summary_place(p) place_piece(p, game.cmd.where) if (piece_type(p) === ELITE) end_govern_space() else { game.cmd.count[0] += 1 } } else { log_summary_remove(p) remove_piece(p) game.cmd.count[1] += 1 } }, next: end_govern_space } function end_govern_space() { pop_summary() if (game.vm && game.cmd.limited) end_command() else game.state = "govern" } /* REBEL COMMANDS */ function can_migrate() { if (count_pieces_on_map(game.current, ELITE) > 0) return true return false } function can_migrate_in_space(s) { for (let ss of SPACES[s].adjacent) if (has_unmoved_piece(ss, game.current)) return true return false } function goto_migrate() { init_command("Migrate") game.cmd.pieces = [] game.cmd.shift = false game.state = "migrate" } states.migrate = { inactive: "Migrate", prompt() { view.prompt = "Migrate: Select a Province to move into." if (can_select_cmd_space(1) && can_migrate()) for (let s = first_space; s <= last_province; ++s) if (!is_selected_cmd_space(s) && can_migrate_in_space(s)) gen_action_space(s) view.actions.end_migrate = prompt_end_cmd(1) }, space(s) { push_undo() game.cmd.where = s select_cmd_space(s, 1) goto_migrate_space() }, end_migrate: end_command } function goto_migrate_space() { push_summary() game.state = "migrate_space" game.cmd.count = 0 } states.migrate_space = { inactive: "Migrate", prompt() { view.prompt = `Migrate: Move in up to three ${PIECE_FACTION_TYPE_NAME[game.current][ELITE]}s.` view.where = game.cmd.where for_each_movable(game.current, p => { if ( set_has(SPACES[game.cmd.where].adjacent, piece_space(p)) && !set_has(game.cmd.pieces, p) && game.cmd.count < 3 ) gen_action_piece(p) }) if (game.cmd.count > 0) view.actions.next = 1 if (game.vm && game.vm.fp === 62 && game.cmd.count === 0) view.actions.skip = 1 }, piece(p) { push_undo() log_summary_move_from(p) set_add(game.cmd.pieces, p) place_piece(p, game.cmd.where) if (is_tributary(game.cmd.where)) to_obedient(p) game.cmd.count += 1 }, next() { log_space(game.cmd.where, "Migrate") pop_summary() let f = (game.current === BK) ? VE : BK if (game.vm) { for (let p of game.cmd.pieces) { set_add(game.vm.pp, p); } } if (!game.cmd.shift && has_piece_faction(game.cmd.where, f)) { game.state = "migrate_shift_influence" } else if (game.vm) { vm_next() } else { game.state = "migrate" } }, skip() { if (game.summary) game.summary = null vm_next() } } states.migrate_shift_influence = { inactive: "Migrate", prompt() { if (game.resources[game.current] > 0 ) { view.prompt = "Migrate: Pay a Resource to Shift Influence." gen_action_resources(game.current) } else { view.prompt = "Migrate: Not enough Resources to Shift Influence." } view.actions.skip = 1 }, resources() { push_undo() let f = (game.current === BK) ? VE : BK log_br() add_resources(game.current, -1) add_influence(game.current) game.cmd.shift = true game.inf_shift = { "next": "migrate", } remove_influence(f) }, skip() { game.cmd.shift = true end_migrate_shift_influence() } } function end_migrate_shift_influence() { if (game.vm) { vm_next() } else { game.state = "migrate" } } function can_rally() { return has_piece(AVAILABLE, game.current, ELITE) } function can_rally_in_space(s) { if (game.current === BK) { return (s === S_MAHARASHTRA || has_piece(s, BK, ELITE) || has_piece(s, BK, DISC)) } else if (game.current === VE) { return (s === S_KARNATAKA || has_piece(s, VE, ELITE) || has_piece(s, VE, DISC)) } } function rally_count() { let count = 1 if (game.current === BK) { count += (game.cmd.where === S_MAHARASHTRA ? 1 : 0) } else if (game.current === VE) { count += (game.cmd.where === S_KARNATAKA ? 1 : 0) count += (has_piece(game.cmd.where, VE, DISC) ? 1 : 0) } count += (is_faction_control(game.cmd.where, game.current) ? 1 : 0) return count } function goto_rally() { init_command("Rally") game.state = "rally" } function goto_rally_space() { push_summary() game.cmd.count = rally_count() let p = find_piece(AVAILABLE, game.current, ELITE) if (game.cmd.count === 1 && p > -1) { log_summary_place(p) place_piece(p, game.cmd.where) end_rally_space() } else { game.state = "rally_space" } } function end_rally_space() { pop_summary() game.state = "rally" } states.rally = { inactive: "Rally", prompt() { if (game.current === BK) view.prompt = "Rally: Select Maharashtra or a Province with your pieces." else if (game.current === VE) view.prompt = "Rally: Select Karnataka or a Province with your pieces." if (can_select_cmd_space(1) && can_rally()) { for (let s = first_space; s <= last_space; ++s) { if (!is_selected_cmd_space(s) && can_rally_in_space(s)) gen_action_space(s) } } view.actions.end_rally = prompt_end_cmd(1) }, space(s) { push_undo() select_cmd_space(s, 1) log_space(s, "Rally") goto_rally_space() }, end_rally: end_command, } states.rally_space = { inactive: "Rally", prompt() { if (find_piece(AVAILABLE, game.current, ELITE) === -1) { view.prompt = `Rally: No available ${PIECE_FACTION_TYPE_NAME[game.current][ELITE]}.` view.actions.next = 1 } else { view.prompt = `Rally: Place up to ${format_unit_count(game.current, ELITE, game.cmd.count)}.` view.where = game.cmd.where gen_place_piece(game.current, ELITE) } view.actions.next = 1 }, piece(p) { push_undo() log_summary_place(p) place_piece(p, game.cmd.where) game.cmd.count -= 1 if (game.cmd.count === 0) end_rally_space() }, next() { end_rally_space() } } /* DECREES */ function prompt_end_decree() { if (game.decree.count === 0) { view.prompt = game.decree.type + ": Done." return 1 } return 0 } function init_decree(type) { if (!is_succession()) { push_undo() log_h2(faction_short[game.current] + " - " + type) } game.decree = { spaces: [], pieces: [], type: type, } } function init_free_decree(type, s) { game.decree = { type: type, limited: s >= 0 ? 1 : 0, free: 1, spaces: [], pieces: [], where: s, } if (s >= 0) set_add(game.decree.spaces, s) } function gen_any_decree() { if (game.current === DS) { view.actions.collect = can_collect() ? 1 : 0 view.actions.campaign = can_campaign() ? 1 : 0 view.actions.demand = can_demand() ? 1 : 0 } else if (game.current === BK) { view.actions.trade = can_trade() ? 1 : 0 view.actions.build = can_build() ? 1 : 0 view.actions.conspire = can_conspire() ? 1 : 0 } else if (game.current === VE) { view.actions.tax = can_tax() ? 1 : 0 view.actions.build = can_build() ? 1 : 0 view.actions.compel = can_compel() ? 1 : 0 } } function can_demand() { for (let s = first_space; s <= last_province; ++s) if (can_demand_in_space(s)) return true return false } function can_demand_in_space(s) { return is_faction_control(s, DS) && has_piece(s, DS, ELITE) } function can_tax() { return true } function tax_count() { return count_pieces_on_map(VE, DISC) + game.prosperity[VE] } /* DELHI SULTANATE DECREES */ function can_collect() { return collect_count() > 0 } function collect_count() { let c = Math.floor(game.prosperity[DS]) return Math.floor(game.prosperity[0] / 2) } function goto_collect() { init_decree("Collect Tribute") game.decree.count = 1 game.state = "collect" } states.collect = { inactive: "Collect Tribute", prompt() { view.prompt = `Collect Tribute: Collect ${collect_count()} from half the total Tributary Prosperity.` if (game.decree.count > 0) gen_action_resources(DS) view.actions.end_collect = prompt_end_decree() }, resources(f) { let c = collect_count() add_resources(DS, c) log_resources(DS, c) game.decree.count = 0 goto_cavalry(2, "collect") }, end_collect: end_decree, } function can_campaign() { if (count_pieces_on_map(DS, ELITE) > 0 || count_pieces_on_map(DS, TROOPS) > 0) return true return false } function can_start_campaign_in_space(s) { if (has_piece(s, DS, TROOPS) || has_piece(s, DS, ELITE)) return true return false } function goto_campaign() { init_decree("Campaign") game.decree.campaign = [] game.decree.selected = [] game.state = "campaign" } // TODO: precompute all distances from warangal! function is_n_from_warangal(n, s) { let queue = [ [ s, [] ] ] while (queue.length) { let [ node, path ] = queue.shift() if (path.length === n) { if (is_adjacent_to_city(C_WARANGAL, node)) return true } else if (path.length < n) { for (let neighbor of SPACES[node].adjacent) { // if (!set_has(path, neighbor) && !set_has(game.decree.campaign, neighbor)) if (!set_has(path, neighbor) && !game.decree.campaign.includes(neighbor)) { queue.push([ neighbor, [ ...path, neighbor ] ]) } } } } return false } states.campaign = { inactive: "Campaign", prompt() { view.campaign_spaces = game.decree.campaign.slice().sort((a, b) => a - b) if (game.vm && game.vm.fp === 13) { view.prompt = "Campaign: Select a route up to four spaces long, finishing adjacent to Warangal." if (game.decree.campaign.length === 0) { for (let s = first_space; s <= last_space; ++s) { if (can_start_campaign_in_space(s) && is_n_from_warangal(3, s)) gen_action_space(s) } } else if (game.decree.campaign.length < 4) { for (let s of SPACES[game.decree.campaign.at(-1)].adjacent) { if (!game.decree.campaign.includes(s) && is_n_from_warangal(3-game.decree.campaign.length, s)) gen_action_space(s) } if (game.decree.campaign.length > 1 && is_n_from_warangal(1, game.decree.campaign.at(-1))) view.actions.next = 1 } } else { view.prompt = "Campaign: Select a route up to four spaces long, from start to finish." if (game.decree.campaign.length === 0) { for (let s = first_space; s <= last_space; ++s) { if (can_start_campaign_in_space(s) && is_campaign_succ(s)) gen_action_space(s) } } else if (game.decree.campaign.length < 4) { for (let s of SPACES[game.decree.campaign.at(-1)].adjacent) { if (!game.decree.campaign.includes(s) && is_campaign_succ(s) && has_valid_campaign_connection(s)) gen_action_space(s) } if (game.decree.campaign.length > 1) view.actions.next = 1 } } if (is_succession()) view.actions.skip = 1 }, space(s) { push_undo() game.decree.campaign.push(s) if (game.decree.campaign.length === 4) { if (game.vm) game.vm.m = s game.state = "campaign_moves" } }, next() { push_undo() if (game.vm) game.vm.m = game.decree.campaign.at(-1) game.state = "campaign_moves" }, skip() { logi("Nothing") vm_next() } } states.campaign_moves = { inactive: "Campaign", prompt() { view.prompt = "Campaign: Move units along the campaign trail." view.campaign_spaces = game.decree.campaign.slice().sort((a, b) => a - b) view.who = game.decree.selected for_each_movable(DS, p => { if ( game.decree.campaign.slice(0, -1).includes(piece_space(p)) && !set_has(game.decree.pieces, p) ) gen_action_piece(p) }) if (game.decree.selected.length > 0) { let min_indx = Math.max(...game.decree.selected.map(p => { return game.decree.campaign.indexOf(piece_space(p)) })) for (let s of game.decree.campaign.slice(min_indx + 1)) { gen_action_space(s) } } if (game.decree.pieces.length > 0) { view.actions.end_campaign = 1 } }, piece(p) { if (!set_has(game.decree.selected, p)) set_add(game.decree.selected, p) else set_delete(game.decree.selected, p) }, space(s) { push_undo() log_space(s, "Campaign") push_summary() for (let p of game.decree.selected) { log_summary_place(p) place_piece(p, s) set_add(game.decree.pieces, p) } set_clear(game.decree.selected) pop_summary() }, end_campaign: end_decree } function is_campaign_succ(s) { if (game.vm && game.vm.fp > 73) if ([S_MOUNTAIN_PASSES, S_PUNJAB].includes(s)) return false return true } function has_valid_campaign_connection(s) { if (game.decree.campaign.length === 3) return true for (let ss of SPACES[s].adjacent) { if (!game.decree.campaign.includes(ss)) return true } return false } /* REBELS DECREES */ function can_build() { if (!has_piece(AVAILABLE, game.current, DISC)) return false for (let s = first_space; s <= last_province; ++s) if (can_build_in_space(s)) return true return false } function can_build_in_space(s) { return has_piece(s, game.current, ELITE) && !has_piece(s, game.current, DISC) } function goto_build() { init_decree("Build") game.decree.count = 1 game.state = "build" } states.build = { inactive: "Build", prompt() { if (game.current === BK) view.prompt = "Build: Select a Province with an Amir." else if (game.current === VE) view.prompt = "Build: Select a Province with a Raja." if (game.decree.count === 1) for (let s = first_space; s <= last_space; ++s) if (can_build_in_space(s)) gen_action_space(s) view.actions.end_build = prompt_end_decree() }, space(s) { push_undo() build_in_space(s) game.decree.count = 0 }, end_build: end_decree, } function build_in_space(s) { push_summary() let p = find_piece(AVAILABLE, game.current, DISC) log_summary_place(p) place_piece(p, s) log_space(s, "Build") pop_summary() } function can_compel() { for (let s = first_space; s <= last_province; ++s) if (can_compel_in_space(s)) return true return false } function can_compel_in_space(s) { if ( (has_piece_faction(s, VE) || SPACES[s].adjacent.some(ss => has_piece_faction(ss, VE))) && (has_piece(AVAILABLE, VE, ELITE) || SPACES[s].adjacent.some(ss => has_piece(ss, VE, ELITE))) ) return true return false } function goto_compel() { init_decree("Compel") game.state = "compel" } states.compel = { inactive: "Compel", prompt() { view.prompt = "Compel: Select a Province with or adjacent to your pieces." for (let s = first_space; s <= last_province; ++s) if (can_compel_in_space(s)) gen_action_space(s) }, space(s) { game.decree.where = s goto_compel_space() } } function goto_compel_space() { log_space(game.decree.where, "Compel") push_summary() game.decree.count = 2 game.state = "compel_space" } states.compel_space = { inactive: "Compel", prompt() { if (game.decree.count > 0) { view.prompt = "Compel: Move in or place up to two Rajas." gen_place_piece(game.current, ELITE) for (let s of SPACES[game.decree.where].adjacent) { gen_piece_in_space(s, VE, ELITE) } } else if (game.inf[VE] < 2) { view.prompt = "Compel: Done." view.actions.end_compel = 1 } else { view.prompt = "Compel: Exercise your influence over other kingdoms." } if (game.inf[VE] >= 2 && game.decree.count < 2) { view.actions.next = 1 } else if (game.inf[VE] < 2 && game.decree.count < 2) { view.actions.end_compel = 1 } }, piece(p) { push_undo() if (piece_space(p) === AVAILABLE) log_summary_place(p) else log_summary_move_from(p) let pp = place_piece(p, game.decree.where) set_add(game.decree.pieces, pp) game.decree.count -= 1 }, next() { pop_summary() game.state = "compel_space_influencial" }, end_compel: end_decree } states.compel_space_influencial = { inactive: "Compel", prompt() { view.prompt = "Compel: Remove one Troop, Governor, or Amir." gen_piece_in_space(game.decree.where, DS, TROOPS) gen_piece_in_space(game.decree.where, DS, ELITE) gen_piece_in_space(game.decree.where, BK, ELITE) if (count_faction_units(game.decree.where, DS) + count_faction_units(game.decree.where, BK) === 0) { view.prompt = "Compel: No Troops, Governors, or Amirs in the selected Province." view.actions.skip = 1 } }, piece(p) { logi("Removed " + piece_symbol(p) + " from S" + piece_space(p)) let f = piece_faction(p) remove_piece(p) if (f === DS) for (let r of game.decree.pieces) to_rebel(r) goto_cavalry(1, "end_decree") }, skip() { goto_cavalry(1, "end_decree") } } function can_conspire() { for (let s = first_space; s <= last_province; ++s) if (can_conspire_in_space(s)) return true return false } function can_conspire_in_space(s) { if (game.inf[BK] < 2) { if (has_piece_faction(s, BK) && has_piece(s, DS, ELITE)) return true } else if ( (has_piece(s, DS, ELITE) || has_piece(s, VE, ELITE)) && (has_piece_faction(s, BK) || SPACES[s].adjacent.some(ss => has_piece_faction(ss, BK))) ) return true return false } function goto_conspire() { init_decree("Conspire") game.decree.count = 2 game.state = "conspire" } states.conspire = { inactive: "Conspire", prompt() { if (game.decree.count === 0 || !can_conspire()) { view.prompt = "Conspire: Done." } else { if (game.decree.count === 2) view.actions.end_conspire = 0 view.prompt = game.inf[BK] < 2 ? "Conspire: Select a Province with your pieces to convert a Governor." : "Conspire: Select a Province with or adjacent to your pieces to convert a Governor or Raja."; for (let s = first_space; s <= last_province; ++s) if (can_conspire_in_space(s) && !set_has(game.decree.spaces, s)) gen_action_space(s) } view.actions.end_conspire ??= 1 }, space(s) { push_undo() game.decree.count -= 1 goto_conspire_space(s) }, end_conspire: end_decree } function goto_conspire_space(s) { log_space(s, "Conspire") game.decree.where = s set_add(game.decree.spaces, s) let is_ds = has_piece(s, DS, ELITE) let is_ve = has_piece(s, VE, ELITE) if (is_ds && !is_ve) goto_conspire_replace(find_piece(s, DS, ELITE)) else game.state = "conspire_space" } states.conspire_space = { inactive: "Conspire", prompt() { if (game.inf[BK] < 2) { view.prompt = "Conspire: Select a Governor to convert to an Amir." gen_piece_in_space(game.decree.where, DS, ELITE) } else { view.prompt = "Conspire: Select a Governor or Raja to convert to an Amir." gen_piece_in_space(game.decree.where, DS, ELITE) gen_piece_in_space(game.decree.where, VE, ELITE) } }, piece(p) { goto_conspire_replace(p) } } function goto_conspire_replace(p) { game.decree.replaced = p game.decree.is_rebel = is_rebel(p) remove_piece(p) game.state = "conspire_replace" } states.conspire_replace = { inactive: "Conspire", prompt() { if (game.decree.is_rebel === null) { view.prompt = `Conspire: Replace with an Obedient or Rebelling Amir.` view.actions.obedient = 1 view.actions.rebelling = 1 } else if (count_pieces(AVAILABLE, BK, ELITE) > 0) { view.prompt = `Conspire: Replace with ${game.decree.rebel ? "a Rebelling " : "an "} Amir?` gen_place_piece(BK, ELITE) } else { view.prompt = "Conspire: No available Amir." } view.actions.skip = 1 }, piece(p) { let amir = find_piece(AVAILABLE, BK, ELITE) game.decree.pp = place_piece(amir, game.decree.where) if (game.decree.rebel) to_rebel(game.decree.pp) if (!game.decree.rebel) { game.decree.is_rebel = null game.state = "conspire_replace" } else { logi(`Replaced a ${piece_symbol(game.decree.replaced)} for a Rebelling ${piece_symbol(game.decree.pp)}.`) game.state = "conspire" } }, obedient() { push_undo() logi(`Replaced a ${piece_symbol(game.decree.replaced)} for an ${piece_symbol(game.decree.pp)}.`) game.state = "conspire" }, rebelling() { push_undo() to_rebel(game.decree.pp) logi(`Replaced a ${piece_symbol(game.decree.replaced)} for a Rebelling ${piece_symbol(game.decree.pp)}.`) game.state = "conspire" }, skip() { logi("1 " + piece_symbol(game.decree.replaced) + " removed.") game.state = "conspire" } } function can_trade() { return true } function trade_count() { let count = 0 for (let s = first_space; s <= last_province; ++s) count += (count_pieces(s, BK, DISC) + count_pieces(s, BK, ELITE)) > 0 ? 1 : 0 return count } function goto_trade() { init_decree("Trade") game.decree.count = 1 game.state = "trade" } states.trade = { inactive: "Trade", prompt() { view.prompt = `Trade: Collect ${trade_count()} from Provinces where you have pieces.` if (game.decree.count === 1) gen_action_resources(BK) view.actions.end_trade = prompt_end_decree() }, resources(f) { let t = trade_count() add_resources(game.current, t) log_resources(BK, t) game.decree.count = 0 goto_cavalry(trade_cavalry_count(), "trade") }, end_trade: end_decree, } /* MONGOL INVADERS COMMANDS */ function gen_mi_command() { view.actions.amass = can_amass() ? 1 : 0 view.actions.advance = can_advance() ? 1 : 0 view.actions.mi_attack = can_mi_attack() ? 1 : 0 } function can_advance() { return (has_piece(S_MOUNTAIN_PASSES, MI, TROOPS) || has_piece(S_PUNJAB, MI, TROOPS)) } states.advance = { inactive: "Advance", prompt() { view.prompt = "Advance: March the Mongol Invaders towards Delhi." if (has_piece(S_MOUNTAIN_PASSES, MI, TROOPS)) gen_action_space(S_PUNJAB) if (has_piece(S_PUNJAB, MI, TROOPS)) gen_action_space(S_DELHI) }, space(s) { push_summary() game.cmd.where = s game.state = "advance_space" } } states.advance_space = { inactive: "Advance", prompt() { view.prompt = `Advance: Move one or more of Mongol Invaders into ${SPACE_NAME[game.cmd.where]}.` let origin = game.cmd.where === S_DELHI ? S_PUNJAB : S_MOUNTAIN_PASSES for (let p = first_piece[MI][TROOPS]; p <= last_piece[MI][TROOPS]; ++p) if (piece_space(p) === origin) gen_action_piece(p) if (game.cmd.pieces.length >= 1) view.actions.end_advance = 1 }, piece(p) { set_add(game.cmd.pieces, p) log_summary_move_from(p) place_piece(p, game.cmd.where) }, end_advance() { game.cmd.pieces = [] log_space(game.cmd.where, "Advance") pop_summary() goto_strategic_assistance() } } function can_amass() { return has_piece(AVAILABLE, MI, TROOPS) } states.amass = { inactive: "Amass", prompt() { view.prompt = "Amass: Place three Mongol Invaders in the Mountain Passes." gen_action_space(S_MONGOL_INVADERS) }, space(s) { amass() goto_strategic_assistance() } } function amass() { let n_available = Math.min(count_faction_pieces(AVAILABLE, MI), 3) let s = null for (let i = 0; i < n_available; ++i) { s = amass_space(s) let p = find_piece(AVAILABLE, MI, TROOPS) log_summary_place(p) place_piece(p, s) } if (game.summary) pop_summary() amass_trinkle_down(S_MOUNTAIN_PASSES, S_PUNJAB) amass_trinkle_down(S_PUNJAB, S_DELHI) } function amass_space(s) { let s2 = null if (count_faction_pieces(S_MOUNTAIN_PASSES, MI) < 4) s2 = S_MOUNTAIN_PASSES else if (count_faction_pieces(S_PUNJAB, MI) < 4) s2 = S_PUNJAB else s2 = S_DELHI if (s === s2) return s else { if (game.summary) pop_summary() log_space(s2, "Amass") push_summary() return s2 } } function amass_trinkle_down(s_source, s_dest) { let n = count_faction_pieces(s_source, MI) - 4 if (n > 0) { log_space(s_dest, "Amass") push_summary() for (let i = n; i > 0; --i) { let p = find_piece(s_source, MI, TROOPS) log_summary_place(p) place_piece(p, s_dest) } pop_summary() } } function can_mi_attack() { let p0 = first_piece[MI][TROOPS] let p1 = last_piece[MI][TROOPS] for (let p = p0; p <= p1; ++p) if (piece_space(p) >= 0) return true return false } states.mi_attack = { inactive: "Attack & Plunder", prompt() { view.prompt = "Attack & Plunder: Select a space to assault Sultanate forces." for (let s = first_space; s <= last_space; ++s) if (has_piece_faction(s, MI)) gen_action_space(s) }, space(s) { game.cmd.where = s if (count_faction_pieces(s, DS) > 0) { log_space(s, "Attack") game.cmd.attacker = MI game.cmd.target = DS set_add(game.cmd.spaces, s) goto_attack_space() } else { goto_plunder() } } } function goto_plunder() { if (count_pieces(game.cmd.where, MI, TROOPS) > 0) { if (game.cmd.where != S_MOUNTAIN_PASSES) change_current(DS) log_space(game.cmd.where, "Plunder") game.state = "plunder" } else end_plunder() } states.plunder = { inactive: "Attack & Plunder", prompt() { if (game.resources[DS] <= 3) { view.prompt = "Attack & Plunder: Plunder cannot reduce Delhi Sultanate Resources below 3." view.actions.next = 1 } else { let n = Math.min(game.resources[DS] - 3, count_pieces(game.cmd.where, MI, TROOPS)) if (game.active === DS) view.prompt = `Attack & Plunder: Lose ${n} Resources from the plundering Mongol Invaders!` else view.prompt = `Attack & Plunder: The Sultanate loses ${n} Resources from the plundering Mongol Invaders!` gen_action_resources(DS) } }, resources(f) { let n = Math.min(game.resources[DS] - 3, count_pieces(game.cmd.where, MI, TROOPS)) logi_resources(f, -n) add_resources(f, -n) goto_plunder_remove() }, next: goto_plunder_remove } function goto_plunder_remove() { game.cmd.count = count_pieces(game.cmd.where, MI, TROOPS) if (game.cmd.where === S_MOUNTAIN_PASSES || game.cmd.count === 0) end_plunder() else { if (game.cmd.where === S_DELHI) game.cmd.count *= 2 push_summary() change_current(DS) game.state = "plunder_remove" } } function has_troops_in_provinces() { for (let s = first_space; s <= last_province; ++s) { if (has_piece(s, DS, TROOPS)) return true } return false } states.plunder_remove = { inactive: "Attack & Plunder", prompt() { if (game.cmd.count > 0 && has_troops_in_provinces()) { view.prompt = `Attack & Plunder: Remove ${format_unit_count(DS, TROOPS, game.cmd.count)} from any Provinces.` for (let s = first_space; s <= last_province; ++s) { if (has_piece(s, DS, TROOPS)) gen_action_space(s) } } else { view.prompt = "Attack & Plunder: Done." view.actions.next = 1 } }, space(s) { push_undo() game.cmd.count -= 1 let p = find_piece(s, DS, TROOPS) log_summary_remove_from(p) remove_piece(p) }, next: end_plunder } function end_plunder() { if (game.summary && game.summary.length > 0) { pop_summary() } else if (game.summary) { game.summary = null } let n = count_pieces(game.cmd.where, MI, TROOPS) - 3 if (n > 0) { for (let i = 0; i < n; ++i) { let p = find_piece(game.cmd.where, MI, TROOPS) remove_piece(p) } log_br() log(`Returned ${n} CMI from S${game.cmd.where}.`) } if (game.cmd.free === 0 && game.inf[game.cmd.sa_faction] === 0) { game.state = "end_mongol_invasion" } else { change_current(game.cmd.sa_faction) goto_strategic_assistance() } } /* TRIBUTARY AND REBELS */ function add_tributary(s) { game.tributary |= (1 << s) to_obedient_space(s) update_control() } function is_tributary(s) { return game.tributary & (1 << s) } function is_faction_control(s, faction) { return game.control[faction] & (1 << s) } function is_rebel_faction(faction) { if (faction === BK || faction === VE) return true return false } function is_rebel_faction_elite(p) { return (piece_type(p) === ELITE && [BK, VE].includes(piece_faction(p))) } function other_rebel_faction(faction) { return faction === VE ? BK : VE } function remove_tributary(s) { game.tributary &= ~(1 << s) update_control() } function has_rebel(s, faction) { let p0 = first_piece[faction][ELITE] let p1 = last_piece[faction][ELITE] for (let p = p0; p <= p1; ++p) if (piece_space(p) === s) if (is_rebel(p)) return true return false } function is_rebel(p) { let faction = piece_faction(p) let piece_index = p - first_piece[faction][ELITE] return game.rebel[faction] & (1 << piece_index) } function to_obedient(p) { if (is_rebel_faction_elite(p)) { let faction = piece_faction(p) let piece_index = p - first_piece[faction][ELITE] game.rebel[faction] &= ~(1 << piece_index) } } function to_obedient_space(s) { to_obedient_space_faction(s, BK) to_obedient_space_faction(s, VE) } function to_obedient_space_faction(s, faction) { for_each_piece(faction, ELITE, p => { if (piece_space(p) === s) to_obedient(p) }) } function to_rebel(p) { let faction = piece_faction(p) let piece_index = p - first_piece[faction][ELITE] game.rebel[faction] |= (1 << piece_index) } function to_rebel_space(s, faction) { for_each_piece(faction, ELITE, p => { if (piece_space(p) === s) to_rebel(p) }) } function update_rebel(p, s) { let faction = piece_faction(p) if (is_faction_control(s, faction)) to_rebel(p) } function gen_action_obedient_in_space(s, faction) { for_each_piece(faction, ELITE, p => { if (piece_space(p) === s && !is_rebel(p)) gen_action_piece(p) }) } /* MISC SPACE + PIECE QUERIES */ function count_pieces(s, faction, type) { let n = 0 for_each_piece(faction, type, p => { if (piece_space(p) === s) ++n }) return n } function count_pieces_on_map(faction, type) { let n = 0 for_each_piece(faction, type, p => { if (piece_space(p) >= 0) ++n }) return n } function count_faction_pieces(s, faction) { switch (faction) { case DS: return count_pieces(s, DS, TROOPS) + count_pieces(s, DS, ELITE) + count_pieces(s, DS, DISC) case BK: return count_pieces(s, BK, ELITE) + count_pieces(s, BK, DISC) case VE: return count_pieces(s, VE, ELITE) + count_pieces(s, VE, DISC) case MI: return count_pieces(s, MI, TROOPS) } } function count_faction_units(s, faction) { switch (faction) { case DS: return count_pieces(s, DS, TROOPS) + count_pieces(s, DS, ELITE) case BK: return count_pieces(s, BK, ELITE) case VE: return count_pieces(s, VE, ELITE) case MI: return count_pieces(s, MI, TROOPS) } } function has_majority(s) { let d = count_faction_pieces(s, DS) let b = count_faction_pieces(s, BK) let v = count_faction_pieces(s, VE) let m = count_faction_pieces(s, MI) if (d > b + v + m) return DS else if (b > d + v + m) return BK else if (v > b + d + m) return VE else if (m > b + d+ v) return MI else return -1 } function find_piece(s, faction, type) { let p0 = first_piece[faction][type] let p1 = last_piece[faction][type] for (let p = p0; p <= p1; ++p) if (piece_space(p) === s) return p return -1 } function has_piece(s, faction, type) { return find_piece(s, faction, type) != -1 } function has_piece_faction(s, faction) { return count_faction_pieces(s, faction) > 0 } function has_piece_enemy_faction(s, faction) { return FACTIONS .filter(f => f != faction) .some(f => count_faction_pieces(s, f) > 0) } function has_units_enemy_faction(s, faction) { return FACTIONS .filter(f => f != faction) .some(f => count_faction_units(s, f) > 0) } function has_unmoved_piece(space, faction) { let unmoved = false for_each_movable(faction, p => { if (piece_space(p) === space) if (!game.cmd.pieces || game.cmd.pieces.length === 0) unmoved = true else if (!set_has(game.cmd.pieces, p)) unmoved = true }) return unmoved } function has_valid_attackers(s, faction) { let valid_attacker = false for_each_movable(faction, p => { if (piece_space(p) === s && (!game.cmd || !game.cmd.pieces || !set_has(game.cmd.pieces, p))) valid_attacker = true if (piece_space(p) === s || (has_piece(piece_space(p), faction, DISC) && SPACES[s].adjacent.includes(piece_space(p)))) if (!game.cmd.pieces || game.cmd.pieces.length === 0) valid_attacker = true else if (!set_has(game.cmd.pieces, p)) valid_attacker = true }) return valid_attacker } function can_support_from(s, faction) { let valid_attacker = false for_each_movable(faction, p => { if (piece_space(p) === s && has_piece(s, faction, DISC) && s !== game.cmd.where && !set_has(game.cmd.pieces, p)) valid_attacker = true }) return valid_attacker } function has_valid_support_attackers(s, faction) { for (let ss of SPACES[s].adjacent) if (can_support_from(ss, faction)) return true return false } function gen_place_piece(faction, type) { let can_place = false for_each_piece(faction, type, p => { if (piece_space(p) === AVAILABLE) { gen_action_piece(p) can_place = true } }) return can_place } function move_all_faction_piece_from(faction, type, from, to) { for_each_piece(faction, type, p => { if (piece_space(p) === from) place_piece(p, to) }) } function is_province(s) { return (s >= first_space && s <= last_province) } function is_adjacent_to_city(city, s) { return set_has(CITIES[city].adjacent, s) } function is_selected_cmd_space(s) { return game.cmd.spaces && set_has(game.cmd.spaces, s) } function is_selected_decree_space(s) { return game.decree.spaces && set_has(game.decree.spaces, s) } function place_piece(p, s) { if (piece_space(p) === AVAILABLE) p = find_piece(AVAILABLE, piece_faction(p), piece_type(p)) set_piece_space(p, s) update_control() update_rebel(p, s) return p } function remove_piece(p) { to_obedient(p) set_piece_space(p, AVAILABLE) update_control() } function piece_faction(p) { if (p >= first_piece[MI][TROOPS] && p <= last_piece[MI][TROOPS]) return MI if (p >= first_piece[DS][TROOPS] && p <= last_piece[DS][TROOPS]) return DS if (p >= first_piece[DS][ELITE] && p <= last_piece[DS][ELITE]) return DS if (p >= first_piece[BK][ELITE] && p <= last_piece[BK][ELITE]) return BK if (p >= first_piece[VE][ELITE] && p <= last_piece[VE][ELITE]) return VE if (p >= first_piece[DS][DISC] && p <= last_piece[DS][DISC]) return DS if (p >= first_piece[BK][DISC] && p <= last_piece[BK][DISC]) return BK if (p >= first_piece[VE][DISC] && p <= last_piece[VE][DISC]) return VE throw "IMPOSSIBLE - piece_faction" } function piece_symbol(p) { let f = piece_faction(p) let t = piece_type(p) if (PIECE_FACTION_TYPE_SYMBOL[f][t] !== null) return PIECE_FACTION_TYPE_SYMBOL[f][t] else return PIECE_FACTION_TYPE_NAME[f][t] } function piece_name(p) { return PIECE_FACTION_TYPE_NAME[piece_faction(p)][piece_type(p)] } function faction_type_symbol(f, t) { if (PIECE_FACTION_TYPE_SYMBOL[f][t] !== null) return PIECE_FACTION_TYPE_SYMBOL[f][t] else return PIECE_FACTION_TYPE_NAME[f][t] } function piece_type(p) { if (p >= first_piece[MI][TROOPS] && p <= last_piece[MI][TROOPS]) return TROOPS if (p >= first_piece[DS][TROOPS] && p <= last_piece[DS][TROOPS]) return TROOPS if (p >= first_piece[DS][ELITE] && p <= last_piece[DS][ELITE]) return ELITE if (p >= first_piece[BK][ELITE] && p <= last_piece[BK][ELITE]) return ELITE if (p >= first_piece[VE][ELITE] && p <= last_piece[VE][ELITE]) return ELITE if (p >= first_piece[DS][DISC] && p <= last_piece[DS][DISC]) return DISC if (p >= first_piece[BK][DISC] && p <= last_piece[BK][DISC]) return DISC if (p >= first_piece[VE][DISC] && p <= last_piece[VE][DISC]) return DISC throw "IMPOSSIBLE - piece_type" } function has_governor(s) { return has_piece(s, DS, ELITE) } function has_amir(s) { return has_piece(s, BK, ELITE) } function has_fort(s) { return has_piece(s, BK, DISC) } function has_qasbah(s) { return has_piece(s, DS, DISC) } function has_temple(s) { return has_piece(s, VE, DISC) } function has_ds_unit(s) { return has_piece(s, DS, ELITE) || has_piece(s, DS, TROOPS) } function has_mi_unit(s) { return has_piece(s, MI, TROOPS) } function is_adjacent(a, b) { return set_has(SPACES[a].adjacent, b) } function is_enemy_piece(p) { return piece_faction(p) !== game.current } function is_enemy_unit(p) { return is_enemy_piece(p) && (piece_type(p) !== DISC) } function is_piece(p, faction, type) { return p >= first_piece[faction][type] && p <= last_piece[faction][type] } function is_mongol_invader(p) { return piece_faction(p) === MI } function is_ds_unit(p) { return (is_piece(p, DS, ELITE) || is_piece(p, DS, TROOPS)) } function is_troop(p) { return piece_faction(p) === DS && piece_type(p) === TROOPS } function is_governor(p) { return piece_faction(p) === DS && piece_type(p) === ELITE } function is_amir(p) { return piece_faction(p) === BK && piece_type(p) === ELITE } function is_raja(p) { return piece_faction(p) === VE && piece_type(p) === ELITE } function is_fort(p) { return piece_faction(p) === BK && piece_type(p) === DISC } function is_qasbah(p) { return piece_faction(p) === DS && piece_type(p) === DISC } function is_temple(p) { return piece_faction(p) === VE && piece_type(p) === DISC } function is_piece_in_event_space(p) { return piece_space(p) === game.vm.s } function is_piece_on_map(p) { return piece_space(p) > AVAILABLE && piece_space(p) <= S_MONGOL_INVADERS } /* EVENT SPECIFIC */ function has_majority_goa() { let c = [ count_faction_pieces(S_MAHARASHTRA, DS) + count_faction_pieces(S_KARNATAKA, DS), count_faction_pieces(S_MAHARASHTRA, BK) + count_faction_pieces(S_KARNATAKA, BK), count_faction_pieces(S_MAHARASHTRA, VE) + count_faction_pieces(S_KARNATAKA, VE) ] let max = Math.max(...c) return c.filter(n => n === max).length > 1 ? 1 : c.indexOf(max) } function n_province_with_both_rebels() { let n = 0 for (let s = first_space; s <= last_province; ++s) { if (has_piece_faction(s, VE) && has_piece_faction(s, BK)) n += 1 } return n } /* CAVALRY */ function n_available_cavalry() { let n = 0 for (let c = 0; c <= LAST_CAVALRY; ++c) if (game.cavalry[c] === AVAILABLE) n += 1 return n } function gen_take_cavalry(faction) { let can_place = false for (let c = 0; c <= LAST_CAVALRY; ++c) { if (game.cavalry[c] === AVAILABLE) { gen_action_token(c) can_place = true } } if (!can_place && faction === game.current) { for (let c = 0; c <= LAST_CAVALRY; ++c) { if (game.cavalry[c] !== faction) gen_action_token(c) } } return can_place } function gen_action_faction_cavalry(faction) { for (let c = 0; c <= LAST_CAVALRY; ++c) if (game.cavalry[c] === faction) gen_action_token(c) } function set_cavalry_faction(c, f) { game.cavalry[c] = f } function trade_cavalry_count() { if (game.inf[BK] === 0) return 1; if (game.inf[BK] <= 2) return 2; return 3; } function find_cavalry(faction) { for (let c = 0; c <= LAST_CAVALRY; ++c) if (game.cavalry[c] === faction) return c return -1 } function has_cavalry(faction) { if (!game.cmd.cavalry && faction === MI) return true return find_cavalry(faction) != -1 } function n_cavalry(faction) { return game.cavalry.filter(c => c === faction).length } function use_cavalry(faction) { let c = find_cavalry(faction) game.cavalry[c] = AVAILABLE } /* INFLUENCE */ function add_influence(faction) { if (game.inf[faction] === 4) return game.inf[faction]++ log(`Increased ${INF_TOKEN[faction]} to ${game.inf[faction]}.`) update_vp() if (faction === BK && game.inf[faction] === 2) move_all_faction_piece_from(BK, ELITE, S_BK_INF_2, AVAILABLE) else if (faction === BK && game.inf[faction] === 4) move_all_faction_piece_from(BK, ELITE, S_BK_INF_4, AVAILABLE) else if (faction === VE) move_all_faction_piece_from(VE, ELITE, S_VE_INF_1 + game.inf[faction] - 1, AVAILABLE) } function remove_influence(faction) { if (game.inf[faction] === 0) { end_influence() } else { game.inf[faction]-- log(`Decreased ${INF_TOKEN[faction]} to ${game.inf[faction]}.`) update_vp() if (faction === BK && game.inf[faction] === 3) troops_to_inf_track(BK, S_BK_INF_4) else if (faction === BK && game.inf[faction] === 1) troops_to_inf_track(BK, S_BK_INF_2) else if (faction === VE) troops_to_inf_track(VE, S_VE_INF_1 + game.inf[faction]) else end_influence() } } function end_influence() { if (game.vm && game.vm.fp === 80) game.state = "vm_influence_succession" else if (game.vm) vm_next() else { if (game.inf_shift.next === "goto_cavalry") goto_cavalry() else if (game.inf_shift.next === "vm_next") vm_next() else game.state = game.inf_shift.next } } function troops_to_inf_track(faction, s) { let n_available = Math.min(count_pieces(AVAILABLE, faction, ELITE), 2) let moved = 0 let p = null while (n_available > moved) { p = find_piece(AVAILABLE, faction, ELITE) place_piece(p, s) moved++ } if (moved < 2) { goto_deccan_influence_track(faction, 2-moved, s) } else { end_influence() } } function goto_deccan_influence_track(faction, n_troops, s) { push_summary() if (game.inf_shift === null) game.inf_shift = {} game.inf_shift.n = n_troops game.inf_shift.s = s game.inf_shift.save_current = game.current change_current(faction) game.state = "deccan_influence_track" } states.deccan_influence_track = { inactive: `Deccan Influence Track`, prompt() { if (game.inf_shift.n > 0) { view.prompt = `Deccan Influence Dwindles: Return ${game.inf_shift.n} ${PIECE_FACTION_TYPE_NAME[game.current][ELITE]}${game.inf_shift.n > 1 ? "s" : ""} to the track.` for_each_piece(game.current, ELITE, p => { if (piece_space(p) >= 0 && piece_space(p) <= S_PUNJAB) gen_action_piece(p) }) } else { view.prompt = "Deccan Influence Dwindles: Done." view.actions.end_return = 1 } }, piece(p) { push_undo() log_summary_remove_from(p) to_obedient(p) place_piece(p, game.inf_shift.s) game.inf_shift.n -= 1 }, end_return() { pop_summary() change_current(game.inf_shift.save_current) end_influence() } } /* UTILS */ function add_resources(faction, n) { game.resources[faction] = Math.max(0, Math.min(24, game.resources[faction] + n)) } function update_control() { game.control = [0, 0, 0] game.prosperity = [0, 0, 0] for (let s = first_space; s <= last_province; ++s) { if (is_tributary(s)) game.prosperity[DS] += SPACES[s].pop else { let f = has_majority(s) if (f <= VE) game.control[f] |= (1 << s) if (f === BK || f === VE) { game.prosperity[f] += SPACES[s].pop to_rebel_space(s, f) } } } update_vp() } function update_vp() { game.vp[DS] = game.prosperity[DS] game.vp[BK] = game.prosperity[BK] + count_pieces_on_map(BK, DISC) + game.inf[BK] game.vp[VE] = game.prosperity[VE] + count_pieces_on_map(VE, DISC) + game.inf[VE] } /* ACTIONS */ function gen_action(action, argument) { if (!(action in view.actions)) view.actions[action] = [] set_add(view.actions[action], argument) } function gen_action_die(d) { gen_action("die", d) } function gen_action_influence(faction) { gen_action("influence", faction) } function gen_action_piece(p) { gen_action("piece", p) } function gen_action_resources(faction) { gen_action("resources", faction) } function gen_action_space(s) { gen_action("space", s) } function gen_action_token(t) { gen_action("token", t) } function gen_action_faction(f) { gen_action("space", 17+f) } function gen_choose_faction(faction) { if (game.current === faction) { switch (faction) { case DS: view.actions.ds = 1 break case BK: view.actions.bk = 1 break case VE: view.actions.ve = 1 break } } else { switch (faction) { case DS: view.actions.choose_ds = 1 break case BK: view.actions.choose_bk = 1 break case VE: view.actions.choose_ve= 1 break } } } /* NEGOTIATION */ function is_player_ds() { return game.current === DS } function is_player_bk() { return game.current === BK } function is_player_ve() { return game.current === VE } function action_ask_resources() { push_undo() game.transfer = { current: game.current, state: game.state, count: 0, } game.state = "ask_resources" } states.ask_resources = { disable_negotiation: true, prompt() { view.prompt = "Negotiate: Ask another faction for Resources?" if (!is_player_ds() && game.resources[DS] > 0) gen_action_resources(DS) if (!is_player_bk() && game.resources[BK] > 0) gen_action_resources(BK) if (!is_player_ve() && game.resources[VE] > 0) gen_action_resources(VE) }, resources(faction) { change_current(faction) game.state = "give_resources" }, } states.give_resources = { inactive: "Transfer Resources", disable_negotiation: true, prompt() { view.prompt = `Negotiate: ${faction_name[game.transfer.current]} asked for Resources.` if (game.resources[game.current] >= 1 && game.resources[game.transfer.current] < 24) gen_action_resources(game.current) if (game.transfer.count > 0) { view.actions.done = 1 view.actions.undo = 1 } else { view.actions.deny = 1 view.actions.undo = 0 } }, resources(_) { game.transfer.count++ add_resources(game.current, -1) add_resources(game.transfer.current, 1) }, undo() { add_resources(game.current, game.transfer.count) add_resources(game.transfer.current, -game.transfer.count) game.transfer.count = 0 }, deny() { log_transfer(`${faction_short[game.current]} denied request from ${faction_short[game.transfer.current]}.`) end_negotiation() }, done() { clear_undo() log_transfer_resources(game.current, game.transfer.current, game.transfer.count) end_negotiation() }, } function action_transfer_resources() { push_undo() game.transfer = { current: game.current, state: game.state, count: null } game.state = "transfer_resources" } states.transfer_resources = { disable_negotiation: true, prompt() { view.prompt = "Transfer Resources to another faction." if (game.resources[game.current] >= 1) { if (!is_player_ds() && game.resources[DS] < 24) gen_action_resources(DS) if (!is_player_bk() && game.resources[BK] < 24) gen_action_resources(BK) if (!is_player_ve() && game.resources[VE] < 24) gen_action_resources(VE) } if (game.transfer.count) view.actions.done = 1 else view.actions.done = 0 }, resources(to) { if (!game.transfer.count) game.transfer.count = [0, 0, 0, 0] game.transfer.count[to]++ add_resources(game.current, -1) add_resources(to, 1) }, done() { for (let i = 0; i < 4; ++i) if (game.transfer.count[i] > 0) log_transfer_resources(game.current, i, game.transfer.count[i]) end_negotiation() }, } function can_ask_cavalry() { return game.cavalry.some(c => ![-1, game.current].includes(c)) } function action_ask_cavalry() { push_undo() game.transfer = { current: game.current, state: game.state, count: 0, } game.state = "ask_cavalry" } states.ask_cavalry = { disable_negotiation: true, prompt() { view.prompt = "Negotiate: Ask another faction for Cavalry?" if (!is_player_ds() && n_cavalry(DS) > 0) { gen_action_faction_cavalry(DS) gen_action_faction(DS) } if (!is_player_bk() && n_cavalry(BK) > 0) { gen_action_faction_cavalry(BK) gen_action_faction(BK) } if (!is_player_ve() && n_cavalry(VE) > 0) { gen_action_faction_cavalry(VE) gen_action_faction(VE) } }, token(c) { change_current(game.cavalry[c]) game.state = "give_cavalry" }, space(s) { let f = s - 17 change_current(f) game.state = "give_cavalry" } } states.give_cavalry = { inactive: "Transfer Cavalry", disable_negotiation: true, prompt() { view.prompt = `Negotiate: ${faction_name[game.transfer.current]} asked for Cavalry.` if (n_cavalry(game.current) >= 1) gen_action_faction_cavalry(game.current) if (game.transfer.count > 0) { view.actions.done = 1 view.actions.undo = 1 } else { view.actions.deny = 1 view.actions.undo = 0 } }, token(c) { push_undo() game.transfer.count++ set_cavalry_faction(c, game.transfer.current) }, deny() { log_transfer(`${faction_short[game.current]} denied request from ${faction_short[game.transfer.current]}.`) end_negotiation() }, done() { clear_undo() log_transfer_cavalry(game.current, game.transfer.current, game.transfer.count) end_negotiation() }, } function action_transfer_cavalry() { push_undo() game.transfer = { current: game.current, state: game.state, count: null } game.state = "transfer_cavalry" } states.transfer_cavalry = { disable_negotiation: true, prompt() { view.prompt = "Transfer Cavalry to another faction." if (n_cavalry(game.current) >= 1) { if (!is_player_ds()) gen_action_faction(DS) if (!is_player_bk()) gen_action_faction(BK) if (!is_player_ve()) gen_action_faction(VE) } if (game.transfer.count) view.actions.done = 1 else view.actions.done = 0 }, space(s) { let to = s - 17 if (!game.transfer.count) game.transfer.count = [0, 0, 0] let c = find_cavalry(game.current) game.transfer.count[to]++ set_cavalry_faction(c, to) }, done() { for (let i = 0; i < 3; ++i) if (game.transfer.count[i] > 0) log_transfer_cavalry(game.current, i, game.transfer.count[i]) end_negotiation() }, } function end_negotiation() { change_current(game.transfer.current) game.state = game.transfer.state delete game.transfer } /* PING */ function action_ping() { if (!game.ping) game.ping = { save_current: game.current, save_state: game.state } game.state = "ping" } states.ping = { disable_negotiation: true, inactive: "Ping", prompt() { view.prompt = "Ping which faction to respond to chat?" if (game.current !== DS) view.actions.ds = 1 if (game.current !== BK) view.actions.bk = 1 if (game.current !== VE) view.actions.ve = 1 view.actions.undo = 1 }, ds() { change_current(DS) game.state = "pong" }, bk() { change_current(BK) game.state = "pong" }, ve() { change_current(VE) game.state = "pong" }, undo() { states.pong.resume() }, } states.pong = { disable_negotiation: true, inactive: "Ping", prompt() { view.prompt = faction_name[game.ping.save_current] + " has requested your response in chat." view.actions.resume = 1 view.actions.undo = 0 }, resume() { change_current(game.ping.save_current) game.state = game.ping.save_state delete game.ping }, } /* LOGGING */ function log(msg) { game.log.push(msg) } function logi(msg) { log(">" + msg) } function log_br() { if (game.log.length > 0 && game.log[game.log.length - 1] !== "") game.log.push("") } function log_h1(msg) { log_br() log(".h1 " + msg) log_br() } function log_h2(msg) { log_br() log(".h2 " + msg) log_br() } function log_action(msg) { log_br() log(".h3 " + msg) } function log_transfer(msg) { log_br() log(".i " + msg) log_br() } function log_transfer_resources(from, to, n) { log_transfer(`${faction_short[from]} gave ${n} RES to ${faction_short[to]}.`) } function log_transfer_cavalry(from, to, n) { log_transfer(`${faction_short[from]} gave ${n} CAV to ${faction_short[to]}.`) } function log_space(s, action) { if (action) log_action("S" + s + " - " + action) else log_action("S" + s) } function push_summary() { if (game.summary) { console.log("SUMMARY ", game.summary) throw "TOO MANY SUMMARIES" } game.summary = [] } function log_summary(msg) { for (let item of game.summary) { if (item[1] === msg) { item[0]++ return } } game.summary.push([1, msg]) } function pop_summary() { if (game.summary.length > 0) { for (let [n, msg] of game.summary) { log(">" + msg.replace("%", String(n))) } } else { log(">Nothing") } game.summary = null } function upop_summary(msg="Nothing") { if (game.summary.length > 0) { for (let [n, msg] of game.summary) { log(msg.replace("%", String(n))) } } else { log(msg) } game.summary = null } function placed_summary(type="") { if (game.summary.length > 0) { for (let [n, msg] of game.summary) { log(msg.replace("%", String(n)) + " in S" + game.vm.s + ".") } } else { log("Declined placing " + type + " in S" + game.vm.s + ".") } game.summary = null } function log_summary_place(p) { let from = piece_space(p) if (from !== AVAILABLE) log_summary("Moved % " + piece_symbol(p) + " from S" + from) else log_summary("Placed % " + piece_symbol(p)) } function log_summary_move_from(p) { log_summary("Moved % " + piece_symbol(p) + " from S" + piece_space(p)) } function log_summary_remove_from(p) { log_summary("Removed % " + piece_symbol(p) + " from S" + piece_space(p)) } function log_summary_remove(p) { log_summary("Removed % " + piece_symbol(p)) } function log_summary_cavalry(c) { let from = game.cavalry[c] if (from !== AVAILABLE) log_summary(faction_flags[game.current] + " +% CAV from " + faction_flags[from] + ".") else log_summary(faction_flags[game.current] + " +% CAV from supply.") } function log_summary_resources(faction) { log_summary(faction_flags[faction] + " +% RES") } function logi_resources(faction, n) { if (n >= 0) logi(faction_flags[faction] + " +" + n + " RES") else logi(faction_flags[faction] + " " + n + " RES" ) } function log_resources(faction, n) { if (n >= 0) log(faction_flags[faction] + " +" + n + " RES") else log(faction_flags[faction] + " " + n + " RES") } function format_unit_count(faction, type, n) { return `${n} ${PIECE_FACTION_TYPE_NAME[faction][type]}${n > 1 ? "s" : ""}` } // === MISC PIECE QUERIES === function piece_space(p) { return game.pieces[p] } function set_piece_space(p, s) { game.pieces[p] = s } /* COMMON LIBRARY */ 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) { // An MLCG using integer arithmetic with doubles. // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf // m = 2**35 − 31 return (game.seed = game.seed * 200105 % 34359738337) % range } function random_bigint(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 } function shuffle(list) { // Fisher-Yates shuffle for (let i = list.length - 1; i > 0; --i) { let j = random(i + 1) let tmp = list[j] list[j] = list[i] list[i] = tmp } } function shuffle_bigint(list) { // Fisher-Yates shuffle for (let i = list.length - 1; i > 0; --i) { let j = random_bigint(i + 1) let tmp = list[j] list[j] = list[i] list[i] = tmp } } // 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_remove_item(array, item) { let n = array.length for (let i = 0; i < n; ++i) if (array[i] === item) return array_remove(array, i) } 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_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 } 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 } } } function set_toggle(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 } } array_insert(set, a, item) } // 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, 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 { array_remove_pair(map, m<<1) return } } } function object_diff(a, b) { if (a === b) return false if (a !== null && b !== null && typeof a === "object" && typeof b === "object") { if (Array.isArray(a)) { if (!Array.isArray(b)) return true let a_length = a.length if (b.length !== a_length) return true for (let i = 0; i < a_length; ++i) if (object_diff(a[i], b[i])) return true return false } for (let key in a) if (object_diff(a[key], b[key])) return true for (let key in b) if (!(key in a)) return true return false } return true } // same as Object.groupBy function object_group_by(items, callback) { let groups = {} if (typeof callback === "function") { for (let item of items) { let key = callback(item) if (key in groups) groups[key].push(item) else groups[key] = [ item ] } } else { for (let item of items) { let key = item[callback] if (key in groups) groups[key].push(item) else groups[key] = [ item ] } } return groups } // === ITERATORS AND ACTION GENERATORS === function auto_place_piece(s, faction, type, is_rebel) { if (can_place_piece(s, faction, type)) { let p = find_piece(AVAILABLE, faction, type) if (p >= 0) { place_piece(p, s) if (is_rebel) to_rebel(p) return true } } return false } function can_place_piece(s, faction, type) { return can_stack_piece(s, faction, type) && has_piece(AVAILABLE, faction, type) } function can_stack_piece(s, faction, type) { if (type === DISC) return !has_piece(s, faction, type) return true } function for_each_piece(faction, type, f) { let p0 = first_piece[faction][type] let p1 = last_piece[faction][type] for (let p = p0; p <= p1; ++p) f(p) } function gen_piece_in_space(space, faction, type) { for_each_piece(faction, type, p => { if (piece_space(p) === space) gen_action_piece(p) }) } function gen_faction_in_space(space, faction) { if (faction === BK || faction === VE || faction === DS) { gen_piece_in_space(space, faction, ELITE) gen_piece_in_space(space, faction, DISC) } if (faction === DS || faction === MI) gen_piece_in_space(space, faction, TROOPS) } function for_each_movable(faction, f) { if (faction === BK) for_each_piece(BK, ELITE, f) else if (faction === VE) for_each_piece(VE, ELITE, f) else if (faction === DS) { for_each_piece(DS, TROOPS, f) for_each_piece(DS, ELITE, f) } else if (faction === MI) { for_each_piece(MI, TROOPS, f) } } // === EVENTS === function gen_any_event() { if (set_has(SINGLE_EVENTS, this_card())) { view.actions.event = 1 } else { view.actions.unshaded = 1 view.actions.shaded = 1 } if (game.of_gods_and_kings[1] === game.current) { view.actions.gk_shaded = 1 view.actions.gk_unshaded = 1 } } function goto_gk_event(shaded) { push_undo() let c = game.of_gods_and_kings[0] game.of_gods_and_kings[2] = 1 game.cmd = 0 if (shaded) { log_h2(faction_short[game.current] + " - Shaded Event") log(faction_flags[game.current] + " used the set-aside Event Card.") log_br() log(".h3 C" + c) log(">.i " + data.card_flavor_shaded[c] + ".") log_br() goto_vm(c * 2 + 1) } else { log_h2(faction_short[game.current] + " - Event") log(faction_flags[game.current] + " used the set-aside Event Card.") log_br() log(".h3 C" + c) if (data.card_flavor[c]) log(">.i " + data.card_flavor[c] + ".") log_br() goto_vm(c * 2 + 0) } } function goto_event(shaded) { push_undo() let c = this_card() game.cmd = 0 if (shaded) { log_h2(faction_short[game.current] + " - Shaded Event") log(".h3 C" + c) log(">.i " + data.card_flavor_shaded[c] + ".") log_br() goto_vm(c * 2 + 1) } else { log_h2(faction_short[game.current] + " - Event") log(".h3 C" + c) if (data.card_flavor[c]) log(">.i " + data.card_flavor[c] + ".") log_br() goto_vm(c * 2 + 0) } } function end_event() { if (game.of_gods_and_kings[2] === 1) clean_gk() game.vm = null resume_event_card() } function event_prompt(str) { if (typeof str === "undefined") str = CODE[game.vm.fp][game.vm.prompt][1] if (typeof str === "function") str = str() if (game.of_gods_and_kings[2] === 1) view.prompt = data.card_title[game.of_gods_and_kings[0]] + ": " + str else view.prompt = data.card_title[this_card()] + ": " + str } // === VM === function goto_vm(proc) { game.state = "vm" game.vm = { prompt: 0, fp: proc, ip: 0, ss: [], s: -1, pp: [], pl: [], p: -1, m: 0, } vm_exec() } function vm_asm() { vm_operand(1) vm_next() } function vm_clean_p() { game.vm.pp = [] game.vm.pl = [] vm_next() } function vm_clean_cmd() { reset_cmd() vm_next() } function vm_current() { if (vm_operand(1) !== game.current) game.state = "vm_current" else vm_next() } states.vm_current = { prompt() { let list = vm_operand(1) event_prompt("Select Faction.") if (Array.isArray(list)) { if (list.includes(DS)) gen_choose_faction(DS) if (list.includes(BK)) gen_choose_faction(BK) if (list.includes(VE)) gen_choose_faction(VE) } else { if (list === DS) gen_choose_faction(DS) if (list === BK) gen_choose_faction(BK) if (list === VE) gen_choose_faction(VE) } }, choose_ds() { this.ds() }, choose_bk() { this.bk() }, choose_ve() { this.ve() }, ds() { if (game.current !== DS) clear_undo() change_current(DS) log_transfer(faction_short[game.current] + "...") vm_next() }, bk() { if (game.current !== BK) clear_undo() change_current(BK) log_transfer(faction_short[game.current] + "...") vm_next() }, ve() { if (game.current !== VE) clear_undo() change_current(VE) log_transfer(faction_short[game.current] + "...") vm_next() } } function vm_force_current() { change_current(vm_operand(1)) vm_next() } function vm_exec() { vm_inst(0)() } function vm_goto(cmd, nop, dir, step) { let balance = 1 while (balance > 0) { game.vm.ip += dir if (vm_inst(0) === cmd) --balance if (vm_inst(0) === nop) ++balance if (game.vm.ip < 0 || game.vm.ip > CODE[game.vm.fp].length) throw "ERROR" } game.vm.ip += step vm_exec() } function vm_if() { if (!vm_operand(1)) { let balance = 1 while (balance > 0) { ++game.vm.ip switch (vm_operand(0)) { case vm_if: ++balance break case vm_endif: --balance break case vm_else: if (balance === 1) --balance break } if (game.vm.ip < 0 || game.vm.ip > CODE[game.vm.fp].length) throw "ERROR" } } vm_next() } function vm_else() { vm_goto(vm_endif, vm_if, 1, 1) } function vm_endif() { vm_next() } function vm_inst(a) { return CODE[game.vm.fp][game.vm.ip][a] } function vm_log() { log(vm_operand(1)) vm_next() } function vm_log_br() { log_br() vm_next() } function vm_log_succ() { log_h2(faction_short[vm_operand(1)]) log(".i " + vm_operand(2)) log_br() vm_next() } function vm_pop_summary() { if (game.summary) { upop_summary() log_br() } vm_next() } function vm_mark_space() { if (game.vm.m) set_add(game.vm.m, game.vm.s) else game.vm.m = [ game.vm.s ] vm_next() } function vm_move() { log("Moved " + piece_symbol(game.vm.p) + " to S" + game.vm.s + " from S" + piece_space(game.vm.p) + ".") place_piece(game.vm.p, game.vm.s) vm_next() } function vm_next() { game.vm.ip ++ vm_exec() } function vm_operand(a) { let x = CODE[game.vm.fp][game.vm.ip][a] if (a > 0 && typeof x === "function") return x() return x } function vm_prompt() { if (game.vm.prompt) game.vm._prompt = game.vm.prompt game.vm.prompt = game.vm.ip vm_next() } function pop_vm_prompt() { if (game.vm._prompt) { game.vm.prompt = game.vm._prompt delete game.vm._prompt } else { game.vm.prompt = 0 } } function vm_summary_remove() { if (!game.summary) push_summary() log_summary_remove_from(game.vm.p) remove_piece(game.vm.p) vm_next() } function vm_remove() { log("Removed " + piece_symbol(game.vm.p) + " from S" + piece_space(game.vm.p) + ".") remove_piece(game.vm.p) vm_next() } function vm_repeat() { game.vm.n = vm_operand(1) vm_next() } function vm_endrepeat() { if (--game.vm.n === 0) vm_next() else vm_goto(vm_repeat, vm_endrepeat, -1, 1) } function vm_return() { reset_cmd() game.decree = {} if (!is_succession()) game.state = "vm_return" else end_event() } states.vm_return = { prompt() { event_prompt("Done.") view.actions.end_event = 1 }, end_event, } function vm_while() { if (vm_operand(1)) vm_next() else vm_goto(vm_endwhile, vm_while, 1, 1) } function vm_endwhile() { vm_goto(vm_while, vm_endwhile, -1, 0) } // VM: SPACE function vm_space() { let n = vm_inst(3) let f = vm_inst(4) if (can_vm_space(n, f)) { game.state = "vm_space" } else { // pop_vm_prompt() game.vm.ss = [] game.vm.s = -1 vm_goto(vm_endspace, vm_space, 1, 1) } } function vm_endspace() { vm_goto(vm_space, vm_endspace, -1, 0) } function can_vm_space(n, f) { if (game.vm.ss.length < n) for (let s = first_space; s <= last_space; ++s) if (!set_has(game.vm.ss, s) && f(s)) return true return false } states.vm_space = { prompt() { let f = vm_inst(4) view.who = game.vm.p event_prompt() for (let s = first_space; s <= last_space; ++s) if (!set_has(game.vm.ss, s) && f(s)) gen_action_space(s) if (game.vm.ss.length >= vm_inst(2)) view.actions.skip = 1 }, space(s) { if (vm_inst(1)) push_undo() set_add(game.vm.ss, s) game.vm.s = s vm_next() }, skip() { if (vm_inst(1)) push_undo() game.vm.opt = 1 vm_goto(vm_endspace, vm_space, 1, 1) }, } function vm_save_space() { game.vm._ss = game.vm.ss game.vm._s = game.vm.s game.vm.ss = [] game.vm.s = -1 vm_next() } function vm_set_piece_space() { game.vm.s = piece_space(game.vm.p) vm_next() } function vm_set_space() { game.vm.s = vm_operand(1) vm_next() } function vm_stay_eligible() { log(faction_flags[game.current] + " stayed Eligible.") log_br() game.marked |= (16 << game.current) vm_next() } // VM: PIECE ITERATOR function vm_piece() { if (can_vm_piece()) { game.state = "vm_piece" } else { // pop_vm_prompt() game.vm.pp = [] game.vm.p = -1 vm_goto(vm_endpiece, vm_piece, 1, 1) } } function vm_endpiece() { vm_goto(vm_piece, vm_endpiece, -1, 0) } function can_vm_piece() { let f = vm_inst(4) if (game.vm.pp.length < vm_inst(3)) for (let p = all_first_piece; p <= all_last_piece; ++p) if (piece_space(p) >= 0 && !set_has(game.vm.pp, p) && f(p, piece_space(p))) return true return false } states.vm_piece = { prompt() { let f = vm_inst(4) if (game.vm.s >= 0) view.where = game.vm.s event_prompt() for (let p = all_first_piece; p <= all_last_piece; ++p) if (piece_space(p) >= 0 && !set_has(game.vm.pp, p) && f(p, piece_space(p))) gen_action_piece(p) if (game.vm.pp.length >= vm_inst(2)) view.actions.skip = 1 }, piece(p) { if (vm_inst(1)) push_undo() set_add(game.vm.pp, p) game.vm.p = p vm_next() }, skip() { if (vm_inst(1)) push_undo() game.vm.opt = 1 game.vm.pp = [] vm_goto(vm_endpiece, vm_piece, 1, 1) }, } // VM: MOVE PIECE function vm_move_to() { push_summary() game.vm.pl = [] game.state = "vm_move_to" } function vm_can_move_to(f) { for (let p = all_first_piece; p <= all_last_piece; ++p) if (f(p)) return true return false } states.vm_move_to = { prompt() { let f = vm_inst(2) event_prompt() for (let p = all_first_piece; p <= all_last_piece; ++p) if (f(p)) gen_action_piece(p) view.actions.skip = 1 }, piece(p) { log_summary_place(p) place_piece(p, vm_operand(1)) set_add(game.vm.pl, p) if (!vm_can_move_to(vm_inst(2))) end_vm_move_to(vm_operand(1)) }, skip() { end_vm_move_to(vm_operand(1)) } } function end_vm_move_to(s) { if (game.summary && game.summary.length > 0) { log_space(vm_operand(1), "Move") pop_summary() } else if (game.summary) game.summary = null vm_next() } // VM: PLACE PIECE function vm_auto_place() { let is_rebel = vm_operand(3) let faction = vm_operand(4) let type = vm_operand(5) if (auto_place_piece(game.vm.s, faction, type, is_rebel)) { log("Placed " + faction_type_symbol(faction, type) + " in S" + game.vm.s + ".") vm_next() } else { vm_place() } } function vm_place() { if (can_vm_place()) { push_summary() game.vm.pl = [] game.state = "vm_place" } else { vm_next() } } function can_vm_place_imp(s, faction, type) { if (!can_stack_piece(s, faction, type)) return false if (game.current === faction) // TODO: PLACE ONLY IF AVAILABLE return true return has_piece(AVAILABLE, faction, type) } function can_vm_place() { let faction = vm_operand(4) let type = vm_operand(5) if (typeof faction === "object" && typeof type === "object") { for (let f of faction) for (let t of type) if (can_vm_place_imp(game.vm.s, f, t)) return true } else if (typeof faction === "object") { for (let f of faction) if (can_vm_place_imp(game.vm.s, f, type)) return true } else if (typeof type === "object") { for (let t of type) if (can_vm_place_imp(game.vm.s, faction, t)) return true } else { if (can_vm_place_imp(game.vm.s, faction, type)) return true } return false } states.vm_place = { prompt() { let skip = vm_inst(2) let n = vm_inst(3) - game.vm.pl.length let faction = vm_operand(4) let type = vm_operand(5) let where = SPACE_NAME[game.vm.s] let possible = false view.where = game.vm.s if (typeof type === "object") { event_prompt(`Place up to ${n} Units in ${where}.`) for (let t of type) if (gen_place_piece(faction, t)) possible = true } else { event_prompt(`Place up to ${n} ${PIECE_FACTION_TYPE_NAME[faction][type]} in ${where}.`) if (gen_place_piece(faction, type)) possible = true } if (skip || !possible) view.actions.skip = 1 }, piece(p) { if (vm_inst(1)) push_undo() log_summary_place(p) let pl = place_piece(p, game.vm.s) set_add(game.vm.pl, pl) if (vm_inst(3) - game.vm.pl.length === 0) { placed_summary() vm_next() } }, skip() { if (vm_inst(1)) push_undo() if (vm_inst(2)) // only flag as optional if opcode is opt game.vm.opt = 1 placed_summary(PIECE_FACTION_TYPE_NAME[vm_operand(4)][vm_operand(5)]) vm_next() }, } // VM: REPLACE function vm_replace() { if (!game.decree) game.decree = {} game.vm.rs = piece_space(game.vm.p) remove_piece(game.vm.p) game.state = "vm_replace" } states.vm_replace = { prompt() { let faction = vm_operand(1) let type = vm_operand(2) let rebel_p = vm_operand(3) if (count_pieces(AVAILABLE, faction, type) > 0) { event_prompt(`Replace with a ${rebel_p ? "Rebelling " : ""}${PIECE_FACTION_TYPE_NAME[faction][type]}?`) gen_place_piece(faction, type) } else { event_prompt(`No available ${PIECE_FACTION_TYPE_NAME[faction][type]}.`) } view.actions.skip = 1 }, piece(p) { push_undo() p = place_piece(p, game.vm.rs) game.decree.who = p let rebel_p = vm_operand(3) if (rebel_p) { to_rebel(p) } log(`Replaced a ${piece_symbol(game.vm.p)} for a ${rebel_p ? "Rebelling " : ""}${piece_symbol(p)} in S${game.vm.rs}.`) if (vm_operand(4)) goto_rebel_or_obedient(game.vm.rs) else vm_next() }, skip: vm_next } function goto_rebel_or_obedient(s) { game.decree.where = s game.state = "rebel_or_obedient" } states.rebel_or_obedient = { inactive: "Obedient or Rebelling units", prompt() { event_prompt(`Obedient or Rebelling ${PIECE_FACTION_TYPE_NAME[game.current][ELITE]}.`) view.actions.obedient = 1 view.actions.rebelling = 1 }, obedient() { push_undo() end_rebel_or_obedient() }, rebelling() { push_undo() let p = game.decree.who to_rebel(p) log(`A ${piece_symbol(p)} in S${game.decree.where} to Rebel.`) end_rebel_or_obedient() } } function end_rebel_or_obedient() { if (game.vm && game.vm.ip) vm_next() else game.state = "conspire" } // VM: GAIN_CAVALRY function vm_gain_cavalry() { goto_cavalry(vm_operand(1), "") } function vm_spend_cavalry() { let n = vm_inst(1) for (let i = 0; i < n; ++i) { game.cavalry[find_cavalry(game.current)] = AVAILABLE } log(`${faction_flags[game.current]} spent ${n} CAV`) vm_next() } // VM: INFLUENCE function vm_add_influence() { if (game.inf[vm_operand(1)] !== 4) add_influence(vm_operand(1)) vm_next() } function vm_remove_influence() { if (game.inf[vm_operand(1)] !== 0) remove_influence(vm_operand(1)) else vm_next() } // VM: RESOURCES function vm_resources() { game.state = "vm_resources" } states.vm_resources = { prompt() { let skip = vm_operand(1) let f = vm_operand(2) let n = vm_operand(3) if (n > 0) { event_prompt(`${faction_name[f]} +${n} Resources.`) gen_action_resources(f) } else if (n === 0) { event_prompt(`${faction_name[f]} +0 Resources.`) view.actions.skip = 1 } else { event_prompt(`${faction_name[f]} ${n} Resources.`) gen_action_resources(f) } if (skip) view.actions.skip = 1 }, resources(f) { let n = vm_operand(3) if (!is_succession()) log_resources(f, n) else logi_resources(f, n) add_resources(f, n) vm_next() }, skip() { let f = vm_operand(2) if (!is_succession()) log_resources(f, 0) else logi_resources(f, 0) vm_next() } } function vm_steal() { game.state = "vm_steal" } states.vm_steal = { prompt() { let f1 = vm_operand(1) let f2 = vm_operand(2) let n = Math.min(vm_operand(3), game.resources[f2]) if (n == 0) { event_prompt(`${faction_name[f2]} has no Resources.`) view.actions.skip = 1 } else { event_prompt(`Steal ${n} Resources from ${faction_name[f2]}.`) gen_action_resources(f2) } }, resources(f) { let n = Math.min(vm_operand(3), game.resources[f]) add_resources(game.current, n) add_resources(f, -n) log(`${faction_flags[game.current]} stole ${n} RES from ${faction_flags[f]}.`) vm_next() }, skip() { vm_next() } } // VM : CAVALRY OR RESOURCES function vm_cav_resources() { game.vm.count = vm_operand(1) if (game.vm.count > 0) push_summary() game.state = "vm_cav_resources" } states.vm_cav_resources = { prompt() { if (game.vm.count === 0) { event_prompt("No Cavalry nor Resources to gain.") view.actions.skip = 1 } else { event_prompt(`Gain ${game.vm.count} Resource${game.vm.count > 1 ? "s": ""} or Cavalry token${game.vm.count > 1 ? "s": ""}.`) gen_action_resources(game.current) gen_take_cavalry(game.current) } }, resources(f) { log_summary_resources(game.current) add_resources(game.current, 1) tick_cav_resources() }, token(c) { log_summary_cavalry(c) set_cavalry_faction(c, game.current) tick_cav_resources() }, skip() { logi("Nothing") vm_next() } } function tick_cav_resources() { game.vm.count -= 1 if (game.vm.count === 0) { log_br() upop_summary() log_br() vm_next() } } function vm_steal_cavalry() { game.state = "vm_steal_cavalry" } states.vm_steal_cavalry = { prompt() { let f1 = vm_operand(1) let f2 = vm_operand(2) let n = Math.min(vm_operand(3), n_cavalry(f2)) if (n == 0) { event_prompt(`${faction_name[f2]} has no Cavalry.`) view.actions.skip = 1 } else { event_prompt(`Steal ${n} Cavalry token from ${faction_name[f2]}.`) gen_action_faction_cavalry(f2) } }, token(c) { let f2 = vm_operand(2) let n = Math.min(vm_operand(3), n_cavalry(f2)) for (let i = 0; i < n; ++i) { c = find_cavalry(f2) set_cavalry_faction(c, game.current) } log(`${faction_flags[game.current]} stole ${n} CAV from ${faction_flags[f2]}.`) vm_next() }, skip() { vm_next() } } // VM : REBEL function vm_to_rebel() { to_rebel_space(game.vm.s, vm_operand(1)) vm_next() } // VM: CAMPAIGN function vm_campaign() { init_free_decree("Campaign") game.decree.campaign = [] game.decree.selected = [] game.state = "campaign" } // VM: CONTROL function vm_place_tributary() { add_tributary(game.vm.s) log("Placed Tributary marker in S" + game.vm.s) vm_next() } function vm_remove_tributary() { remove_tributary(game.vm.s) log("Removed Tributary marker in S" + game.vm.s) vm_next() } // VM: ATTACK function vm_free_attack() { let pieces = (game.cmd && game.cmd.pieces) ? game.cmd.pieces : [] game.cmd = { type: "Attack", limited: 1, free: 1, spaces: [], selected: [], pieces: pieces, where: game.vm.s, } if (!can_attack_in_space(game.vm.s) && !(game.cmd && game.cmd.pieces)) { vm_next() } else { log_space(game.vm.s, "Attack") if (is_timurid()) game.cmd.attacker = MI else game.cmd.attacker = game.current game.cmd.support_space = null if (game.vm.fp === 43) game.cmd.free = 0 if (typeof vm_inst(1) !== "undefined") { game.cmd.target = vm_inst(1) goto_attack_space() } else { goto_attack_select() } } } // VM: BUILD function vm_free_build() { build_in_space(game.vm.s) vm_next() } // VM: COMPEL function vm_free_compel() { init_free_decree("Compel", game.vm.s) goto_compel_space() } // VM: DEMAND_OBEDIENCE function vm_demand_obedience() { if (can_demand_in_space(game.vm.s)) demand_obedience_in_space(game.vm.s) vm_next() } // VM: GOVERN function vm_free_govern_in_space() { init_free_command_in_space("Govern", game.vm.s) goto_govern_space() } function vm_free_govern_in_space_skip() { init_free_command_in_space("Govern", game.vm.s) push_summary() game.cmd.count = [0, 0] game.state = "govern_space" } function vm_govern_in_space() { init_free_command_in_space("Govern", game.vm.s) select_cmd_space(game.vm.s, 1) goto_govern_space() } function vm_govern() { game.cmd = { type: "Govern", limited: 0, free: 0, spaces: [], pieces: [], where: -1, pass: 1 } game.state = "govern" } // VM: MARCH function vm_free_march() { init_free_command_in_space("March", game.vm.s) game.cmd.pieces = [] goto_march_space() } // VM: MIGRATE function vm_free_migrate() { let shift = (game.cmd && game.cmd.shift) ? true : false game.cmd = { type: "Migrate", limited: 1, free: 1, spaces: [], selected: [], pieces: [], where: game.vm.s, } game.cmd.pieces = game.vm.pp game.cmd.shift = shift goto_migrate_space() } // VM: RALLY function vm_free_rally() { init_free_command_in_space("Rally", game.vm.s) goto_rally_space() } // VM: REBEL function vm_free_rebel() { game.cmd = { type: "Rebel", limited: 1, free: 1, spaces: [], selected: [], pieces: [], where: -1, } game.state = "rebel" } // VM: FLIP DYNASTY function vm_flip_dynasty() { game.state = "vm_flip_dynasty" } states.vm_flip_dynasty = { prompt() { event_prompt("End of the oppressive Khalji Dynasty.") view.actions.dynasty_card = 1 }, dynasty_card() { game.succ += 1 log("Rebel Commands are now available.") vm_next() } } // VM: INFLUENCE SUCCESSION function vm_influence_succession() { game.cmd = {} game.cmd.who = null game.cmd.count = 3 game.state = "vm_influence_succession" } states.vm_influence_succession = { prompt() { if (game.resources[game.current] < 3) { event_prompt("Not enough Resources.") } else if (game.cmd.count === 0) { event_prompt("Done.") } else if (!game.cmd.who) { event_prompt(`Select the Influence marker to shift (${game.cmd.count} remaining).`) gen_action_influence(BK) gen_action_influence(VE) } else { let f2 = other_rebel_faction(game.cmd.who) event_prompt(`Spend 3 Resources to shift ${faction_name[game.cmd.who]}'s Influence (${game.cmd.count} remaining).`) gen_action_influence(f2) if (game.inf[game.cmd.who] < 4) view.actions.add_influence = 1 if (game.inf[game.cmd.who] > 0) view.actions.remove_influence = 1 } if (game.cmd.count === 3) view.actions.skip = 1 if (game.cmd.count < 3) view.actions.next = 1 }, influence(f) { game.cmd.who = f }, add_influence() { push_undo() add_resources(game.current, -3) add_influence(game.cmd.who) game.cmd.count -= 1 }, remove_influence() { push_undo() add_resources(game.current, -3) remove_influence(game.cmd.who) game.cmd.count -= 1 }, next: vm_next, skip() { log("Declined Influence shift.") vm_next() }, } // VM : TIMURID CRISIS function vm_timurid_crisis() { game.state = "vm_timurid_crisis" } states.vm_timurid_crisis = { prompt() { event_prompt("Mongol Invaders gather in the Mountain Passes.") gen_action_space(S_MONGOL_INVADERS) }, space(s) { log_space(S_MOUNTAIN_PASSES, "Advance") let n = Math.floor(count_pieces(AVAILABLE, MI, TROOPS) / 2) push_summary() for (let i = 0; i < n; ++i) { let p = find_piece(AVAILABLE, MI, TROOPS) log_summary_place(p) place_piece(p, S_MOUNTAIN_PASSES) } pop_summary() vm_next() } } // VM : TIMURID ADVANCE function vm_timurid_advance() { let s1 = vm_operand(1) let s2 = vm_operand(2) push_summary() advance_pieces(MI, TROOPS, s1, s2) advance_pieces(DS, TROOPS, s1, s2) advance_pieces(DS, ELITE, s1, s2) log_action("S" + s2 + " - Advance") pop_summary() vm_next() } function advance_pieces(f, t, s1, s2) { let n = count_pieces(s1, f, t) for (let i = 0; i < n; ++i) { let p = find_piece(s1, f, t) log_summary_place(p) place_piece(p, s2) } } // VM : END GAME function vm_end_game() { // Aftermath let vp = Math.max(3 - count_pieces(S_DELHI, MI, TROOPS), -3) log_action("Invasion's aftermath...") if (vp > 0) logi(faction_flags[DS] + " VP +" + vp + ".") else logi(faction_flags[DS] + " VP " + vp + ".") game.vp[DS] += vp log_h1("Victory Phase") for (let i = 0; i < 3; i++) { logi(faction_short[i] + " " + game.vp[i] + " VP") } let result = get_result() goto_game_over(result) } function get_idx_max(arr) { let m = Math.max(...arr) return arr .map((v, i) => (v === m ? i : -1)) .filter(i => i !== -1) } function get_result() { let i_vp = get_idx_max([...game.vp]) if (i_vp.length === 1) return faction_name[i_vp[0]] let i_res = get_idx_max(i_vp.map(i => game.resources[i])) if (i_res.length === 1) return faction_name[i_vp[i_res]] return i_res.map(i => faction_name[i_vp[i]]).join(",") } // === GAME END === function goto_game_over(result) { game.state = "game_over" game.current = -1 game.active = "None" game.result = result game.victory = game.result + " won!" log_br() log(game.victory) return true } // VM: MIX function vm_conspire_trade() { game.state = "vm_conspire_trade" } function vm_govern_attack_demand() { game.state = "vm_govern_attack_demand"} states.vm_conspire_trade = { prompt() { event_prompt("Conspire or Trade") view.actions.conspire = can_conspire() ? 1 : 0 view.actions.trade = can_trade() ? 1 : 0 }, conspire() { push_undo() goto_conspire() }, trade() { push_undo() goto_trade() } } function can_govern_attack_demand_in_space(s) { if ( (can_govern() && can_govern_in_space(s)) || (can_attack() && can_attack_in_space(s)) || (can_demand() && can_demand_in_space(s)) ) return true return false } states.vm_govern_attack_demand = { prompt() { event_prompt("Govern, Attack or Demande Obedience in selected Province.") view.actions.govern = can_govern() && can_govern_in_space(game.vm.s) ? 1 : 0 view.actions.attack = can_attack() && can_attack_in_space(game.vm.s) ? 1 : 0 view.actions.demand = can_demand() && can_demand_in_space(game.vm.s) ? 1 : 0 }, govern() { push_undo() vm_govern_in_space() }, attack() { push_undo() vm_free_attack() }, demand() { push_undo() vm_demand_obedience() } } function vm_any_limited_command() { reset_cmd() game.state = "vm_any_limited_command" } states.vm_any_limited_command = { prompt() { event_prompt("Execute a free limited Command.") view.actions.rally = can_rally() ? 1 : 0 view.actions.migrate = can_migrate() ? 1 : 0 view.actions.rebel = can_rebel() ? 1 : 0 view.actions.attack = can_attack() ? 1 : 0 view.actions.skip = 1 }, rally() { push_undo() init_vm_command("Rally", true, true) game.state = "rally" }, migrate() { push_undo() init_vm_command("Migrate", true, true) game.state = "migrate" }, rebel() { push_undo() init_vm_command("Rebel", true, true) game.state = "rebel" }, attack() { push_undo() init_vm_command("Attack", true, true) game.cmd.attacker = game.current game.cmd.support_space = null game.state = "attack" }, skip: vm_next } // VM: SHADED_6 function vm_shaded_6() { init_free_decree("Campaign") game.decree = { campaign: [], pieces: [], selected: [] } game.state = "campaign" } function vm_shaded_6_2() { if (has_piece_faction(game.vm.m, BK) || has_piece_faction(game.vm.m, VE)) game.state = "shaded_6_2" else vm_next() } states.shaded_6_2 = { prompt() { event_prompt("Steal 3 Resources from a Faction where the Campaign ended.") if (has_piece_faction(game.vm.m, BK)) gen_action_resources(BK) if (has_piece_faction(game.vm.m, VE)) gen_action_resources(VE) }, resources(f) { push_undo() let n = Math.min(game.resources[f], 3) add_resources(f, -n) add_resources(DS, n) log(`${faction_flags[game.current]} stole ${n} RES from ${faction_flags[f]}.`) vm_next() } } // VM: EVENT_25 function vm_event_25() { game.state = "event_25" } states.event_25 = { prompt() { if (game.current === BK) { event_prompt("Trade or gain 3 Resources.") view.actions.trade = 1 } else if (game.current === VE) { event_prompt("Tax or gain 3 Resources.") view.actions.tax = 1 } gen_action_resources(game.current) }, resources(f) { push_undo() add_resources(game.current, 3) log_resources(game.current, 3) vm_next() }, tax() { push_undo() goto_tax() }, trade() { push_undo() goto_trade() } } // VM: EVENT_26 function vm_event_26() { game.of_gods_and_kings[1] = game.current log(faction_flags[game.current] + " drew C" + game.of_gods_and_kings[0]) clear_undo() vm_next() } function clean_gk() { game.of_gods_and_kings[1] = null game.of_gods_and_kings[2] = 0 } // VM: SHADED_29 function vm_shaded_29() { push_summary() game.cmd = { count: 3 } game.state = "shaded_29" } states.shaded_29 = { prompt() { if (game.cmd.count > 0) { let can_take = gen_take_cavalry(game.current) if (can_take) event_prompt(`Spend Resources to gain Cavalry token (${game.cmd.count} left).`) else { event_prompt("Gain Cavalry: No more available Cavalry token.") view.actions.next = 1 } } else { event_prompt("No more Cavalry token to acquire.") } view.actions.next = 1 }, token(c) { push_undo() game.cmd.count -= 1 add_resources(game.current, -1) log_summary_cavalry(c) set_cavalry_faction(c, game.current) }, next() { if (game.cmd.count < 3) { log(`${faction_flags[game.current]} spent ${3-game.cmd.count} RES`) upop_summary() } else { game.summary = null } vm_next() } } // === CONST === // Factions const DS = 0 const BK = 1 const VE = 2 const MI = 3 const FACTIONS = [DS, BK, VE] const REBEL_FACTIONS = [BK, VE] // Role names const NAME_DS = "Delhi Sultanate" const NAME_BK = "Bahmani Kingdom" const NAME_VE = "Vijayanagara Empire" const NAME_SOLO = "Solo" // Player pieces types const DISC = 0 const ELITE = 1 const TROOPS = 2 const UNITS = [TROOPS, ELITE] // Pieces status const AVAILABLE = -1 const OUT_OF_PLAY = -2 const ANY_PIECES = [ DISC, ELITE, TROOPS ] const PIECE_FACTION_TYPE_NAME = [ [ "Qasbah", "Governor", "Troop" ], [ "Fort", "Amir", null ], [ "Temple", "Raja", null ], [ null, null, "Invader" ] ] const PIECE_FACTION_TYPE_SYMBOL = [ [ "DDS", "EDS", "CDS" ], [ "DBK", "EBK", null ], [ "DVE", "EVE", null ], [ null, null, "CMI" ] ] const LAST_CAVALRY = 9 const INF_TOKEN = [null, "IBK", "IVE"] // Sequence of Play options const ELIGIBLE = 0 const SOP_LIMITED_COMMAND = 1 const SOP_COMMAND_DECREE = 2 const SOP_EVENT_OR_COMMAND = 3 const SOP_PASS = 4 const INELIGIBLE = 5 // === CONST === const CODE = [] // EVENT 1 CODE[1 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_current, REBEL_FACTIONS ], [ vm_prompt, "Move all Governors to Provinces adjacent to Delhi." ], [ vm_piece, false, 999, 999, (p,s)=>(is_governor(p)) ], [ vm_space, true, 1, 1, (s)=>(set_has(SPACES[S_DELHI].adjacent, s) && s != S_PUNJAB) ], [ vm_move ], [ vm_endspace ], [ vm_endpiece ], [ vm_free_rebel ], [ vm_return ], ] // SHADED 1 CODE[1 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_current, DS ], [ vm_prompt, "Select a space to move Units into and Attack for free." ], [ vm_space, true, 1, 1, (s)=>s>=0 ], [ vm_prompt, ()=>`Move up to ${8-game.vm.pl.length} units into the selected space.` ], [ vm_mark_space ], [ vm_move_to, ()=>(game.vm.s), (p,s)=>(is_ds_unit(p) && is_piece_on_map(p) && !is_piece_in_event_space(p) && game.vm.pl.length < 8) ], [ vm_if, ()=>(has_piece_faction(game.vm.s, BK) || has_piece_faction(game.vm.s, VE)) ], [ vm_to_rebel, BK ], [ vm_to_rebel, VE ], [ vm_log_br ], [ vm_log, ()=>`Opposing Units to Rebelling in ${SPACE_NAME[game.vm.s]}.` ], [ vm_endif ], [ vm_endspace ], [ vm_clean_cmd ], [ vm_prompt, ()=>`Free Attack in ${SPACE_NAME[game.vm.m[0]]}.` ], [ vm_space, true, 1, 1, (s)=>(s === game.vm.m[0] && can_attack_in_space(s)) ], [ vm_free_attack ], [ vm_endspace ], [ vm_return ], ] // EVENT 2 CODE[2 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_prompt, "Remove up to 2 Delhi Sultanate Units in both Mountain Passes and Punjab." ], [ vm_space, true, 2, 2, (s)=>((s === S_MOUNTAIN_PASSES || s === S_PUNJAB) && has_ds_unit(s)) ], [ vm_prompt, "Remove up to 2 Delhi Sultanate Units." ], [ vm_piece, true, 0, 2, (p,s)=>is_ds_unit(p) && is_piece_in_event_space(p) ], [ vm_summary_remove ], [ vm_endpiece ], [ vm_endspace ], [ vm_pop_summary ], [ vm_return ], ] // SHADED 2 CODE[2 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_current, DS ], [ vm_prompt, "Attack in Mountain Passes or Punjab for free." ], [ vm_space, true, 1, 1, (s)=>((s === S_MOUNTAIN_PASSES || s === S_PUNJAB) && has_ds_unit(s) && has_piece(s, MI, TROOPS)) ], [ vm_free_attack ], [ vm_endspace ], [ vm_clean_cmd ], [ vm_prompt, "Rearrange up to 6 Units among Mountain Passes and Punjab and one other space." ], [ vm_space, true, 1, 1, (s)=>(s !== S_MOUNTAIN_PASSES && s !== S_PUNJAB) ], [ vm_mark_space ], [ vm_endspace ], [ vm_prompt, ()=>`Move up to ${6-game.vm.pp.length} Units.` ], [ vm_piece, false, 0, 6, (p,s)=>([S_MOUNTAIN_PASSES, S_PUNJAB, game.vm.m[0]].includes(piece_space(p)) && is_ds_unit(p)) ], [ vm_prompt, ()=>`Move up to ${6-game.vm.pp.length+1} Units.` ], [ vm_space, true, 1, 1, (s)=>((s === S_MOUNTAIN_PASSES || s === S_PUNJAB || s === game.vm.m[0]) && s !== piece_space(game.vm.p)) ], [ vm_move ], [ vm_endspace ], [ vm_endpiece ], [ vm_return ], ] // EVENT 3 CODE[3 * 2 + 0] = [ [ vm_current, REBEL_FACTIONS ], [ vm_prompt, "Free Rally and gain 2 Resources in each Province adjacent to Warangal where you have a piece." ], [ vm_space, true, 0, 999, (s)=>(is_adjacent_to_city(C_WARANGAL, s) && has_piece_faction(s, game.current)) ], [ vm_free_rally ], [ vm_resources, false, ()=>(game.current), 2 ], [ vm_endspace ], [ vm_return ], ] // SHADED 3 CODE[3 * 2 + 1] = [ [ vm_current, DS ], [ vm_prompt, "In each Province adjacent to Warangal where you have a piece, steal 2 Resources from each other Faction present." ], [ vm_space, true, 0, 999, (s)=>(is_adjacent_to_city(C_WARANGAL, s) && has_piece_faction(s, DS) && has_piece_enemy_faction(s, DS)) ], [ vm_if, ()=>has_piece_faction(game.vm.s, BK) ], [ vm_steal, DS, BK, 2 ], [ vm_endif ], [ vm_if, ()=>has_piece_faction(game.vm.s, VE) ], [ vm_steal, DS, VE, 2 ], [ vm_endif ], [ vm_endspace ], [ vm_return ], ] // EVENT 4 CODE[4 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_current, REBEL_FACTIONS ], [ vm_prompt, ()=>`Replace a Governor with an Obedient or Rebelling ${PIECE_FACTION_TYPE_NAME[game.current][ELITE]}.` ], [ vm_piece, false, 1, 1, (p,s)=>is_governor(p) ], [ vm_replace, ()=>(game.current), ELITE, false, true ], [ vm_endpiece ], [ vm_return ], ] // SHADED 4 CODE[4 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_current, DS ], [ vm_prompt, "Govern ignoring location requirements." ], [ vm_asm, ()=>game.vm.ignore_loc_req = true ], [ vm_govern ], [ vm_return ], ] // EVENT 5 CODE[5 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_current, REBEL_FACTIONS ], [ vm_prompt, ()=>`Replace up to 2 Delhi Sultanate Units with Rebelling ${PIECE_FACTION_TYPE_NAME[game.current][ELITE]}.` ], [ vm_piece, true, 0, 2, (p,s)=>(is_adjacent_to_city(C_CHITTOR, piece_space(p)) && is_ds_unit(p)) ], [ vm_replace, ()=>(game.current), ELITE, true, false ], [ vm_endpiece ], [ vm_prompt, ()=>`Place a ${PIECE_FACTION_TYPE_NAME[game.current][DISC]} in Rajput Kingdoms.` ], [ vm_space, true, 0, 1, (s)=>(s === S_RAJPUT_KINGDOMS && can_place_piece(s, game.current, DISC)) ], [ vm_auto_place, false, 0, false, ()=>(game.current), DISC ], [ vm_endspace ], [ vm_return ], ] // SHADED 5 CODE[5 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_current, DS ], [ vm_prompt, "Replace up to 3 opposing Units adjacent to Chittor with Troops." ], [ vm_piece, true, 0, 3, (p,s)=>(is_adjacent_to_city(C_CHITTOR, piece_space(p)) && is_enemy_unit(p)) ], [ vm_replace, DS, TROOPS, false, false ], [ vm_endpiece ], [ vm_prompt, "Place a Qasbah in Rajput Kingdoms." ], [ vm_space, true, 0, 1, (s)=>(s === S_RAJPUT_KINGDOMS && can_place_piece(s, DS, DISC)) ], [ vm_auto_place, false, 0, false, DS, DISC ], [ vm_endspace ], [ vm_return ], ] // EVENT 6 CODE[6 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_current, REBEL_FACTIONS ], [ vm_prompt, ()=>`Remove a Tributary marker in one Province adjacent to Warangal.` ], [ vm_space, true, 0, 1, (s)=>(is_adjacent_to_city(C_WARANGAL, s)) ], [ vm_remove_tributary ], [ vm_prompt, ()=>`Place up to 2 ${PIECE_FACTION_TYPE_NAME[game.current][ELITE]}.` ], [ vm_place, true, 1, 2, ()=>(game.current), ELITE ], [ vm_endspace ], [ vm_return ], ] // SHADED 6 CODE[6 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_current, DS ], [ vm_prompt, "Execute a Campaign ending in a Province adjacent to Warangal." ], [ vm_shaded_6 ], [ vm_shaded_6_2 ], [ vm_return ], ] // EVENT 7 CODE[7 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_current, REBEL_FACTIONS ], [ vm_prompt, ()=>`Place up to 2 ${PIECE_FACTION_TYPE_NAME[game.current][ELITE]}s in Tamilakam.` ], [ vm_space, true, 1, 1, (s)=>(s === S_TAMILAKAM) ], [ vm_place, true, 1, 2, ()=>(game.current), ELITE ], [ vm_endspace ], [ vm_cav_resources, 3 ], [ vm_return ], ] // SHADED 7 CODE[7 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_current, DS ], [ vm_prompt, ()=>`Place up to 4 Troops and a Governor in Tamilakam.` ], [ vm_space, true, 1, 1, (s)=>(s === S_TAMILAKAM) ], [ vm_place, true, 1, 4, DS, TROOPS ], [ vm_place, true, 1, 1, DS, ELITE ], [ vm_endspace ], [ vm_cav_resources, 3 ], [ vm_return ], ] // EVENT 8 CODE[8 * 2 + 0] = [ [ vm_current, REBEL_FACTIONS ], [ vm_prompt, ()=>`In each Province adjacent to Devagiri and/or Gulbarga, replace a Delhi Sultanate Unit with a ${PIECE_FACTION_TYPE_NAME[game.current][ELITE]}.` ], [ vm_space, true, 0, 999, (s)=>(is_adjacent_to_city(C_DEVAGIRI, s) || is_adjacent_to_city(C_GULBARGA, s)) && has_ds_unit(s) ], [ vm_prompt, ()=>`Replace a Delhi Sultanate Unit with a ${PIECE_FACTION_TYPE_NAME[game.current][ELITE]}.` ], [ vm_piece, false, 1, 1, (p,s)=>is_ds_unit(p) && is_piece_in_event_space(p) ], [ vm_replace, ()=>(game.current), ELITE, false, false ], [ vm_endpiece ], [ vm_endspace ], [ vm_return ], ] // SHADED 8 CODE[8 * 2 + 1] = [ [ vm_current, DS ], [ vm_prompt, ()=>`In each Province adjacent to Devagiri and/or Gulbarga, Govern for free ignoring location requirements and place a Troop.` ], [ vm_asm, ()=>game.vm.ignore_loc_req = true ], [ vm_space, true, 0, 999, (s)=>(is_adjacent_to_city(C_DEVAGIRI, s) || is_adjacent_to_city(C_GULBARGA, s)) && (can_govern_in_space(s) || can_place_piece(s, DS, TROOPS)) ], [ vm_if, ()=>(can_govern_in_space(game.vm.s)) ], [ vm_free_govern_in_space_skip ], [ vm_endif ], [ vm_if, ()=>(can_place_piece(game.vm.s, DS, TROOPS)) ], [ vm_place, false, 1, 1, DS, TROOPS ], [ vm_endif ], [ vm_endspace ], [ vm_return ], ] // EVENT 9 CODE[9 * 2 + 0] = [ [ vm_current, VE ], [ vm_prompt, "Compel in a Province adjacent to Warangal that you do not control, then Build there." ], [ vm_add_influence, VE ], [ vm_space, true, 1, 1, (s)=>(is_adjacent_to_city(C_WARANGAL, s) && !is_faction_control(s, game.current) && can_compel_in_space(s)) ], [ vm_free_compel ], [ vm_mark_space ], [ vm_endspace ], [ vm_prompt, ()=>`Build in ${SPACE_NAME[game.vm.m[0]]}.` ], [ vm_space, true, 0, 1, (s)=>(s === game.vm.m[0] && can_place_piece(s, game.current, DISC)) ], [ vm_free_build ], [ vm_endspace ], [ vm_return ], ] // SHADED 9 CODE[9 * 2 + 1] = [ [ vm_current, DS ], [ vm_resources, false, ()=>(game.current), 3 ], [ vm_prompt, "Place up to 4 Troops and a Governor in a Province adjacent to Warangal and Demand Obedience there." ], [ vm_space, true, 1, 1, (s)=>is_adjacent_to_city(C_WARANGAL, s) ], [ vm_place, true, 1, 4, DS, TROOPS ], [ vm_place, true, 1, 1, DS, ELITE ], [ vm_demand_obedience ], [ vm_endspace ], [ vm_return ], ] // EVENT 10 CODE[10 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_prompt, "Move up to 4 Delhi Sultanate Units to adjacent Provinces." ], [ vm_piece, true, 0, 4, (p,s)=>(is_ds_unit(p) && is_province(piece_space(p))) ], [ vm_prompt, "Move Delhi Sultanate Unit to an adjacent Province." ], [ vm_space, false, 1, 1, (s)=>(is_adjacent(s, piece_space(game.vm.p)) && is_province(s)) ], [ vm_move ], [ vm_endspace ], [ vm_endpiece ], [ vm_resources, false, DS, -5 ], [ vm_return ], ] // SHADED 10 CODE[10 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_current, DS ], [ vm_prompt, "Move any Qasbahs to spaces containing Governors." ], [ vm_piece, true, 0, 999, (p,s)=>is_qasbah(p) ], [ vm_prompt, "Move Qasbah to any space containing a Governor." ], [ vm_space, true, 1, 1, (s)=>has_governor(s) && !has_qasbah(s) ], [ vm_move ], [ vm_endspace ], [ vm_endpiece ], [ vm_prompt, "Add up to 2 Troops in each space with a Qasbah." ], [ vm_space, true, 0, 999, (s)=>has_qasbah(s) ], [ vm_place, true, 1, 2, DS, TROOPS ], [ vm_endspace ], [ vm_return ], ] // EVENT 11 CODE[11 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_prompt, "Place up to two Mongol Invaders in each of Mtn Passes and Punjab." ], [ vm_space, true, 0, 2, (s)=>(s === S_PUNJAB || s === S_MOUNTAIN_PASSES) ], [ vm_place, true, 1, 2, MI, TROOPS ], [ vm_endspace ], [ vm_return ], ] // SHADED 11 CODE[11 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_current, DS ], [ vm_prompt, "Remove up to 4 Mongol Invaders." ], [ vm_piece, true, 0, 4, (p,s)=>is_mongol_invader(p) ], [ vm_summary_remove ], [ vm_endpiece ], [ vm_pop_summary ], [ vm_return ], ] // EVENT 12 CODE[12 * 2 + 0] = [ [ vm_current, REBEL_FACTIONS ], [ vm_gain_cavalry, ()=>2 ], [ vm_prompt, "Remove up to 4 Delhi Sultanate pieces from Provinces adjacent to Warangal." ], [ vm_piece, true, 0, 4, (p,s)=>(is_adjacent_to_city(C_WARANGAL, piece_space(p)) && piece_faction(p) === DS) ], [ vm_summary_remove ], [ vm_endpiece ], [ vm_pop_summary ], [ vm_return ], ] // SHADED 12 CODE[12 * 2 + 1] = [ [ vm_current, DS ], [ vm_gain_cavalry, ()=>2 ], [ vm_prompt, "Place a Qasbah in a Province adjacent to Warangal." ], [ vm_space, true, 0, 1, (s)=>is_adjacent_to_city(C_WARANGAL, s) && can_place_piece(s, DS, DISC) ], [ vm_auto_place, false, 0, false, DS, DISC ], [ vm_endspace ], [ vm_asm, ()=>game.vm.count = 0 ], [ vm_repeat, 4 ], [ vm_prompt, ()=>`Place up to ${4-game.vm.count} Troops adjacent to Warangal.` ], [ vm_space, true, 0, 1, (s)=>is_adjacent_to_city(C_WARANGAL, s) && can_place_piece(s, DS, TROOPS) ], [ vm_auto_place, false, 0, false, DS, TROOPS ], [ vm_asm, ()=>(game.vm.count += 1) ], [ vm_endspace ], [ vm_endrepeat ], [ vm_prompt, "Execute a free Limited Attack adjacent to Warangal." ], [ vm_space, true, 1, 1, (s)=>(is_adjacent_to_city(C_WARANGAL, s) && can_attack_in_space(s)) ], [ vm_free_attack ], [ vm_endspace ], [ vm_return ], ] // EVENT 13 CODE[13 * 2 + 0] = [ [ vm_current, VE ], [ vm_asm, ()=>game.vm.count = (Math.min((4 - game.inf[VE]), 2) * 2) ], [ vm_add_influence, VE ], [ vm_add_influence, VE ], [ vm_asm, ()=>game.vm.placed = 0 ], [ vm_repeat, ()=>(game.vm.count) ], [ vm_prompt, ()=>`Place up to ${game.vm.count-game.vm.placed} Rajas adjacent to Vijayanagara.` ], [ vm_space, true, 0, 1, (s)=>is_adjacent_to_city(C_VIJAYANAGARA, s) && can_place_piece(s, VE, ELITE) ], [ vm_auto_place, false, 0, false, VE, ELITE ], [ vm_asm, ()=>(game.vm.placed += 1) ], [ vm_endspace ], [ vm_endrepeat ], [ vm_return ], ] // SHADED 13 CODE[13 * 2 + 1] = [ [ vm_prompt, "Select a Province with Rajas adjacent to a Province with a Bahmani piece." ], [ vm_space, true, 1, 1, (s)=>(has_piece(s, VE, ELITE) && SPACES[s].adjacent.some(ss => has_piece_faction(ss, BK))) ], [ vm_mark_space ], [ vm_endspace ], [ vm_prompt, "Select a destination Province for the Rajas." ], [ vm_space, true, 1, 1, (s)=>(is_adjacent(s, game.vm.m) && has_piece_faction(s, BK)) ], [ vm_prompt, ()=>`Move any number of Rajas into ${SPACE_NAME[game.vm.m[0]]}.` ], [ vm_piece, false, 0, 999, (p,s)=>(is_raja(p) && (piece_space(p) === game.vm.m[0])) ], [ vm_move ], [ vm_endpiece ], [ vm_mark_space ], [ vm_endspace ], [ vm_current, BK ], [ vm_prompt, ()=>`Attack Vijayanagara Empire in ${SPACE_NAME[game.vm.m[0]]} for free.` ], [ vm_space, true, 0, 1, (s)=>(game.vm.m[1] === s) ], [ vm_free_attack, VE ], [ vm_endspace ], [ vm_return ], ] // EVENT 14 CODE[14 * 2 + 0] = [ [ vm_current, VE ], [ vm_gain_cavalry, ()=>2 ], [ vm_prompt, "Remove a Tributary marker in a Province adjacent to Vijayanagara." ], [ vm_space, true, 0, 1, (s)=>is_adjacent_to_city(C_VIJAYANAGARA, s) && is_tributary(s) ], [ vm_remove_tributary ], [ vm_endspace ], [ vm_asm, ()=>game.vm.count = 0 ], [ vm_repeat, 2 ], [ vm_prompt, ()=>`Place up to ${2-game.vm.count} Rajas adjacent to Vijayanagara.` ], [ vm_space, true, 0, 1, (s)=>is_adjacent_to_city(C_VIJAYANAGARA, s) && can_place_piece(s, VE, ELITE) ], [ vm_auto_place, false, 0, false, VE, ELITE ], [ vm_asm, ()=>(game.vm.count += 1) ], [ vm_endspace ], [ vm_endrepeat ], [ vm_return ], ] // SHADED 14 CODE[14 * 2 + 1] = [ [ vm_current, DS ], [ vm_gain_cavalry, ()=>3 ], [ vm_prompt, "Place up to 4 Troops and a Governor in a province adjacent to Vijayanagara and Demand Obedience there." ], [ vm_space, true, 0, 1, (s)=>is_adjacent_to_city(C_VIJAYANAGARA, s) ], [ vm_place, true, 1, 4, DS, TROOPS ], [ vm_place, true, 1, 1, DS, ELITE ], [ vm_demand_obedience ], [ vm_endspace ], [ vm_return ], ] // EVENT 15 CODE[15 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_prompt, "Replace any Governors adjacent to Devagiri with Rebelling Amirs." ], [ vm_piece, true, 0, 999, (p,s)=>is_adjacent_to_city(C_DEVAGIRI, piece_space(p)) && is_governor(p) ], [ vm_set_piece_space ], [ vm_mark_space ], [ vm_replace, BK, ELITE, true, false ], [ vm_set_space, -1 ], [ vm_endpiece ], [ vm_prompt, "Remove a Tributary marker in one affected Province." ], [ vm_space, true, 0, 1, (s)=>is_tributary(s) && set_has(game.vm.m, s) ], [ vm_remove_tributary ], [ vm_endspace ], [ vm_return ], ] // SHADED 15 CODE[15 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_prompt, "Replace an Amir with a Governor and place a Tributary marker in a Province adjacent to Devagiri." ], [ vm_space, true, 1, 1, (s)=>(is_adjacent_to_city(C_DEVAGIRI, s) && !is_tributary(s) && has_amir(s)) ], [ vm_asm, ()=>game.vm.p = find_piece(game.vm.s, BK, ELITE) ], [ vm_replace, DS, ELITE, false, false ], [ vm_place_tributary ], [ vm_endspace ], [ vm_return ], ] // EVENT 16 CODE[16 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_current, BK ], [ vm_asm, ()=>game.vm.count = 0 ], [ vm_repeat, 3 ], [ vm_prompt, ()=>`Place up to ${3-game.vm.count} Amirs adjacent to Gulbarga.` ], [ vm_space, true, 0, 1, (s)=>is_adjacent_to_city(C_GULBARGA, s) && can_place_piece(s, BK, ELITE) ], [ vm_auto_place, false, 0, false, BK, ELITE ], [ vm_asm, ()=>(game.vm.count += 1) ], [ vm_endspace ], [ vm_endrepeat ], [ vm_prompt, "Place a Fort adjacent to Gulbarga." ], [ vm_space, true, 0, 1, (s)=>is_adjacent_to_city(C_GULBARGA, s) && can_place_piece(s, BK, DISC) ], [ vm_auto_place, false, 0, false, BK, DISC ], [ vm_endspace ], [ vm_return ], ] // SHADED 16 CODE[16 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_current, [DS, VE] ], [ vm_if, ()=>game.current === DS ], [ vm_prompt, "Free March into a Province with a Fort, then remove the Fort." ], [ vm_space, true, 0, 1, (s)=>has_piece(s, BK, DISC) && can_march_in_space(s) ], [ vm_free_march ], [ vm_asm, ()=>game.vm.p = find_piece(game.vm.s, BK, DISC) ], [ vm_remove ], [ vm_endspace ], [ vm_else ], [ vm_prompt, "Free Migrate into a Province with a Fort, then remove the Fort." ], [ vm_space, true, 0, 1, (s)=>has_piece(s, BK, DISC) && can_migrate_in_space(s) ], [ vm_free_migrate ], [ vm_log_br ], [ vm_asm, ()=>game.vm.p = find_piece(game.vm.s, BK, DISC) ], [ vm_remove ], [ vm_endspace ], [ vm_endif ], [ vm_return ], ] // EVENT 17 CODE[17 * 2 + 0] = [ [ vm_current, BK ], [ vm_add_influence, BK ], [ vm_log_br ], [ vm_prompt, "Remove up to 5 Delhi Sultanate pieces from Provinces where you have a piece." ], [ vm_piece, true, 0, 5, (p,s)=>(is_ds_unit(p) || is_qasbah(p)) && has_piece_faction(piece_space(p), BK) ], [ vm_summary_remove ], [ vm_endpiece ], [ vm_pop_summary ], [ vm_return ], ] // SHADED 17 CODE[17 * 2 + 1] = [ [ vm_prompt, "Remove a Fort and up to 2 Amirs from one Province." ], [ vm_space, true, 0, 1, (s)=>has_piece_faction(s, BK) ], [ vm_prompt, ()=>`Remove a Fort from ${SPACE_NAME[game.vm.s]}.` ], [ vm_piece, true, 0, 1, (p,s)=>is_piece_in_event_space(p) && is_fort(p) ], [ vm_remove ], [ vm_endpiece ], [ vm_prompt, ()=>`Remove up to 2 Amirs from ${SPACE_NAME[game.vm.s]}.` ], [ vm_piece, true, 0, 2, (p,s)=>is_piece_in_event_space(p) && is_amir(p) ], [ vm_summary_remove ], [ vm_endpiece ], [ vm_pop_summary ], [ vm_endspace ], [ vm_resources, false, BK, -2 ], [ vm_return ], ] // EVENT 18 CODE[18 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_if, ()=>n_province_with_both_rebels() > 0 ], [ vm_repeat, ()=>(n_province_with_both_rebels()) ], [ vm_add_influence, BK ], [ vm_endrepeat ], [ vm_else ], [ vm_log, "No Province containing both Bahmani and Vijayanagara pieces." ], [ vm_endif ], [ vm_return ], ] // SHADED 18 CODE[18 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_current, [DS, VE] ], [ vm_prompt, ()=>`Replace an Amir with a ${PIECE_FACTION_TYPE_NAME[game.current][ELITE]}.` ], [ vm_piece, false, 0, 1, (p,s)=>is_amir(p) && is_piece_on_map(p) ], [ vm_replace, ()=>(game.current), ELITE, false, false ], [ vm_endpiece ], [ vm_remove_influence, BK ], [ vm_return ], ] // EVENT 19 CODE[19 * 2 + 0] = [ [ vm_current, REBEL_FACTIONS ], [ vm_prompt, ()=>`Place up to 2 ${PIECE_FACTION_TYPE_NAME[game.current][ELITE]} in Jaunpur and Bengal.` ], [ vm_space, true, 0, 999, (s)=>(s === S_BENGAL || s === S_JAUNPUR) && can_place_piece(s, game.current, ELITE) ], [ vm_place, false, 1, 2, ()=>(game.current), ELITE ], [ vm_endspace ], [ vm_prompt, "Remove a Tributary marker in Jaunpur or Bengal." ], [ vm_space, true, 0, 1, (s)=>(s === S_BENGAL || s === S_JAUNPUR) && is_tributary(s) ], [ vm_remove_tributary ], [ vm_endspace ], [ vm_return ], ] // SHADED 19 CODE[19 * 2 + 1] = [ [ vm_current, DS ], [ vm_prompt, ()=>`Place up to 4 ${PIECE_FACTION_TYPE_NAME[game.current][TROOPS]} in Jaunpur and Bengal.` ], [ vm_space, true, 0, 999, (s)=>(s === S_BENGAL || s === S_JAUNPUR) && can_place_piece(s, game.current, TROOPS) ], [ vm_place, false, 1, 4, ()=>(game.current), TROOPS ], [ vm_endspace ], [ vm_prompt, "Place a Tributary marker in Jaunpur or Bengal." ], [ vm_space, true, 0, 1, (s)=>(s === S_BENGAL || s === S_JAUNPUR) && !is_tributary(s) ], [ vm_place_tributary ], [ vm_endspace ], [ vm_return ], ] // EVENT 20 CODE[20 * 2 + 0] = [ [ vm_current, REBEL_FACTIONS ], [ vm_add_influence, ()=>(game.current) ], [ vm_prompt, "In Orissa or an adjacent Province, you may free Migrate, then free Rally, then Build." ], [ vm_space, true, 1, 1, (s)=>(is_adjacent_to_city(C_WARANGAL, s) || s === S_BENGAL) ], [ vm_mark_space ], [ vm_prompt, ()=>`Migrate in ${SPACE_NAME[game.vm.s]} for free.` ], [ vm_if, ()=>(can_migrate_in_space(game.vm.s)) ], [ vm_free_migrate ], [ vm_endif ], [ vm_endspace ], [ vm_prompt, ()=>`Rally in ${SPACE_NAME[game.vm.m[0]]} for free.` ], [ vm_space, true, 0, 1, (s)=>(game.vm.m[0] === s && can_rally_in_space(s)) ], [ vm_free_rally ], [ vm_endspace ], [ vm_prompt, ()=>`Build in ${SPACE_NAME[game.vm.m[0]]}.` ], [ vm_space, true, 0, 1, (s)=>(game.vm.m[0] === s && can_build_in_space(s)) ], [ vm_free_build ], [ vm_endspace ], [ vm_return ], ] // SHADED 20 CODE[20 * 2 + 1] = [ [ vm_current, DS ], [ vm_prompt, "In Orissa, remove a Temple/Fort, place a Governor, a Qasbah, up to 2 Troops and a Tributary marker." ], [ vm_space, true, 1, 1, (s)=>(s === S_ORISSA) ], [ vm_piece, false, 0, 1, (p,s)=>((is_temple(p) || is_fort(p)) && is_piece_in_event_space(p)) ], [ vm_remove ], [ vm_endpiece ], [ vm_place, false, 1, 1, DS, ELITE ], [ vm_place, false, 1, 1, DS, DISC ], [ vm_place, false, 1, 2, DS, TROOPS ], [ vm_place_tributary ], [ vm_endspace ], [ vm_return ], ] // EVENT 21 CODE[21 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_current, REBEL_FACTIONS ], [ vm_prompt, "Move any Troops from Gondwana and Madhyadesh to Delhi." ], [ vm_move_to, S_DELHI, (p,s)=>(is_troop(p) && (piece_space(p) === S_GONDWANA || piece_space(p) === S_MADHYADESH)) ], [ vm_return ], ] // SHADED 21 CODE[21 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_current, DS ], [ vm_if, ()=>(can_campaign()) ], [ vm_campaign ], [ vm_endif ], [ vm_prompt, ()=>`Govern, Attack, or Demand Obedience in a Province adjacent to the Vindhya Range.` ], [ vm_space, true, 1, 1, (s)=>([S_MALWA, S_MADHYADESH, S_JAUNPUR, S_GONDWANA].includes(s) && can_govern_attack_demand_in_space(s)) ], [ vm_govern_attack_demand ], [ vm_endspace ], [ vm_return ], ] // EVENT 22 CODE[22 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_current, REBEL_FACTIONS ], [ vm_prompt, ()=>`Build in any Province with a ${PIECE_FACTION_TYPE_NAME[game.current][ELITE]}.` ], [ vm_space, true, 1, 1, (s)=>can_build_in_space(s) ], [ vm_free_build ], [ vm_endspace ], [ vm_return ], ] // SHADED 22 CODE[22 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_current, DS ], [ vm_prompt, "Place a Troop in one space." ], [ vm_space, true, 0, 1, (s)=>can_place_piece(s, DS, TROOPS) ], [ vm_place, false, 1, 1, DS, TROOPS ], [ vm_endspace ], [ vm_prompt, "Place up to 2 Troops in a second space." ], [ vm_space, true, 0, 1, (s)=>can_place_piece(s, DS, TROOPS) ], [ vm_place, false, 1, 2, DS, TROOPS ], [ vm_endspace ], [ vm_prompt, "Place up to 4 Troops in a third space." ], [ vm_space, true, 0, 1, (s)=>can_place_piece(s, DS, TROOPS) ], [ vm_place, false, 1, 4, DS, TROOPS ], [ vm_endspace ], [ vm_return ], ] // EVENT 23 CODE[23 * 2 + 0] = [ [ vm_current, VE ], [ vm_add_influence, VE ], [ vm_prompt, "Build and then replace a Unit with a Raja in each Province with a Temple." ], [ vm_space, true, 0, 1, (s)=>can_build_in_space(s) ], [ vm_free_build ], [ vm_log_br ], [ vm_endspace ], [ vm_prompt, "Replace a Unit with a Raja in each Province with a Temple." ], [ vm_space, true, 0, 999, (s)=>has_temple(s) && has_units_enemy_faction(s) ], [ vm_piece, false, 0, 1, (p,s)=>is_enemy_unit(p) && is_piece_in_event_space(p) ], [ vm_replace, VE, ELITE, false, false ], [ vm_endpiece ], [ vm_endspace ], [ vm_return ], ] // SHADED 23 CODE[23 * 2 + 1] = [ [ vm_prompt, "Place 4 Units in a Province with a Temple." ], [ vm_space, true, 0, 1, (s)=>has_temple(s) ], [ vm_place, false, 1, 4, ()=>(game.current), UNITS ], [ vm_prompt, "Remove the Temple." ], [ vm_piece, false, 1, 1, (p,s)=>(is_temple(p) && is_piece_in_event_space(p)) ], [ vm_remove ], [ vm_endpiece ], [ vm_endspace ], [ vm_steal, ()=>(game.current), VE, 2 ], [ vm_return ], ] // EVENT 24 CODE[24 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_current, VE ], [ vm_prompt, "Free Migrate in up to 2 locations." ], [ vm_space, true, 0, 2, (s)=>can_migrate_in_space(s) ], [ vm_free_migrate ], [ vm_mark_space ], [ vm_endspace ], [ vm_log_br ], [ vm_prompt, "Replace a Unit with a Raja in one of the spaces." ], [ vm_clean_p ], [ vm_piece, false, 0, 1, (p,s)=>(set_has(game.vm.m, piece_space(p)) && is_enemy_unit(p)) ], [ vm_replace, ()=>(game.current), ELITE, false, false ], [ vm_endpiece ], [ vm_return ], ] // SHADED 24 CODE[24 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_current, [DS, BK] ], [ vm_prompt, "Place up to 2 Units in each Province with a Temple." ], [ vm_space, true, 0, 999, (s)=>has_temple(s) ], [ vm_place, false, 1, 2, ()=>(game.current), UNITS ], [ vm_endspace ], [ vm_return ], ] // EVENT 25 CODE[25 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_current, REBEL_FACTIONS ], [ vm_event_25 ], [ vm_return ], ] // SHADED 25 CODE[25 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_current, DS ], [ vm_prompt, "Place up to 2 Troops in 3 Provinces adjacent to Goa or Warangal." ], [ vm_space, true, 0, 3, (s)=>(is_adjacent_to_city(C_GOA, s) || is_adjacent_to_city(C_WARANGAL, s)) ], [ vm_mark_space ], [ vm_place, false, 1, 2, DS, TROOPS ], [ vm_endspace ], [ vm_prompt, "Govern in one of the selected Provinces for free." ], [ vm_space, true, 1, 1, (s)=>(can_govern_in_space(s) && set_has(game.vm.m, s)) ], [ vm_free_govern_in_space ], [ vm_endspace ], [ vm_return ], ] // EVENT 26 CODE[26 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_event_26 ], [ vm_return ], ] // EVENT 27 CODE[27 * 2 + 0] = [ [ vm_current, BK ], [ vm_add_influence, BK ], [ vm_prompt, "Place up to 2 Amirs in each of Sindh and Gujarat." ], [ vm_space, true, 0, 999, (s)=>(s === S_GUJARAT || s === S_SINDH) && can_place_piece(s, game.current, ELITE) ], [ vm_place, false, 1, 2, BK, ELITE ], [ vm_endspace ], [ vm_prompt, "Place a Fort and remove a Tributary marker in Sindh." ], [ vm_space, true, 1, 1, (s)=>(s === S_SINDH) ], [ vm_place, false, 0, 1, BK, DISC ], [ vm_remove_tributary ], [ vm_endspace ], [ vm_return ], ] // SHADED 27 CODE[27 * 2 + 1] = [ [ vm_current, DS ], [ vm_prompt, "Place up to 2 Troops and a Governor in each of Sindh and Gujarat." ], [ vm_space, true, 0, 999, (s)=>(s === S_GUJARAT || s === S_SINDH) && (can_place_piece(s, game.current, ELITE) || can_place_piece(s, game.current, TROOPS)) ], [ vm_place, false, 1, 2, DS, TROOPS ], [ vm_place, false, 1, 1, DS, ELITE ], [ vm_endspace ], [ vm_prompt, "Place a Qasbah and a Tributary marker in Sindh." ], [ vm_space, true, 1, 1, (s)=>(s === S_SINDH) ], [ vm_place, false, 0, 1, DS, DISC ], [ vm_place_tributary ], [ vm_endspace ], [ vm_return ], ] // EVENT 28 CODE[28 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_add_influence, BK ], [ vm_remove_influence, VE ], [ vm_current, BK ], [ vm_prompt, "Spend a Cavalry token to place 2 Amirs and remove a Raja in Andhra." ], [ vm_space, true, 0, 1, (s)=>((s === S_ANDHRA) && (n_cavalry(BK) > 0)) ], [ vm_spend_cavalry, 1 ], [ vm_prompt, "Place 2 Amirs in Andhra." ], [ vm_place, false, 0, 2, BK, ELITE ], [ vm_prompt, "Remove a Raja in Andhra." ], [ vm_piece, false, 1, 1, (p,s)=>(is_raja(p) && is_piece_in_event_space(p)) ], [ vm_remove ], [ vm_endpiece ], [ vm_endspace ], [ vm_return ], ] // SHADED 28 CODE[28 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_current, [DS, VE] ], [ vm_if, ()=>(game.current === DS) ], [ vm_prompt, "In a space with a Bahmani piece, place up to 2 Troops and free Attack (defender rolls no dice)." ], [ vm_else ], [ vm_prompt, "In a space with a Bahmani piece, place up to 2 Rajas and free Attack (defender rolls no dice)." ], [ vm_endif ], [ vm_space, true, 1, 1, (s)=>(has_piece_faction(s, BK)) ], [ vm_if, ()=>(game.current === DS) ], [ vm_place, false, 1, 2, DS, TROOPS ], [ vm_else ], [ vm_place, false, 1, 2, VE, ELITE ], [ vm_endif ], [ vm_free_attack, BK ], [ vm_endspace ], [ vm_return ], ] // EVENT 29 CODE[29 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_current, BK ], [ vm_gain_cavalry, ()=>3 ], [ vm_prompt, "Execute a free Limited Attack." ], [ vm_space, true, 1, 1, (s)=>(can_attack_in_space(s)) ], [ vm_free_attack ], [ vm_endspace ], [ vm_return ], ] // SHADED 29 CODE[29 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_shaded_29 ], [ vm_prompt, "Execute a free Limited Attack." ], [ vm_space, true, 1, 1, (s)=>(can_attack_in_space(s)) ], [ vm_free_attack ], [ vm_endspace ], [ vm_return ], ] // EVENT 30 CODE[30 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_current, BK ], [ vm_prompt, "Free Attack in each Province with a Fort (defender rolls no dice)." ], [ vm_space, true, 0, 5, (s)=>(has_fort(s) && can_attack_in_space(s)) ], [ vm_free_attack ], [ vm_endspace ], [ vm_return ], ] // SHADED 30 CODE[30 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_prompt, "Free limited Attack (attacker rolls 4 dice; defender rolls no dice)." ], [ vm_space, true, 1, 1, (s)=>(can_attack_in_space(s)) ], [ vm_free_attack ], [ vm_endspace ], [ vm_return ], ] // EVENT 31 CODE[31 * 2 + 0] = [ [ vm_current, REBEL_FACTIONS ], [ vm_prompt, "Free Migrate into a Province adjacent to Vijayanagara." ], [ vm_space, true, 0, 1, (s)=>(is_adjacent_to_city(C_VIJAYANAGARA, s) && can_migrate_in_space(s)) ], [ vm_mark_space ], [ vm_free_migrate ], [ vm_clean_cmd ], [ vm_clean_p ], [ vm_prompt, ()=>`Remove an opposing Unit from ${SPACE_NAME[game.vm.s]}.` ], [ vm_piece, false, 1, 1, (p,s)=>(is_piece_in_event_space(p) && is_enemy_unit(p)) ], [ vm_remove ], [ vm_endpiece ], [ vm_endspace ], [ vm_prompt, ()=>`Free Attack in ${SPACE_NAME[game.vm.m[0]]}.` ], [ vm_space, true, 0, 1, (s)=>(s === game.vm.m[0] && can_attack_in_space(s)) ], [ vm_free_attack ], [ vm_endspace ], [ vm_return ], ] // SHADED 31 CODE[31 * 2 + 1] = [ [ vm_current, DS ], [ vm_prompt, "Remove a Fort adjacent to Vijayanagara" ], [ vm_piece, false, 0, 1, (p,s)=>(is_fort(p) && is_adjacent_to_city(C_VIJAYANAGARA, piece_space(p))) ], [ vm_remove ], [ vm_endpiece ], [ vm_prompt, "Remove a Temple adjacent to Vijayanagara" ], [ vm_piece, false, 0, 1, (p,s)=>(is_temple(p) && is_adjacent_to_city(C_VIJAYANAGARA, piece_space(p))) ], [ vm_remove ], [ vm_endpiece ], [ vm_remove_influence, BK ], [ vm_remove_influence, VE ], [ vm_return ], ] // EVENT 32 CODE[32 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_current, REBEL_FACTIONS ], [ vm_add_influence, ()=>(game.current) ], [ vm_remove_influence, ()=>(other_rebel_faction(game.current)) ], [ vm_prompt, ()=>`Replace up to 2 ${PIECE_FACTION_TYPE_NAME[other_rebel_faction(game.current)][ELITE]}s with 2 ${PIECE_FACTION_TYPE_NAME[game.current][ELITE]}s.` ], [ vm_piece, false, 0, 2, (p,s)=>is_enemy_piece(p) && is_rebel_faction_elite(p) && is_piece_on_map(p) ], [ vm_replace, ()=>(game.current), ELITE, false, false ], [ vm_endpiece ], [ vm_return ], ] // SHADED 32 CODE[32 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_remove_influence, BK ], [ vm_remove_influence, VE ], [ vm_return ], ] // EVENT 33 CODE[33 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_current, ()=>(has_majority_goa()) ], [ vm_prompt, "Remove up to 3 opposing Units adjacent to Goa." ], [ vm_piece, false, 0, 3, (p,s)=>(is_adjacent_to_city(C_GOA, piece_space(p)) && is_enemy_unit(p)) ], [ vm_summary_remove ], [ vm_endpiece ], [ vm_pop_summary ], [ vm_resources, false, ()=>(game.current), 2 ], [ vm_return ], ] // SHADED 33 CODE[33 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_current, DS ], [ vm_prompt, "Reduce Rebel Factions Resources by half." ], [ vm_resources, true, BK, ()=>(-Math.floor(game.resources[BK]/2)) ], [ vm_resources, true, VE, ()=>(-Math.floor(game.resources[VE]/2)) ], [ vm_return ], ] // EVENT 34 CODE[34 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_current, VE ], [ vm_steal, VE, BK, 2 ], [ vm_steal_cavalry, VE, BK, 2 ], [ vm_return ], ] // SHADED 34 CODE[34 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_current, BK ], [ vm_conspire_trade ], [ vm_return ], ] // EVENT 35 CODE[35 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_current, REBEL_FACTIONS ], [ vm_add_influence, ()=>(game.current) ], [ vm_prompt, ()=>`Place a ${PIECE_FACTION_TYPE_NAME[game.current][ELITE]} in up to 3 Provinces with a ${PIECE_FACTION_TYPE_NAME[other_rebel_faction(game.current)][ELITE]}.` ], [ vm_space, true, 0, 3, (s)=>has_piece(s, other_rebel_faction(game.current), ELITE) && can_place_piece(s, game.current, ELITE) ], [ vm_auto_place, false, 0, false, ()=>(game.current), ELITE ], [ vm_endspace ], [ vm_return ], ] // SHADED 35 CODE[35 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_remove_influence, BK ], [ vm_remove_influence, VE ], [ vm_return ], ] // EVENT 36 CODE[36 * 2 + 0] = [ [ vm_stay_eligible ], [ vm_current, VE ], [ vm_asm, ()=>game.vm.count = 0 ], [ vm_repeat, 4 ], [ vm_prompt, ()=>`Place up to ${4-game.vm.count} Rajas adjacent to Chittor.` ], [ vm_space, true, 0, 1, (s)=>is_adjacent_to_city(C_CHITTOR, s) && can_place_piece(s, VE, ELITE) ], [ vm_auto_place, false, 0, false, VE, ELITE ], [ vm_asm, ()=>(game.vm.count += 1) ], [ vm_endspace ], [ vm_endrepeat ], [ vm_return ], ] // SHADED 36 CODE[36 * 2 + 1] = [ [ vm_stay_eligible ], [ vm_remove_influence, VE ], [ vm_prompt, "Remove up to 3 Rajas from the map." ], [ vm_piece, false, 0, 3, (p,s)=>is_raja(p) && is_piece_on_map(p) ], [ vm_summary_remove ], [ vm_endpiece ], [ vm_pop_summary ], [ vm_return ], ] // SUCC 0 CODE[0 * 2 + 74] = [ [ vm_force_current, DS ], [ vm_log_succ, DS, "Power struggle in Delhi." ], [ vm_flip_dynasty ], [ vm_log_succ, DS, "The Sultanate reimposes its dominance." ], [ vm_campaign ], [ vm_force_current, BK ], [ vm_prompt, "Gain Cavalry equal to Influence and Forts." ], [ vm_log_succ, BK, "Military labor market thrives." ], [ vm_gain_cavalry, ()=>(game.inf[BK] + count_pieces_on_map(BK, DISC)) ], [ vm_force_current, VE ], [ vm_prompt, "Gain Resources equal to Influence and Temples." ], [ vm_log_succ, VE, "Temple towns emerge anew." ], [ vm_resources, false, VE, ()=>(game.inf[VE] + count_pieces_on_map(VE, DISC)) ], [ vm_return ], ] // SUCC 2 CODE[2 * 2 + 74] = [ [ vm_force_current, DS ], [ vm_log_succ, DS, "Delhi monitors its interests." ], [ vm_campaign ], [ vm_force_current, BK ], [ vm_log_succ, BK, "Ala-ud-Din Hasan Bahman Shah gathers allies and takes the Deccan throne." ], [ vm_any_limited_command ], [ vm_if, ()=>(game.prosperity[BK] === 0) ], [ vm_log_br ], [ vm_log, "Not enough Prosperity." ], [ vm_else ], [ vm_cav_resources, ()=>(Math.min(game.prosperity[BK],3)) ], [ vm_endif ], [ vm_clean_cmd ], [ vm_force_current, VE ], [ vm_log_succ, VE, "Sangama brothers establish an empire." ], [ vm_any_limited_command ], [ vm_if, ()=>(game.prosperity[VE] === 0) ], [ vm_log_br ], [ vm_log, "Not enough Prosperity." ], [ vm_else ], [ vm_cav_resources, ()=>(Math.min(game.prosperity[VE],3)) ], [ vm_endif ], [ vm_return ], ] // SUCC 3 CODE[3 * 2 + 74] = [ [ vm_force_current, DS ], [ vm_log_succ, DS, "Renewed focus on infrastructure." ], [ vm_influence_succession ], [ vm_force_current, BK ], [ vm_log_succ, BK, "Trade network covers the Deccan." ], [ vm_influence_succession ], [ vm_force_current, VE ], [ vm_log_succ, VE, "Vast, unifying medieval metropolis." ], [ vm_influence_succession ], [ vm_return ], ] // SUCC 4 CODE[4 * 2 + 74] = [ [ vm_force_current, DS ], [ vm_log_succ, DS, "Succession Crisis in Delhi." ], [ vm_timurid_crisis ], [ vm_return ], ] // SUCC 5 CODE[5 * 2 + 74] = [ [ vm_force_current, DS ], [ vm_prompt, "Mongols attack the Mountain Passes!" ], [ vm_space, true, 1, 1, (s)=>(s === S_MOUNTAIN_PASSES && has_ds_unit(s) && has_mi_unit(s)) ], [ vm_free_attack, DS ], [ vm_endspace ], [ vm_prompt, "Mongols move to Punjab." ], [ vm_space, true, 1, 1, (s)=>(s === S_PUNJAB) ], [ vm_timurid_advance, S_MOUNTAIN_PASSES, S_PUNJAB ], [ vm_endspace ], [ vm_prompt, "Mongols attack Punjab!" ], [ vm_space, true, 1, 1, (s)=>(s === S_PUNJAB && has_ds_unit(s) && has_mi_unit(s)) ], [ vm_free_attack, DS ], [ vm_endspace ], [ vm_prompt, "Mongols move to Delhi." ], [ vm_space, true, 1, 1, (s)=>(s === S_DELHI) ], [ vm_timurid_advance, S_PUNJAB, S_DELHI ], [ vm_endspace ], [ vm_prompt, "Mongols lay siege to Delhi!" ], [ vm_space, true, 1, 1, (s)=>(s === S_DELHI && has_ds_unit(s) && has_mi_unit(s)) ], [ vm_while, ()=>(has_ds_unit(game.vm.s) && has_mi_unit(game.vm.s)) ], [ vm_free_attack, DS ], [ vm_endwhile ], [ vm_endspace ], [ vm_end_game ], [ vm_return ], ]