"use strict" let cards = require("./cards.js") const Afghan = 'Afghan' const British = 'British' const Russian = 'Russian' const Political = 'Political' const Intelligence = 'Intelligence' const Economic = 'Economic' const Military = 'Military' const Persia = 201 const Transcaspia = 202 const Herat = 203 const Kabul = 204 const Kandahar = 205 const Punjab = 206 const Persia_Transcaspia = 301 const Persia_Herat = 302 const Transcaspia_Herat = 303 const Transcaspia_Kabul = 304 const Herat_Kabul = 305 const Herat_Kandahar = 306 const Kabul_Kandahar = 307 const Kabul_Punjab = 308 const Kandahar_Punjab = 309 const Gift2 = 400 const Gift4 = 401 const Gift6 = 402 const Safe_House = 500 const first_region = 201 const last_region = 206 const PUBLIC_WITHDRAWAL = 111 const SAFE_HOUSE_1 = 41 const SAFE_HOUSE_2 = 72 const player_names = [ "Gray", "Blue", "Tan", "Red", "Black", "None" ] const player_index = Object.fromEntries(Object.entries(player_names).map(([k,v])=>[v,k|0])) const region_names = { [Persia]: "Persia", [Transcaspia]: "Transcaspia", [Herat]: "Herat", [Kabul]: "Kabul", [Kandahar]: "Kandahar", [Punjab]: "Punjab", } const region_index = { "Persia": Persia, "Transcaspia": Transcaspia, "Herat": Herat, "Kabul": Kabul, "Kandahar": Kandahar, "Punjab": Punjab, } cards.forEach(card => { if (card) { card.region = region_index[card.region] if (card.name === 'EVENT') card.name = card.if_discarded + " / " + card.if_purchased } }) const border_names = { [Persia_Transcaspia]: "Persia/Transcaspia", [Persia_Herat]: "Persia/Herat", [Transcaspia_Herat]: "Transcaspia/Herat", [Transcaspia_Kabul]: "Transcaspia/Kabul", [Herat_Kabul]: "Herat/Kabul", [Herat_Kandahar]: "Herat/Kandahar", [Kabul_Kandahar]: "Kabul/Kandahar", [Kabul_Punjab]: "Kabul/Punjab", [Kandahar_Punjab]: "Kandahar/Punjab", } const borders = { [Transcaspia]: [ Persia_Transcaspia, Transcaspia_Herat, Transcaspia_Kabul ], [Kabul]: [ Transcaspia_Kabul, Herat_Kabul, Kabul_Kandahar, Kabul_Punjab ], [Punjab]: [ Kabul_Punjab, Kandahar_Punjab ], [Persia]: [ Persia_Transcaspia, Persia_Herat ], [Herat]: [ Transcaspia_Herat, Persia_Herat, Herat_Kabul, Herat_Kandahar ], [Kandahar]: [ Kabul_Kandahar, Herat_Kandahar, Kandahar_Punjab ], } const roads = { [Transcaspia]: [ Persia, Herat, Kabul ], [Kabul]: [ Transcaspia, Herat, Kandahar, Punjab ], [Punjab]: [ Kabul, Kandahar ], [Persia]: [ Transcaspia, Herat ], [Herat]: [ Transcaspia, Persia, Kabul, Kandahar ], [Kandahar]: [ Kabul, Herat, Punjab ], } function is_dominance_check(c) { return c >= 101 && c <= 104 } function is_event_card(c) { return c > 100 } let game = null let player = null let view = null let states = {} const scenario_player_count = { "2P": 2, "3P": 3, "4P": 4, "5P": 5 } exports.scenarios = [ "3P", "4P", "5P", "2P" ] exports.roles = function (scenario) { switch (scenario) { case "2P": return player_names.slice(0, 2) case "3P": return player_names.slice(0, 3) case "4P": return player_names.slice(0, 4) case "5P": return player_names.slice(0, 5) } } exports.ready = function (scenario, options, players) { switch (scenario) { case "2P": return players.length === 2 case "3P": return players.length === 3 case "4P": return players.length === 4 case "5P": return players.length === 5 } } function random(n) { return ((game.seed = game.seed * 69621 % 0x7fffffff) / 0x7fffffff) * n | 0 } function shuffle(deck) { for (let i = deck.length - 1; i > 0; --i) { let j = random(i + 1) let tmp = deck[j] deck[j] = deck[i] deck[i] = tmp } } function remove_from_array(array, item) { let i = array.indexOf(item) if (i >= 0) array.splice(i, 1) } function set_active(new_active) { if (game.active !== new_active) clear_undo() game.active = new_active update_aliases() } function update_aliases() { player = game.players[game.active] } function find_card_in_market(c) { for (let row = 0; row < 2; ++row) for (let col = 0; col < 6; ++col) if (c === game.market_cards[row][col]) return [row, col] return null } function find_card_in_court(c) { for (let p = 0; p < game.players.length; ++p) { let court = game.players[p].court for (let i = 0; i < court.length; ++i) if (court[i] === c) return p } return -1 } function next_player(current) { return (current + 1) % game.players.length } function a_coalition(s) { switch (s) { case Afghan: return "an Afghan" case British: return "a British" case Russian: return "a Russian" } } function logbr() { if (game.log.length > 0 && game.log[game.log.length-1] !== "") game.log.push("") } function log(msg) { game.log.push(msg) } function logi(msg) { game.log.push(">" + msg) } function clear_undo() { game.undo = [] } function push_undo() { game.undo.push(JSON.stringify(game, (k,v) => { if (k === 'undo') return 0 if (k === 'log') return v.length return v })) } function pop_undo() { let save_undo = game.undo let save_log = game.log game = JSON.parse(save_undo.pop()) game.undo = save_undo save_log.length = game.log game.log = save_log } function gen_action(action, argument=undefined) { if (argument !== undefined) { if (!(action in view.actions)) { view.actions[action] = [ argument ] } else { if (!view.actions[action].includes(argument)) view.actions[action].push(argument) } } else { view.actions[action] = 1 } } // STATE QUERIES function active_has_court_card(c) { let court = player.court for (let i = 0; i < court.length; ++i) if (court[i] === c) return true return false } function player_has_court_card(p, c) { let court = game.players[p].court for (let i = 0; i < court.length; ++i) if (court[i] === c) return true return false } function which_player_has_court_card(c) { for (let p = 0; p < game.players.length; ++p) { let court = game.players[p].court for (let i = 0; i < court.length; ++i) if (court[i] === c) return p } return -1 } function active_has_russian_influence() { return active_has_court_card(70) } function active_has_persian_influence() { return active_has_court_card(68) } function active_has_herat_influence() { return active_has_court_card(66) } function active_has_claim_of_ancient_lineage() { return active_has_court_card(5) } function active_has_indian_supplies() { return active_has_court_card(51) } function active_has_well_connected() { return active_has_court_card(56) } function active_has_strange_bedfellows() { return active_has_court_card(21) } function active_has_infrastructure() { return active_has_court_card(78) } function active_has_civil_service_reforms() { return active_has_court_card(24) } function active_has_charismatic_courtiers() { return active_has_court_card(42) } function active_has_blackmail_kandahar() { return active_has_court_card(43) } function active_has_blackmail_herat() { return active_has_court_card(54) } function player_has_bodyguards(p) { return player_has_court_card(p, 15) || player_has_court_card(p, 83) } function player_has_indispensable_advisors(p) { return player_has_court_card(p, 1) } function player_has_citadel_in_kabul(p) { return player_has_court_card(p, 17) } function player_has_citadel_in_transcaspia(p) { return player_has_court_card(p, 97) } function player_has_safe_house(p) { return player_has_court_card(p, 41) || player_has_court_card(p, 72) } function which_player_has_insurrection() { return which_player_has_court_card(3) } function player_has_citadel(p, r) { if (r === Kabul) return player_has_citadel_in_kabul(p) if (r === Transcaspia) return player_has_citadel_in_transcaspia(p) return false } function player_coalition_blocks(p) { switch (game.players[p].loyalty) { case Afghan: return 0 case British: return 12 case Russian: return 24 } } function active_coalition_blocks() { switch (player.loyalty) { case Afghan: return 0 case British: return 12 case Russian: return 24 } } function player_cylinders(p) { return 36 + p * 10 } function active_cylinders() { return 36 + game.active * 10 } function min_gift_cost() { let x = active_cylinders() let avail_2 = true, avail_4 = true, avail_6 = true for (let i = x; i < x + 10; ++i) { let s = game.pieces[i] if (s === Gift2) avail_2 = false else if (s === Gift4) avail_4 = false else if (s === Gift6) avail_6 = false } if (avail_2) return 2 if (avail_4) return 4 if (avail_6) return 6 return 1000 } function gen_place_gift() { let x = active_cylinders() let avail_2 = true, avail_4 = true, avail_6 = true for (let i = x; i < x + 10; ++i) { let s = game.pieces[i] if (s === Gift2) avail_2 = false else if (s === Gift4) avail_4 = false else if (s === Gift6) avail_6 = false } if (avail_2 && player.coins >= 2) gen_action('place_gift', 2) if (avail_4 && player.coins >= 4) gen_action('place_gift', 4) if (avail_6 && player.coins >= 6) gen_action('place_gift', 6) } function ruler_of_region(r) { let ruler = -1 let n_afghan = 0 let n_british = 0 let n_russian = 0 for (let i = 0; i < 12; ++i) { if (game.pieces[i] === r) n_afghan ++ if (game.pieces[i+12] === r) n_british ++ if (game.pieces[i+24] === r) n_russian ++ } let max_ruling = Math.max(n_afghan, n_british, n_russian) for (let p = 0; p < game.players.length; ++p) { let n_tribes = 0 let x = player_cylinders(p) for (let i = x; i < x + 10; ++i) if (game.pieces[i] === r) n_tribes++ let n_ruling = n_tribes if (game.players[p].loyalty === Afghan) n_ruling += n_afghan if (game.players[p].loyalty === British) n_ruling += n_british if (game.players[p].loyalty === Russian) n_ruling += n_russian if (n_ruling === max_ruling) { ruler = -1 } else if (n_ruling > max_ruling) { max_ruling = n_ruling if (n_tribes > 0) ruler = p else ruler = -1 } } return ruler } function player_rules_region(p, r) { return ruler_of_region(r) === p } function active_rules_region(r) { return player_rules_region(game.active, r) } function player_rules_any_region(p) { for (let r = first_region; r <= last_region; ++r) if (player_rules_region(p, r)) return true return false } function active_rules_any_region() { return player_rules_any_region(game.active) } function active_can_betray() { let x = active_cylinders() for (let i = x; i < x + 10; ++i) { if (game.pieces[i] >= 1 && game.pieces[i] <= 100) { let c = game.pieces[i] if (cards[c].suit !== Political) return true let p = find_card_in_court(c) if (!player_has_bodyguards(p)) return true } } return false } function card_has_no_spies(c) { for (let p = 0; p < game.players.length; ++p) { let x = player_cylinders(p) for (let i = x; i < x + 10; ++i) if (game.pieces[i] === c) return false } return true } function market_cost(col, c) { if (col === 0) return 0 if (cards[c].patriot === Russian && active_has_russian_influence()) return 0 if (cards[c].region === Persia && active_has_persian_influence()) return 0 if (cards[c].region === Herat && active_has_herat_influence()) return 0 if (game.favored === Military) return 2 * col return col } function is_favored_suit(c) { if (cards[c].suit === game.favored) return true if (player.events.new_tactics && cards[c].suit === Military) return true if (c === 91) return true // Savvy Operator if (c === 99) return true // Irregulars return false } function rightmost_card(row, i) { while (i >= 0 && game.market_cards[row][i] === 0) --i return i } function pay_action_cost(cost, offset) { player.coins -= cost let ra = rightmost_card(0, 5) let rb = rightmost_card(1, 5) for (let i = 0; i < offset; ++i) { ra = rightmost_card(0, ra-1) rb = rightmost_card(1, rb-1) } for (let i = 0; i < cost; i += 2) { if (ra >= 0) game.market_coins[0][ra] ++ if (rb >= 0) game.market_coins[1][rb] ++ ra = rightmost_card(0, ra-1) rb = rightmost_card(1, rb-1) } check_public_withdrawal() } function remove_all_tribes_and_armies(where) { logi(`Removed all tribes and armies in ${region_names[where]}.`) for (let i = 0; i < 36; ++i) if (game.pieces[i] === where) game.pieces[i] = 0 for (let p = 0; p < game.players.length; ++p) { let x = player_cylinders(p) let maybe_overthrow = false for (let i = x; i < x + 10; ++i) { if (game.pieces[i] === where) { game.pieces[i] = 0 maybe_overthrow = true } } if (maybe_overthrow) check_region_overthrow(p, where) } } function player_with_most_spies(c) { let who = -1 let max_spies = 0 for (let p = 0; p < game.players.length; ++p) { let n_spies = count_player_cylinders(p, c) if (n_spies === max_spies) { who = -1 } else if (n_spies > max_spies) { max_spies = n_spies who = p } } return who } function select_available_cylinder() { let x = active_cylinders() for (let i = x; i < x + 10; ++i) if (game.pieces[i] === 0) return i return -1 } function gen_select_cylinder() { let x = active_cylinders() for (let i = x; i < x + 10; ++i) if (!game.used_pieces.includes(i)) gen_action('piece', i) } function gen_select_spy_to_move() { let x = active_cylinders() for (let i = x; i < x + 10; ++i) if (game.pieces[i] > 0 && game.pieces[i] <= 100) gen_action('piece', i) } function gen_select_tribe_to_move() { let x = active_cylinders() for (let i = x; i < x + 10; ++i) if (game.pieces[i] >= first_region && game.pieces[i] <= last_region) gen_action('piece', i) } function select_available_block() { let b = active_coalition_blocks() for (let i = b + 11; i >= b; --i) if (game.pieces[i] === 0) return i return -1 } function select_afghan_block() { for (let i = 0; i < 12; ++i) if (game.pieces[i] === 0) return i return -1 } function gen_select_block() { let b = active_coalition_blocks() for (let i = b; i < b + 12; ++i) if (!game.used_pieces.includes(i)) gen_action('piece', i) } function gen_select_army_to_move() { let b = active_coalition_blocks() for (let i = b; i < b + 12; ++i) { let r = game.pieces[i] if (r >= first_region && r <= last_region) if (active_can_move_from_region(r)) gen_action('piece', i) } } // DISCARD COURT CARD function discard_court_card(c) { let pidx = -1 // Remove card from court for (let p = 0; p < game.players.length; ++p) { let i = game.players[p].court.indexOf(c) if (i >= 0) { game.players[p].court.splice(i, 1) pidx = p break } } if (pidx !== game.phasing) log(`Discarded #${c} from ${player_names[pidx]} court.`) else log(`Discarded #${c} from court.`) // Return all spies on card for (let p = 0; p < game.players.length; ++p) { let x = player_cylinders(p) for (let i = x; i < x + 10; ++i) { if (game.pieces[i] === c) { log(`Returned ${player_names[p]} spy.`) game.pieces[i] = 0 } } } // Return rupees for leverage if (cards[c].leveraged) { if (pidx !== game.phasing) log(`${player_names[pidx]} paid back leverage.`) else log(`Paid back leverage.`) game.players[pidx].coins -= 2 } if (cards[c].suit === Political) check_court_overthrow(pidx, cards[c].region) } // CHANGE LOYALTY function change_loyalty(new_loyalty) { player.loyalty = new_loyalty log(`Loyalty to ${player.loyalty}.`) for (let i = 0; i < player.court.length;) { let c = player.court[i] let card = cards[c] if (card.patriot && card.patriot !== new_loyalty) { discard_court_card(c) } else { ++i } } let x = active_cylinders() for (let i = x; i < x + 10; ++i) { let s = game.pieces[i] if (s === Gift2 || s === Gift4 || s === Gift6) game.pieces[i] = 0 } player.prizes = 0 } // RETURN LEVERAGE function check_leverage() { let first = game.phasing for (let p = first; p < game.players.length; ++p) if (check_player_leverage(p)) return for (let p = 0; p < first; ++p) if (check_player_leverage(p)) return check_safe_house() } function check_player_leverage(p) { if (game.players[p].coins < 0) { if (game.players[p].hand.length + game.players[p].court.length === 0) { game.players[p].coins = 0 } else { logbr() set_active(p) game.state = 'leverage' return true } } return false } states.leverage = { prompt() { if (player.coins < 0) { view.prompt = `Discard cards from your hand or court to pay back leverage.` for (let i = 0; i < player.hand.length; ++i) gen_action('card', player.hand[i]) for (let i = 0; i < player.court.length; ++i) gen_action('card', player.court[i]) } else { view.prompt = `Discard cards from your hand or court to pay back leverage \u2014 done.` gen_action('next') } }, card(c) { push_undo() player.coins ++ if (player.hand.includes(c)) { log(`Discarded #${c} from hand.`) remove_from_array(player.hand, c) } else { discard_court_card(c) } if (player.hand.length + player.court.length === 0) player.coins = 0 }, next() { push_undo() check_leverage() } } // OVERTHROW function check_court_overthrow(p, r) { let court = game.players[p].court let x = player_cylinders(p) let nc = 0 for (let i = 0; i < court.length; ++i) { let info = cards[court[i]] if (info.suit === Political && info.region === r) ++nc } let nt = 0 for (let i = x; i < x + 10; ++i) if (game.pieces[i] === r) ++nt if (nc === 0 && nt > 0) { log(`${player_names[p]} overthrown in ${region_names[r]}.`) for (let i = x; i < x + 10; ++i) { if (game.pieces[i] === r) { // log(`Removed ${player_names[p]} tribe.`) game.pieces[i] = 0 } } } } function check_region_overthrow(p, r) { let court = game.players[p].court let x = player_cylinders(p) let nc = 0 for (let i = 0; i < court.length; ++i) { let info = cards[court[i]] if (info.suit === Political && info.region === r) ++nc } let nt = 0 for (let i = x; i < x + 10; ++i) if (game.pieces[i] === r) ++nt if (nt === 0 && nc > 0) { log(`${player_names[p]} overthrown in ${region_names[r]}.`) for (let i = 0; i < court.length;) { let info = cards[court[i]] if (info.suit === Political && info.region === r) discard_court_card(court[i]) else ++i } } } // BRIBES states.bribe = { prompt() { let p = game.bribe view.prompt = `Must pay ${game.count} rupee bribe to ${player_names[p]}.` if (player.coins - game.reserve >= game.count) gen_action('pay') gen_action('beg') if (player.events.courtly_manners) gen_action('courtly_manners') if (game.undo.length === 0) gen_action('refuse') }, courtly_manners() { log(`Did not pay bribe.`) end_bribe() }, pay() { let p = game.bribe log(`Paid ${game.count} to ${player_names[p]}.`) game.players[p].coins += game.count game.players[game.active].coins -= game.count end_bribe() }, beg() { let p = game.bribe log(`Asked ${player_names[p]} to waive the bribe for #${game.card}.`) game.state = 'waive' set_active(p) }, refuse() { game.card = 0 game.where = 0 resume_actions() }, } states.waive = { prompt() { if (typeof game.where === 'string') view.prompt = `${player_names[game.phasing]} asks you to waive the bribe to use #${game.card} to ${game.where}.` else view.prompt = `${player_names[game.phasing]} asks you to waive the bribe to play #${game.card}.` let max_cost = Math.min(game.players[game.phasing].coins - game.reserve, game.count) gen_action('waive') for (let i = 1; i < max_cost; ++i) gen_action('offer_' + i) gen_action('refuse') }, waive() { log(`${player_names[game.active]} waived the bribe.`) set_active(game.phasing) end_bribe() }, offer_1() { do_offer(1) }, offer_2() { do_offer(2) }, offer_3() { do_offer(3) }, offer_4() { do_offer(4) }, offer_5() { do_offer(5) }, offer_6() { do_offer(6) }, offer_7() { do_offer(7) }, offer_8() { do_offer(8) }, offer_9() { do_offer(9) }, refuse() { log(`${player_names[game.active]} refused to waive the bribe.`) game.state = 'bribe' set_active(game.phasing) }, } function do_offer(n) { log(`${player_names[game.active]} reduced the bribe to ${n}.`) game.count = 1 game.state = 'bribe' set_active(game.phasing) } function end_bribe() { if (typeof game.where === 'string') do_card_action_2() else do_play_2() } // STARTING LOYALTY states.loyalty = { prompt() { view.prompt = `Choose your loyalty \u2014 Afghan, British, or Russian.` gen_action('loyalty_afghan') gen_action('loyalty_british') gen_action('loyalty_russian') }, loyalty_afghan() { set_starting_loyalty(Afghan) }, loyalty_british() { set_starting_loyalty(British) }, loyalty_russian() { set_starting_loyalty(Russian) }, } function set_starting_loyalty(loyalty) { log(`${player_names[game.active]} loyalty to ${loyalty}.`) player.loyalty = loyalty let next = next_player(game.active) if (game.players[next].loyalty) goto_actions() else set_active(next) } // ACTION PHASE function goto_actions() { game.phasing = game.active game.actions = 2 game.used_cards = [] // track cards that have been used game.used_pieces = [] game.selected = -1 game.where = 0 logbr() log(`.turn ${player_names[game.phasing]}`) logbr() let bmh = active_can_blackmail(Herat) let bmk = active_can_blackmail(Kandahar) if (bmh || bmk) { game.state = 'blackmail' game.where = (bmh && bmk) ? -1 : (bmh ? Herat : Kandahar) game.selected = select_available_cylinder() } else { resume_actions() } } function end_action() { check_leverage() } function resume_actions() { set_active(game.phasing) game.selected = -1 game.where = 0 game.state = 'actions' } function goto_next_player() { game.phasing = next_player(game.phasing) set_active(game.phasing) goto_actions() } states.actions = { prompt() { // Pass / End turn if (game.actions > 0) { gen_action('end_turn_pass') } else { gen_action('end_turn') } // Purchase if (game.actions > 0) { for (let row = 0; row < 2; ++row) { for (let col = 0; col < 6; ++col) { let c = game.market_cards[row][col] if (c && market_cost(col, c) <= player.coins && !game.used_cards.includes(c) && c !== PUBLIC_WITHDRAWAL) gen_action('card', c) } } } // Play if (game.actions > 0) { for (let i = 0; i < player.hand.length; ++i) { let c = player.hand[i] gen_action('play_left', c) gen_action('play_right', c) } } // Card-based actions let bonus = false for (let i = 0; i < player.court.length; ++i) { let c = player.court[i] let card = cards[c] if (game.used_cards.includes(c)) continue let favored = is_favored_suit(c) if (favored || game.actions > 0) { let usable = false if (card.tax && active_can_tax()) { gen_action('tax', c) usable = true } if (card.gift && player.coins >= min_gift_cost()) { gen_action('gift', c) usable = true } if (card.build && player.coins >= 2 && active_rules_any_region()) { gen_action('build', c) usable = true } if (card.move && active_can_move()) { gen_action('move', c) usable = true } if (card.betray && player.coins >= 2 && active_can_betray()) { gen_action('betray', c) usable = true } if (card.battle && active_can_battle()) { gen_action('battle', c) usable = true } if (usable && favored) bonus = true } } if (game.actions === 2) view.prompt = `You may take two actions.` else if (game.actions === 1) view.prompt = `You may take one more action.` else view.prompt = `You have no more actions.` if (bonus) view.prompt += ` You may take bonus actions.` }, // Purchase card from market card(c) { push_undo() logbr() let [row, col] = find_card_in_market(c) game.actions -- let cost = market_cost(col, c) let cost_per_card = cost / col for (let i = 0; i < col; ++i) { if (game.market_cards[row][i] > 0) { game.market_coins[row][i] += cost_per_card game.used_cards.push(game.market_cards[row][i]) } else { game.market_coins[1-row][i] += cost_per_card game.used_cards.push(game.market_cards[1-row][i]) } } check_public_withdrawal() logbr() if (cost > 0) player.coins -= cost let took = game.market_coins[row][col] if (took > 0) player.coins += took game.market_coins[row][col] = 0 game.market_cards[row][col] = 0 log(`Purchased #${c}.`) if (cost > 0 && took > 0) logi(`Paid ${cost} and took ${took}.`) else if (cost > 0) logi(`Paid ${cost}.`) else if (took > 0) logi(`Took ${took}.`) if (is_dominance_check(c)) { do_dominance_check('purchase') } else if (is_event_card(c)) { events_if_purchased[cards[c].if_purchased]() } else { player.hand.push(c) resume_actions() } }, // Play card to court play_left(c) { do_play_1(c, 0) }, play_right(c) { do_play_1(c, 1) }, // Use card based ability tax(c) { do_card_action_1(c, "Tax", 0) }, gift(c) { do_card_action_1(c, "Gift", min_gift_cost()) }, build(c) { do_card_action_1(c, "Build", 2) }, move(c) { do_card_action_1(c, "Move", 0) }, betray(c) { do_card_action_1(c, "Betray", 2) }, battle(c) { do_card_action_1(c, "Battle", 0) }, end_turn_pass() { logbr() log(`Passed.`) goto_cleanup_court() }, end_turn() { goto_cleanup_court() }, } // PLAY CARD function do_play_1(c, side) { push_undo() game.card = c game.where = side if (!active_has_charismatic_courtiers() && !game.events.disregard_for_customs) { let ruler = ruler_of_region(cards[c].region) if (ruler >= 0 && ruler !== game.active) { logbr() game.state = 'bribe' game.count = count_player_cylinders(ruler, cards[c].region) game.reserve = 0 game.bribe = ruler return } } do_play_2() } function do_play_2() { let c = game.card let side = game.where game.actions -- logbr() log(`Played #${c}.`) let idx = player.hand.indexOf(c) player.hand.splice(idx, 1) if (side) player.court.push(c) else player.court.unshift(c) goto_play_patriot() } function goto_play_patriot() { let card = cards[game.card] if (card.patriot && card.patriot !== player.loyalty) change_loyalty(card.patriot) goto_play_tribes() } function goto_play_tribes() { let card = cards[game.card] if (card.tribes) { game.count = card.tribes game.state = 'place_tribe' game.where = card.region game.selected = select_available_cylinder() } else { goto_play_roads() } } states.place_tribe = { inactive: "place tribe", prompt() { if (game.selected < 0) { view.prompt = `Place tribe in ${region_names[game.where]} \u2014 select a cylinder.` gen_select_cylinder() } else { view.prompt = `Place tribe in ${region_names[game.where]}.` gen_action('space', game.where) } }, piece(x) { push_undo() game.selected = x }, space(s) { push_undo() logi(`Tribe to ${region_names[s]}.`) game.pieces[game.selected] = s game.used_pieces.push(game.selected) game.selected = -1 if (--game.count === 0) goto_play_roads() else game.selected = select_available_cylinder() }, } function goto_play_roads() { let card = cards[game.card] if (card.roads) { game.count = card.roads game.state = 'place_road' game.where = card.region game.selected = select_available_block() } else { goto_play_armies() } } states.place_road = { inactive: "place road", prompt() { if (game.selected < 0) { view.prompt = `Place ${player.loyalty} road in ${region_names[game.where]} \u2014 select a block to move.` gen_select_block() } else { view.prompt = `Place ${player.loyalty} road in ${region_names[game.where]}.` for (let s of borders[game.where]) gen_action('space', s) } }, piece(x) { push_undo() game.selected = x }, space(s) { push_undo() logi(`${player.loyalty} road to ${border_names[s]}.`) game.pieces[game.selected] = s game.used_pieces.push(game.selected) game.selected = -1 if (--game.count === 0) goto_play_armies() else game.selected = select_available_block() }, } function goto_play_armies() { let card = cards[game.card] if (card.armies) { game.count = card.armies game.state = 'place_army' game.where = card.region game.selected = select_available_block() } else { goto_play_spies() } } states.place_army = { inactive: "place army", prompt() { if (game.selected < 0) { view.prompt = `Place ${player.loyalty} army in ${region_names[game.where]} \u2014 select a block to move.` gen_select_block() } else { view.prompt = `Place ${player.loyalty} army in ${region_names[game.where]}.` gen_action('space', game.where) } }, piece(x) { push_undo() game.selected = x }, space(s) { push_undo() logi(`${player.loyalty} army to ${region_names[s]}.`) game.pieces[game.selected] = s game.used_pieces.push(game.selected) game.selected = -1 if (--game.count === 0) goto_play_spies() else game.selected = select_available_block() }, } function goto_play_spies() { let card = cards[game.card] if (card.spies) { game.count = card.spies game.state = 'place_spy' game.where = card.region game.selected = select_available_cylinder() } else { goto_play_leveraged() } } states.place_spy = { inactive: "place spy", prompt() { if (game.selected < 0) { view.prompt = `Place spy on a court card in ${region_names[game.where]} \u2014 select a cylinder.` gen_select_cylinder() } else { view.prompt = `Place spy on a court card in ${region_names[game.where]}.` for (let p = 0; p < game.players.length; ++p) { let court = game.players[p].court for (let i = 0; i < court.length; ++i) { let card = cards[court[i]] if (card.region === game.where) { gen_action('card', court[i]) } } } } }, piece(x) { push_undo() game.selected = x }, card(c) { push_undo() logi(`Spy to #${c}.`) game.pieces[game.selected] = c game.used_pieces.push(game.selected) game.selected = -1 if (--game.count === 0) goto_play_leveraged() else game.selected = select_available_cylinder() }, } function goto_play_leveraged() { let card = cards[game.card] if (card.leveraged) { logi(`Leveraged.`) player.coins += 2 } goto_play_favored_suit_impact() } function goto_play_favored_suit_impact() { let card = cards[game.card] if (card.climate && !game.events.pashtunwali_values) { game.state = 'favored_suit_impact' game.where = card.climate } else { end_action() } } states.favored_suit_impact = { inactive: "favored suit", prompt() { view.prompt = `Change the favored suit to ${game.where}.` gen_action('suit', game.where) }, suit(suit) { push_undo() logi(`Favored suit to ${suit}.`) game.favored = suit end_action() }, } // CARD-BASED ACTION (COMMON) const card_action_table = { Tax() { game.state = 'tax' }, Gift() { game.selected = select_available_cylinder() game.state = 'gift' }, Build() { game.count = Math.min(3, Math.floor(player.coins / 2)) if (player.events.nation_building) game.count *= 2 game.selected = select_available_block() game.reserve = 0 game.state = 'build' }, Move() { game.selected = -1 game.state = 'move' }, Betray() { pay_action_cost(2, 0) game.state = 'betray' }, Battle() { game.state = 'battle' game.where = 0 }, } function do_card_action_1(c, what, reserve) { push_undo() game.card = c game.where = what if (!active_has_civil_service_reforms() && !game.events.disregard_for_customs) { let who = player_with_most_spies(c) if (who >= 0 && who !== game.active) { logbr() game.state = 'bribe' game.count = count_player_cylinders(who, c) game.reserve = reserve game.bribe = who return } } do_card_action_2() } const past_what = { Tax: "Taxed", Gift: "Gifted", Build: "Built", Move: "Moved", Betray: "Betrayed", Battle: "Battled", } function do_card_action_2() { let c = game.card let what = game.where game.used_cards.push(c) if (!is_favored_suit(c)) game.actions -- game.count = cards[c].rank logbr() log(`${past_what[what]} with #${c}.`) card_action_table[what]() } // CARD-BASED ACTION: TAX function can_tax_player(active, p, claim) { let okay = claim let shelter = 0 let court = game.players[p].court for (let i = 0; i < court.length; ++i) { let c = court[i] if (!okay && player_rules_region(active, cards[c].region)) okay = true if (cards[c].suit === Economic) shelter += cards[c].rank } return okay && game.players[p].coins > shelter } function do_tax_player(p) { push_undo() logi(`Taxed ${player_names[p]} player.`) game.players[p].coins -- player.coins ++ if (--game.count === 0) end_action() } function active_can_tax() { for (let row = 0; row < 2; ++row) for (let col = 0; col < 6; ++col) if (game.market_coins[row][col] > 0) return true let claim = active_has_claim_of_ancient_lineage() for (let p = 0; p < game.players.length; ++p) if (p !== game.active && can_tax_player(game.active, p, claim)) return true return false } states.tax = { prompt() { if (game.count === 1) view.prompt = `Tax \u2014 take up to ${game.count} rupee from market cards or players.` else view.prompt = `Tax \u2014 take up to ${game.count} rupees from market cards or players.` for (let row = 0; row < 2; ++row) { for (let col = 0; col < 6; ++col) { if (game.market_coins[row][col] > 0) gen_action('card', game.market_cards[row][col]) } } let claim = active_has_claim_of_ancient_lineage() for (let p = 0; p < game.players.length; ++p) { if (p !== game.active && can_tax_player(game.active, p, claim)) { gen_action('player_' + p) } } gen_action('pass') }, pass() { push_undo() end_action() }, card(c) { push_undo() let [row, col] = find_card_in_market(c) logi(`Taxed market.`) game.market_coins[row][col] -- player.coins ++ if (--game.count === 0) end_action() }, player_0() { do_tax_player(0) }, player_1() { do_tax_player(1) }, player_2() { do_tax_player(2) }, player_3() { do_tax_player(3) }, player_4() { do_tax_player(4) }, } // CARD-BASED ACTION: GIFT states.gift = { prompt() { if (game.selected < 0) { view.prompt = `Select cylinder to place as Gift.` gen_select_cylinder() } else { view.prompt = `Place cylinder as Gift.` gen_place_gift() } }, piece(x) { push_undo() game.selected = x }, place_gift(cost) { push_undo() logi(`Paid ${cost}.`) pay_action_cost(cost, 0) if (cost === 2) game.pieces[game.selected] = Gift2 else if (cost === 4) game.pieces[game.selected] = Gift4 else if (cost === 6) game.pieces[game.selected] = Gift6 game.used_pieces.push(game.selected) game.selected = -1 end_action() } } // CARD-BASED ACTION: BUILD states.build = { prompt() { view.prompt = `Build up to ${game.count} armies and/or roads.` gen_action('next') let must_pay = !player.events.nation_building || (game.count & 1) === 0 if (!must_pay || player.coins >= 2) { if (game.selected < 0) { gen_select_block() } else { for (let r = first_region; r <= last_region; ++r) { if (active_rules_region(r)) { gen_action('space', r) for (let s of borders[r]) gen_action('space', s) } } } } }, piece(x) { push_undo() game.selected = x }, space(s) { push_undo() let must_pay = !player.events.nation_building || (game.count & 1) === 0 if (must_pay) pay_action_cost(2, game.reserve++) if (s <= last_region) logi(`${player.loyalty} army to ${region_names[s]}.`) else logi(`${player.loyalty} road to ${border_names[s]}.`) game.pieces[game.selected] = s game.used_pieces.push(game.selected) game.selected = -1 --game.count must_pay = !player.events.nation_building || (game.count & 1) === 0 if (game.count === 0 || (must_pay && player.coins < 2)) end_build_action() else game.selected = select_available_block() }, next() { push_undo() end_build_action() }, } states.infrastructure = { prompt() { view.prompt = `Place an additional block.` if (game.selected < 0) { gen_select_block() } else { for (let r = first_region; r <= last_region; ++r) { if (active_rules_region(r)) { gen_action('space', r) for (let s of borders[r]) gen_action('space', s) } } } }, piece(x) { push_undo() game.selected = x }, space(s) { push_undo() if (s <= last_region) logi(`${player.loyalty} army to ${region_names[s]}.`) else logi(`${player.loyalty} road to ${border_names[s]}.`) game.pieces[game.selected] = s game.used_pieces.push(game.selected) game.selected = -1 end_action() }, } function end_build_action() { if (active_has_infrastructure()) { game.state = 'infrastructure' game.selected = select_available_block() } else { end_action() } } // CARD-BASED ACTION: MOVE function last_court_position() { let n = 0 for (let p = 0; p < game.players.length; ++p) n += game.players[p].court.length return n - 1 } function court_card_from_position(y) { let x = 0 for (let p = 0; p < game.players.length; ++p) { let court = game.players[p].court if (y < x + court.length) return court[y - x] x += court.length } return 0 } function court_position_from_card(c) { let x = 0 for (let p = 0; p < game.players.length; ++p) { let court = game.players[p].court for (let i = 0; i < court.length; ++i) { if (c === court[i]) return x + i } x += court.length } return -1 } function find_border(a, b) { if (a > b) { let c = a; a = b; b = c } if (a === Persia && b === Transcaspia) return Persia_Transcaspia if (a === Persia && b === Herat) return Persia_Herat if (a === Transcaspia && b === Herat) return Transcaspia_Herat if (a === Transcaspia && b === Kabul) return Transcaspia_Kabul if (a === Herat && b === Kabul) return Herat_Kabul if (a === Herat && b === Kandahar) return Herat_Kandahar if (a === Kabul && b === Kandahar) return Kabul_Kandahar if (a === Kabul && b === Punjab) return Kabul_Punjab if (a === Kandahar && b === Punjab) return Kandahar_Punjab throw new Error(`bad border ${a} ${b}`) } function can_army_move_across_border(here, next) { let border = find_border(here, next) let b = active_coalition_blocks() for (let i = b; i < b + 12; ++i) if (game.pieces[i] === border) return true return false } function active_can_move_from_region(here) { let supplies = active_has_indian_supplies() for (let next of roads[here]) if (supplies || can_army_move_across_border(here, next)) return true return false } function active_can_move() { let b = active_coalition_blocks() let x = active_cylinders() for (let i = b; i < b + 12; ++i) { let r = game.pieces[i] if (r >= first_region && r <= last_region) if (active_can_move_from_region(r)) return true } for (let i = x; i < x + 10; ++i) { let r = game.pieces[i] if (r > 0 && r <= 100) return true } if (player.events.nationalism) { for (let i = x; i < x + 10; ++i) { let r = game.pieces[i] if (r >= first_region && r <= last_region) if (active_can_move_from_region(r)) return true } } return false } states.move = { prompt() { if (game.selected >= 0) { let here = game.pieces[game.selected] // Spy on a court card if (here <= 100) { let c = here here = court_position_from_card(here) view.prompt = `Move spy from #${c}.` let last = last_court_position() let prev = here > 0 ? here - 1 : last let next = here < last ? here + 1 : 0 gen_action('card', court_card_from_position(prev)) gen_action('card', court_card_from_position(next)) if (active_has_well_connected()) { let pprev = prev > 0 ? prev - 1 : last let nnext = next < last ? next + 1 : 0 gen_action('card', court_card_from_position(pprev)) gen_action('card', court_card_from_position(nnext)) } if (active_has_strange_bedfellows()) { let r = cards[c].region for (let p = 0; p < game.players.length; ++p) { let court = game.players[p].court for (let i = 0; i < court.length; ++i) if (cards[court[i]].region === r) gen_action('card', court[i]) } } } // Army or tribe in a region else { if (game.selected < 36) view.prompt = `Move ${player.loyalty} army from ${region_names[here]}.` else view.prompt = `Move ${player_names[game.active]} tribe from ${region_names[here]}.` let supplies = active_has_indian_supplies() for (let next of roads[here]) if (supplies || can_army_move_across_border(here, next)) gen_action('space', next) } } else { if (player.events.nationalism) { if (game.count === 1) view.prompt = `Move up to ${game.count} spy, army, or tribe \u2014 select a spy, army, or tribe to move.` else view.prompt = `Move up to ${game.count} spies, armies, and/or tribes \u2014 select a spy, army, or tribe to move.` } else { if (game.count === 1) view.prompt = `Move up to ${game.count} spy or army \u2014 select a spy or army to move.` else view.prompt = `Move up to ${game.count} spies and/or armies \u2014 select a spy or army to move.` } gen_action('next') gen_select_army_to_move() gen_select_spy_to_move() if (player.events.nationalism) gen_select_tribe_to_move() } }, piece(x) { push_undo() game.selected = x }, card(c) { push_undo() let old = game.pieces[game.selected] logi(`Spy from #${old} to #${c}.`) game.pieces[game.selected] = c game.selected = -1 if (--game.count === 0) end_action() }, space(s) { push_undo() let old = game.pieces[game.selected] game.pieces[game.selected] = s if (game.selected < 36) { logi(`${player.loyalty} army from ${region_names[old]} to ${region_names[s]}.`) } else { logi(`Tribe from ${region_names[old]} to ${region_names[s]}.`) check_region_overthrow(game.active, old) } game.selected = -1 if (--game.count === 0) end_action() }, next() { push_undo() end_action() }, } // CARD-BASED ACTION: BETRAY states.betray = { prompt() { view.prompt = `Discard one court card where you have a spy.` let x = active_cylinders() for (let i = x; i < x + 10; ++i) { if (game.pieces[i] > 0 && game.pieces[i] <= 100) { let c = game.pieces[i] if (cards[c].suit !== Political) { gen_action('card', c) } else { let p = find_card_in_court(c) if (!player_has_bodyguards(p)) gen_action('card', c) } } } }, card(c) { push_undo() discard_court_card(c) if (cards[c].prize) { game.card = c game.state = 'accept_prize' } else { end_action() } }, } states.accept_prize = { inactive: "accept prize", prompt() { view.prompt = `You may accept #${game.card} as ${a_coalition(cards[game.card].prize)} prize.` gen_action('accept') gen_action('refuse') }, accept() { log(`Accepted ${cards[game.card].prize} prize.`) if (cards[game.card].prize !== player.loyalty) change_loyalty(cards[game.card].prize) player.prizes ++ end_action() }, refuse() { end_action() }, } // CARD-BASED ACTION: BATTLE function gen_battle_blocks(where) { if (player.loyalty !== Afghan) for (let i = 0; i < 12; ++i) if (game.pieces[i] === where) gen_action('piece', i) if (player.loyalty !== British) for (let i = 12; i < 24; ++i) if (game.pieces[i] === where) gen_action('piece', i) if (player.loyalty !== Russian) for (let i = 24; i < 36; ++i) if (game.pieces[i] === where) gen_action('piece', i) } function count_active_spies_on_card(where) { let n = 0 let x = active_cylinders() for (let i = x; i < x + 10; ++i) if (game.pieces[i] === where) ++n return n } function count_enemy_spies_on_card(where) { let n = 0 for (let p = 0; p < game.players.length; ++p) { if (p === game.active) continue if (player_has_indispensable_advisors(p)) continue let x = player_cylinders(p) for (let i = x; i < x + 10; ++i) { if (game.pieces[i] === where) ++n } } return n } function count_player_armies_in_region(p, where) { let n = 0 let b = player_coalition_blocks(p) for (let i = b; i < b + 12; ++i) if (game.pieces[i] === where) ++n return n } function count_player_cylinders(p, r) { let x = player_cylinders(p) let n = 0 for (let i = x; i < x + 10; ++i) if (game.pieces[i] === r) ++n return n } function count_active_armies_in_region(where) { let n = count_player_armies_in_region(game.active, where) if (player.events.nationalism) n += count_player_cylinders(game.active, where) return n } function count_enemy_blocks_on_border(where) { let n = 0 if (player.loyalty !== Afghan) for (let i = 0; i < 12; ++i) if (game.pieces[i] === where) ++n if (player.loyalty !== British) for (let i = 12; i < 24; ++i) if (game.pieces[i] === where) ++n if (player.loyalty !== Russian) for (let i = 24; i < 36; ++i) if (game.pieces[i] === where) ++n return n } function count_enemy_tribes_and_blocks_in_region(where) { let n = 0 for (let p = 0; p < game.players.length; ++p) { if (game.players[p].loyalty === player.loyalty) continue if (player_has_citadel(p, where)) continue let x = player_cylinders(p) for (let i = x; i < x + 10; ++i) if (game.pieces[i] === where) ++n } if (player.loyalty !== Afghan) for (let i = 0; i < 12; ++i) if (game.pieces[i] === where) ++n if (player.loyalty !== British) for (let i = 12; i < 24; ++i) if (game.pieces[i] === where) ++n if (player.loyalty !== Russian) for (let i = 24; i < 36; ++i) if (game.pieces[i] === where) ++n for (let b of borders[where]) n += count_enemy_blocks_on_border(b) return n } function is_battle_card(where) { return count_active_spies_on_card(where) > 0 && count_enemy_spies_on_card(where) > 0 } function is_battle_region(where) { return count_active_armies_in_region(where) > 0 && count_enemy_tribes_and_blocks_in_region(where) > 0 } function piece_owner(x) { if (x < 12) return "Afghan" if (x < 24) return "British" if (x < 36) return "Russian" if (x < 36+10) return player_names[0] if (x < 36+20) return player_names[1] if (x < 36+30) return player_names[2] if (x < 36+40) return player_names[3] if (x < 36+50) return player_names[4] return "undefined" } function active_can_battle() { for (let r = first_region; r <= last_region; ++r) { if (is_battle_region(r)) return true } for (let p = 0; p < game.players.length; ++p) { let court = game.players[p].court for (let i = 0; i < court.length; ++i) if (is_battle_card(court[i])) return true } return false } states.battle = { prompt() { if (game.where <= 0) { view.prompt = `Start a battle in a single region or on a court card.` for (let p = 0; p < game.players.length; ++p) { let court = game.players[p].court for (let i = 0; i < court.length; ++i) { if (is_battle_card(court[i])) gen_action('card', court[i]) } } for (let r = first_region; r <= last_region; ++r) { if (is_battle_region(r)) gen_action('space', r) } } else { let where = game.where if (where >= first_region && where <= last_region) { view.prompt = `Remove up to ${game.count} tribes, roads, or armies from ${region_names[where]}.` gen_battle_blocks(where) for (let border of borders[where]) gen_battle_blocks(border) for (let p = 0; p < game.players.length; ++p) { if (p !== game.active && game.players[p].loyalty !== player.loyalty && !player_has_citadel(p, where)) { let x = player_cylinders(p) for (let i = x; i < x + 10; ++i) if (game.pieces[i] === where) gen_action('piece', i) } } } else { view.prompt = `Remove up to ${game.count} spies on #${where}.` for (let p = 0; p < game.players.length; ++p) { if (p !== game.active && !player_has_indispensable_advisors(p)) { let x = player_cylinders(p) for (let i = x; i < x + 10; ++i) if (game.pieces[i] === where) gen_action('piece', i) } } } gen_action('next') } }, card(where) { push_undo() //logi(`Battle on #${where}.`) game.where = where game.count = Math.min(game.count, count_active_spies_on_card(where)) }, space(s) { push_undo() //logi(`Battle in ${region_names[s]}.`) game.where = s game.count = Math.min(game.count, count_active_armies_in_region(s)) }, piece(x) { push_undo() let where = game.pieces[x] game.pieces[x] = 0 if (x < 36) { if (where >= first_region && where <= last_region) logi(`Removed ${piece_owner(x)} army from ${region_names[where]}.`) else logi(`Removed ${piece_owner(x)} road from ${border_names[where]}.`) } else { let p = Math.floor((x - 36) / 10) if (where <= 100) { logi(`Removed ${piece_owner(x)} spy from #${where}.`) if (player_has_safe_house(p)) game.pieces[x] = Safe_House } else { logi(`Removed ${piece_owner(x)} tribe from ${region_names[where]}.`) check_region_overthrow(p, where) } } if (--game.count === 0) end_action() }, next() { push_undo() end_action() } } // PASSIVE: BLACKMAIL function active_can_blackmail(r) { if ((r === Herat && active_has_blackmail_herat()) || (r === Kandahar && active_has_blackmail_kandahar())) { for (let p = 0; p < game.players.length; ++p) { let court = game.players[p].court for (let i = 0; i < court.length; ++i) { let c = court[i] if (cards[c].region === r && card_has_no_spies(c)) return true } } } return false } states.blackmail = { prompt() { let bmh = game.where !== Kandahar && active_can_blackmail(Herat) let bmk = game.where !== Herat && active_can_blackmail(Kandahar) let msg = "" if (bmh && bmk) msg = "any Herat and/or Kandahar court card without a spy" else if (bmh) msg = "any Herat court card without a spy" else msg = "any Kandahar court card without a spy" if (game.selected < 0) { view.prompt = `Blackmail \u2014 select a spy to place on ${msg}.` gen_select_cylinder() } else { let bmh = game.where !== Kandahar && active_can_blackmail(Herat) let bmk = game.where !== Herat && active_can_blackmail(Kandahar) view.prompt = `Blackmail \u2014 place a spy on ${msg}.` for (let p = 0; p < game.players.length; ++p) { let court = game.players[p].court for (let i = 0; i < court.length; ++i) { let c = court[i] if (bmh && cards[c].region === Herat && card_has_no_spies(c)) gen_action('card', c) if (bmk && cards[c].region === Kandahar && card_has_no_spies(c)) gen_action('card', c) } } } gen_action('pass') }, piece(x) { push_undo() game.selected = x }, card(c) { push_undo() log(`Blackmail spy to #${c}.`) game.pieces[game.selected] = c game.used_pieces.push(game.selected) if (game.where < 0) { game.where = (cards[c].region === Herat) ? Kandahar : Herat game.selected = select_available_cylinder() } else { resume_actions() } }, pass() { push_undo() resume_actions() }, } // PASSIVE: SAFEHOUSE function check_safe_house() { let first = game.phasing for (let p = first; p < game.players.length; ++p) if (check_player_safe_house(p)) return for (let p = 0; p < first; ++p) if (check_player_safe_house(p)) return if (game.actions >= 0) resume_actions() else goto_cleanup_hand() } function check_player_safe_house(p) { let x = player_cylinders(p) for (let i = x; i < x + 10; ++i) { if (game.pieces[i] === Safe_House) { if (player_has_safe_house(p)) { set_active(p) logbr() game.state = 'safe_house' game.selected = i return true } else { game.pieces[i] = 0 } } } return false } states.safe_house = { inactive: "safe house", prompt() { view.prompt = `Safe House \u2014 you may place your killed spy on a Safe House.` if (player.court.indexOf(SAFE_HOUSE_1) >= 0) gen_action('card', SAFE_HOUSE_1) if (player.court.indexOf(SAFE_HOUSE_2) >= 0) gen_action('card', SAFE_HOUSE_2) gen_action('pass') }, card(c) { push_undo() log(`${player_names[game.active]} spy to #${c}.`) game.pieces[game.selected] = c game.selected = -1 check_safe_house() }, pass() { push_undo() game.pieces[game.selected] = 0 game.selected = -1 check_safe_house() }, } // PASSIVE: INSURRECTION function check_insurrection() { let prince = which_player_has_insurrection() if (prince >= 0) { logbr() log(`Prince Akbar Khan`) game.count = 2 game.selected = select_afghan_block() set_active(prince) game.state = 'insurrection' } else { end_dominance_check() } } states.insurrection = { prompt() { if (game.count > 0) { if (game.selected < 0) { view.prompt = `Insurrection \u2014 select an Afghan block to move.` gen_select_block() } else { view.prompt = `Insurrection \u2014 place Afghan army in Kabul.` gen_action('space', Kabul) } } else { view.prompt = `Insurrection \u2014 done.` gen_action('next') } }, piece(x) { push_undo() game.selected = x }, space() { push_undo() logi(`Afghan army to Kabul.`) game.pieces[game.selected] = Kabul game.selected = -1 if (--game.count > 0) game.selected = select_available_block() }, next() { push_undo() end_dominance_check() } } // CLEANUP function player_court_size() { let stars = 3 for (let i = 0; i < player.court.length; ++i) { let c = player.court[i] if (cards[c].suit === Political) stars += cards[c].rank } return stars } function player_hand_size() { let stars = 2 for (let i = 0; i < player.court.length; ++i) { let c = player.court[i] if (cards[c].suit === Intelligence) stars += cards[c].rank } return stars } function goto_cleanup_court() { game.actions = -1 logbr() if (player.court.length > player_court_size()) { game.state = 'cleanup_court' } else { goto_cleanup_hand() } } states.cleanup_court = { inactive: "cleanup court", prompt() { let size = player_court_size() if (player.court.length <= size) { view.prompt = `Discard cards in your court until you are within your limit \u2014 done.` gen_action('next') } else { view.prompt = `Discard cards in your court until you are within your limit (${size}).` for (let i = 0; i < player.court.length; ++i) gen_action('card', player.court[i]) } }, card(c) { push_undo() discard_court_card(c) }, next() { push_undo() check_leverage() } } function goto_cleanup_hand() { if (player.hand.length > player_hand_size()) { game.state = 'cleanup_hand' } else { goto_discard_events() } } states.cleanup_hand = { inactive: "cleanup hand", prompt() { let size = player_hand_size() if (player.hand.length <= size) { view.prompt = `Discard cards in your hand until you are within your limit \u2014 done.` gen_action('next') } else { view.prompt = `Discard cards in your hand until you are within your limit (${size}).` for (let i = 0; i < player.hand.length; ++i) gen_action('card', player.hand[i]) } }, card(c) { push_undo() log(`Discarded #${c} from hand.`) remove_from_array(player.hand, c) }, next() { push_undo() goto_discard_events() } } function goto_discard_events() { if (is_event_card(game.market_cards[0][0]) || is_event_card(game.market_cards[1][0])) { game.state = 'discard_events' } else { goto_refill_market() } } function do_discard_event(row, c) { game.market_cards[row][0] = 0 logbr() log(`Discarded #${c}.`) if (is_dominance_check(c)) { do_dominance_check('discard') } else { events_if_discarded[cards[c].if_discarded](row) } } states.discard_events = { inactive: "discard event cards", prompt() { view.prompt = `Discard any event cards in the leftmost column of the market.` if (is_event_card(game.market_cards[0][0])) gen_action('card', game.market_cards[0][0]) else if (is_event_card(game.market_cards[1][0])) gen_action('card', game.market_cards[1][0]) }, card(c) { push_undo() if (c === game.market_cards[0][0]) do_discard_event(0, c) else do_discard_event(1, c) }, } function discard_instability_cards() { for (let row = 0; row < 2; ++row) { for (let col = 0; col < 6; ++col) { let c = game.market_cards[row][col] if (is_dominance_check(c)) game.market_cards[row][col] = 0 } } } function goto_refill_market() { // Move all cards (and their rupees) to the left. for (let row = 0; row < 2; ++row) { let row_cards = game.market_cards[row] let row_coins = game.market_coins[row] for (let to = 0; to < 6; ++to) { if (row_cards[to] === 0) { for (let from = to + 1; from < 6; ++from) { if (row_cards[from] > 0) { row_cards[to] = row_cards[from] row_cards[from] = 0 row_coins[to] += row_coins[from] row_coins[from] = 0 break } } } } } // Instability ... let instability = 0 for (let row = 0; row < 2; ++row) { for (let col = 0; col < 6; ++col) { let c = game.market_cards[row][col] if (is_dominance_check(c)) ++instability } } clear_undo() // Fill with new cards from left (top row in each column first) for (let col = 0; col < 6; ++col) { for (let row = 0; row < 2; ++row) { if (game.deck.length > 0) { if (game.market_cards[row][col] === 0) { let c = game.deck.pop() game.market_cards[row][col] = c if (is_dominance_check(c)) { if (++instability > 1) { game.state = 'instability' return } } } } } } goto_next_player() } states.instability = { prompt() { view.prompt = "Instability \u2014 perform a Dominance Check immediately." for (let row = 0; row < 2; ++row) { for (let col = 0; col < 6; ++col) { let c = game.market_cards[row][col] if (is_dominance_check(c)) gen_action('card', c) } } }, card() { logbr() log(`Instability!`) discard_instability_cards() do_dominance_check('instability') } } // EVENTS: IF DISCARDED const events_if_discarded = { "Military" () { goto_favored_suit_event(Military) }, "Embarrassment of Riches" () { game.events.embarrassment_of_riches = 1 goto_discard_events() }, "Disregard for Customs" () { game.events.disregard_for_customs = 1 goto_discard_events() }, "Failure to Impress" () { logi("Discarded all loyalty prizes.") for (let p = 0; p < game.players.length; ++p) game.players[p].prizes = 0 goto_discard_events() }, "Riots in Punjab" () { goto_riots(Punjab) }, "Riots in Herat" () { goto_riots(Herat) }, "No effect" () { logi("No effect.") goto_discard_events() }, "Riots in Kabul" () { goto_riots(Kabul) }, "Riots in Persia" () { goto_riots(Persia) }, "Confidence Failure" () { game.state = 'confidence_failure' if (player.hand.length === 0) next_confidence_failure() }, "Intelligence" () { goto_favored_suit_event(Intelligence) }, "Political" () { goto_favored_suit_event(Political) }, } function goto_favored_suit_event(suit) { game.state = 'favored_suit' game.where = suit } states.favored_suit = { inactive: "favored suit", prompt() { view.prompt = `Change the favored suit to ${game.where}.` gen_action('suit', game.where) }, suit(suit) { logi(`Favored suit to ${suit}.`) game.favored = suit goto_discard_events() }, } function goto_riots(where) { game.state = 'riots' game.where = where } states.riots = { prompt() { view.prompt = `Riot in ${region_names[game.where]}.` gen_action('space', game.where) }, space(s) { remove_all_tribes_and_armies(s) goto_discard_events() }, } states.confidence_failure = { inactive: "confidence failure", prompt() { view.prompt = "Confidence Failure \u2014 discard a card from your hand." for (let i = 0; i < player.hand.length; ++i) gen_action('card', player.hand[i]) }, card(c) { log(`${player_names[game.active]} discarded #${c} from hand.`) remove_from_array(player.hand, c) next_confidence_failure() }, } function next_confidence_failure() { let next = game.active for (let i = 0; i < 10; ++i) { next = next_player(next) if (next === game.phasing) return goto_discard_events() if (game.players[next].hand.length > 0) return set_active(next) } throw new Error("FAIL") } // EVENTS: IF PURCHASED const events_if_purchased = { "New Tactics" () { player.events.new_tactics = 1 end_action() }, "Koh-i-noor Recovered" () { player.events.koh_i_noor = 1 end_action() }, "Courtly Manners" () { player.events.courtly_manners = 1 end_action() }, "Rumor" () { game.state = 'rumor' }, "Conflict Fatigue" () { game.events.conflict_fatigue = 1 end_action() }, "Nationalism" () { player.events.nationalism = 1 end_action() }, "Public Withdrawal" () { throw new Error("cannot purchase") }, "Nation Building" () { player.events.nation_building = 1 end_action() }, "Backing of Persian Aristocracy" () { player.coins += 3 end_action() }, "Other Persuasive Methods" () { game.state = 'other_persuasive_methods' }, "Pashtunwali Values" () { game.state = 'pashtunwali_values' }, "Rebuke" () { game.state = 'rebuke' }, } // TODO: other_persuasive_methods // TODO: pashtunwali_values // TODO: rebuke function check_public_withdrawal() { // Remove any money placed on card "Public Withdrawal" from the game. for (let row = 0; row < 2; ++row) for (let col = 0; col < 6; ++col) if (game.market_cards[row][col] === PUBLIC_WITHDRAWAL) game.market_coins[row][col] = 0 } states.rumor = { prompt() { view.prompt = `Rumor \u2014 choose a player.` for (let p = 0; p < game.players.length; ++p) if (p !== game.active) gen_action('player_' + p) }, player_0() { do_rumor(0) }, player_1() { do_rumor(1) }, player_2() { do_rumor(2) }, player_3() { do_rumor(3) }, player_4() { do_rumor(4) }, } function do_rumor(p) { push_undo() log(`${player_names[game.active]} chose ${player_names[p]} for Rumor.`) game.players[p].events.rumor = 1 end_action() } states.other_persuasive_methods = { inactive: "other persuasive methods", prompt() { view.prompt = `Other Persuasive Methods \u2014 exchange your hand with another player.` for (let p = 0; p < game.players.length; ++p) if (p !== game.active) gen_action('player_' + p) }, player_0() { do_other_persuasive_methods(0) }, player_1() { do_other_persuasive_methods(1) }, player_2() { do_other_persuasive_methods(2) }, player_3() { do_other_persuasive_methods(3) }, player_4() { do_other_persuasive_methods(4) }, } function do_other_persuasive_methods(p) { if (game.open) push_undo() else clear_undo() // No undo if closed hands. log(`${player_names[game.active]} exchanged hands with ${player_names[p]}.`) let swap = game.players[game.active].hand game.players[game.active].hand = game.players[p].hand game.players[p].hand = swap end_action() } states.pashtunwali_values = { inactive: "Pastunwali values", prompt() { view.prompt = `Pashtunwali Values \u2014 choose a suit to favor.` gen_action('suit', Political) gen_action('suit', Intelligence) gen_action('suit', Economic) gen_action('suit', Military) }, suit(suit) { log(`Favored suit to ${suit}.`) game.favored = suit game.events.pashtunwali_values = 1 end_action() }, } states.rebuke = { prompt() { view.prompt = `Rebuke \u2014 remove all tribes and armies in a single region.` for (let s = first_region; s <= last_region; ++s) // TODO: can pick empty region? gen_action('space', s) }, space(s) { push_undo() remove_all_tribes_and_armies(s) end_action() }, } // DOMINANCE CHECK function count_cylinders_in_play(p) { let n = 0 let x = player_cylinders(p) for (let i = x; i < x + 10; ++i) if (game.pieces[i] > 0) ++n return n } function count_influence_points(p) { let n = 1 + game.players[p].prizes let x = player_cylinders(p) if (!game.events.embarrassment_of_riches) { let gv = game.players[p].events.koh_i_noor ? 2 : 1 for (let i = x; i < x + 10; ++i) { let s = game.pieces[i] if (s === Gift2 || s === Gift4 || s === Gift6) n += gv } } if (!game.players[p].events.rumor) { let court = game.players[p].court for (let i = 0; i < court.length; ++i) if (cards[court[i]].patriot) ++n } return n } function assign_vp(points, score, sorted) { const PLACE = [ "1st", "2nd", "3rd" ] let place = 0 sorted.sort((a,b) => b-a) while (points.length > 0 && sorted.length > 0) { let n = 0 for (let p = 0; p < game.players.length; ++p) if (score[p] === sorted[0]) ++n let v = 0 for (let i = 0; i < n; ++i) v += points[i] | 0 v = Math.floor(v / n) let msg = `${PLACE[place]} place:` for (let p = 0; p < game.players.length; ++p) { if (score[p] === sorted[0]) { msg += "\n" + player_names[p] + " scored " + v + " vp." game.players[p].vp += v } } log(msg) points = points.slice(n) sorted = sorted.slice(n) place += n } } function count_dominance_cards() { let n = 0 for (let row = 0; row < 2; ++row) for (let col = 0; col < 6; ++col) if (is_dominance_check(game.market_cards[row][col])) ++n for (let i = 0; i < game.deck.length; ++i) if (is_dominance_check(game.deck[i])) ++n return n } function is_final_dominance_check() { for (let row = 0; row < 2; ++row) for (let col = 0; col < 6; ++col) if (is_dominance_check(game.market_cards[row][col])) return false for (let i = 0; i < game.deck.length; ++i) if (is_dominance_check(game.deck[i])) return false return true } function rank_score(a, b) { return parseInt(b) - parseInt(a) } function do_dominance_check(reason) { let n_afghan = 0 let n_british = 0 let n_russian = 0 let success = null for (let i = 0; i < 12; ++i) { if (game.pieces[i] > 0) n_afghan ++ if (game.pieces[i+12] > 0) n_british ++ if (game.pieces[i+24] > 0) n_russian ++ } logi(n_afghan + " Afghan block" + (n_afghan !== 1 ? "s," : ",")) logi(n_british + " British block" + (n_british !== 1 ? "s," : ",")) logi(n_russian + " Russian block" + (n_russian !== 1 ? "s." : ".")) let limit = game.events.conflict_fatigue ? 2 : 4 if (n_afghan >= n_british+limit && n_afghan >= n_russian+limit) success = Afghan else if (n_british >= n_afghan+limit && n_british >= n_russian+limit) success = British else if (n_russian >= n_british+limit && n_russian >= n_afghan+limit) success = Russian let final = is_final_dominance_check() let score = new Array(game.players.length).fill(0) if (success) { logbr() log(`.dc.${success} Dominant ${success} Coalition`) logbr() if (final) log(`Final Dominance Check.`) let list = [] for (let p = 0; p < game.players.length; ++p) { if (game.players[p].loyalty === success) { score[p] = count_influence_points(p) list.push(`${score[p]} ${player_names[p]} influence`) } } list.sort(rank_score) log("Ranking:\n" + list.join(",\n") + ".") if (final) assign_vp([10, 6, 2], score, score.filter(x=>x>0)) else assign_vp([5, 3, 1], score, score.filter(x=>x>0)) } else { logbr() log(`.dc.unsuccessful Unsuccessful Check`) logbr() if (final) log(`Final Dominance Check.`) let list = [] for (let p = 0; p < game.players.length; ++p) { score[p] = count_cylinders_in_play(p) if (score[p] !== 1) list.push(`${score[p]} ${player_names[p]} cylinders`) else list.push(`${score[p]} ${player_names[p]} cylinder`) } list.sort(rank_score) log("Ranking:\n" + list.join(",\n") + ".") if (final) assign_vp([6, 2], score, score.slice()) else assign_vp([3, 1], score, score.slice()) } // Clear the board. if (success) { for (let i = 0; i < 36; ++i) game.pieces[i] = 0 } // Check instant victory let vps = game.players.map(pp => pp.vp).sort((a,b)=>b-a) if (vps[0] >= vps[1] + 4) return goto_pause_game_over() if (final) return goto_pause_game_over() game.events = {} for (let p = 0; p < game.players.length; ++p) game.players[p].events = {} game.where = reason check_insurrection() } function end_dominance_check() { set_active(game.phasing) if (game.state === 'game_over') return switch (game.where) { case 'discard': return goto_discard_events() case 'purchase': return resume_actions() case 'instability': return goto_refill_market() } } function vp_tie(pp) { let court = pp.court let stars = 0 for (let i = 0; i < court.length; ++i) { let c = court[i] if (cards[c].suit === Military) stars += cards[c].rank } return pp.vp * 10000 + stars * 100 + pp.coins } function compute_victory() { let vps = game.players.map((pp,i) => [vp_tie(pp),i]).sort((a,b)=>b[0]-a[0]) let result = [] for (let i = 0; i < vps.length; ++i) if (vps[i][0] === vps[0][0]) result.push(player_names[vps[i][1]]) game.result = result.join(", ") game.victory = result.join(" and ") + " won!" logbr() log(game.victory) } function goto_pause_game_over() { compute_victory() if (game.undo && game.undo.length > 0) game.state = 'pause_game_over' else game.state = 'game_over' } function goto_game_over() { compute_victory() game.state = 'game_over' game.active = 5 } states.pause_game_over = { inactive: "game over", prompt() { view.prompt = game.victory gen_action('end_game') }, end_game() { game.state = 'game_over' game.active = 5 } } // SETUP function prepare_deck() { let court_cards = [] for (let i = 1; i <= 100; ++i) court_cards.push(i) let event_cards = [] for (let i = 105; i <= 116; ++i) event_cards.push(i) let piles = [ [], [], [], [], [], [] ] shuffle(court_cards) for (let i = 0; i < 6; ++i) for (let k = 0; k < 5 + game.players.length; ++k) piles[i].push(court_cards.pop()) // Leftmost pile is 5, rightmost pile is 0 piles[3].push(101) piles[2].push(102) piles[1].push(103) piles[0].push(104) shuffle(event_cards) piles[4].push(event_cards.pop()) piles[4].push(event_cards.pop()) piles[3].push(event_cards.pop()) piles[2].push(event_cards.pop()) piles[1].push(event_cards.pop()) piles[0].push(event_cards.pop()) for (let i = 0; i < 6; ++i) shuffle(piles[i]) game.deck = piles.flat() } exports.setup = function (seed, scenario, options) { let player_count = scenario_player_count[scenario] game = { seed: seed, open: options.open_hands ? 1 : 0, active: 0, state: "none", used_cards: [], used_pieces: [], count: 0, reserve: 0, bribe: -1, selected: -1, region: 0, card: 0, where: 0, phasing: null, actions: 0, deck: [], favored: Political, events: {}, pieces: new Array(36 + player_count * 10).fill(0), market_cards: [ [0,0,0,0,0,0], [0,0,0,0,0,0], ], market_coins: [ [0,0,0,0,0,0], [0,0,0,0,0,0], ], players: [], log: [], undo: [], } for (let i = 0; i < player_count; ++i) { game.players[i] = { vp: 0, loyalty: null, prizes: 0, coins: 4, hand: [], court: [], events: {}, } } prepare_deck() for (let col = 0; col < 6; ++col) for (let row = 0; row < 2; ++row) game.market_cards[row][col] = game.deck.pop() // Starting loyalty, starting with a random player. game.state = 'loyalty' game.active = random(player_count) return save_game() } function load_game(state) { game = state game.active = player_index[game.active] update_aliases() } function save_game() { game.active = player_names[game.active] return game } exports.action = function (state, current, action, arg) { load_game(state) let S = states[game.state] if (action in S) { S[action](arg, current) } else { if (action === 'undo' && game.undo && game.undo.length > 0) pop_undo() else throw new Error("Invalid action: " + action) } return save_game() } exports.resign = function (state, current) { load_game(state) logbr() log(`${current} resigned.`) goto_game_over() game.victory = `${current} resigned.` return save_game() } exports.is_checkpoint = (a, b) => a.phasing !== b.phasing exports.view = function (state, current) { current = player_index[current] load_game(state) view = { log: game.log, active: player_names[game.active], prompt: null, favored: game.favored, events: game.events, pieces: game.pieces, market_cards: game.market_cards, market_coins: game.market_coins, players: game.players, selected: game.selected, cards: [ game.deck.length, count_dominance_cards() ], open: game.open, } if (game.state === 'game_over') { view.prompt = game.victory } else if (current === 'Observer' || game.active !== current) { let inactive = states[game.state].inactive || game.state view.prompt = `Waiting for ${player_names[game.active]} \u2014 ${inactive}...` } else { view.actions = {} states[game.state].prompt() view.prompt = player_names[game.active] + ": " + view.prompt if (game.undo && game.undo.length > 0) view.actions.undo = 1 else view.actions.undo = 0 } save_game() return view }