summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cards.js1239
-rw-r--r--rules.js2382
2 files changed, 3621 insertions, 0 deletions
diff --git a/cards.js b/cards.js
new file mode 100644
index 0000000..9e9bb39
--- /dev/null
+++ b/cards.js
@@ -0,0 +1,1239 @@
+const cards = [
+ null,
+ {
+ "name": "Mohan Lal",
+ "region": 'Kabul',
+ "suit": 'Intelligence',
+ "rank": 3,
+ "spies": 3,
+ "move": 3,
+ "prize": 'Russian',
+ "number": 1
+ },
+ {
+ "name": "Jan-Fishan Khan",
+ "region": 'Kabul',
+ "suit": 'Intelligence',
+ "rank": 2,
+ "armies": 1,
+ "spies": 2,
+ "climate": 'Military',
+ "move": 2,
+ "battle": 2,
+ "prize": 'Russian',
+ "number": 2
+ },
+ {
+ "name": "Prince Akbar Khan",
+ "region": 'Kabul',
+ "suit": 'Intelligence',
+ "rank": 2,
+ "armies": 1,
+ "spies": 2,
+ "climate": 'Military',
+ "battle": 2,
+ "patriot": 'Afghan',
+ "number": 3
+ },
+ {
+ "name": "Charles Stoddart",
+ "region": 'Kabul',
+ "suit": 'Intelligence',
+ "rank": 1,
+ "spies": 1,
+ "gift": 1,
+ "move": 1,
+ "prize": 'Afghan',
+ "number": 4
+ },
+ {
+ "name": "Shah Shujah Durrani",
+ "region": 'Kabul',
+ "suit": 'Political',
+ "rank": 1,
+ "tribes": 1,
+ "build": 1,
+ "prize": 'Afghan',
+ "number": 5
+ },
+ {
+ "name": "Aminullah Khan Logari",
+ "region": 'Kabul',
+ "suit": 'Political',
+ "rank": 1,
+ "armies": 1,
+ "tribes": 1,
+ "spies": 1,
+ "build": 1,
+ "move": 1,
+ "patriot": 'Afghan',
+ "number": 6
+ },
+ {
+ "name": "Dost Mohammad",
+ "region": 'Kabul',
+ "suit": 'Political',
+ "rank": 2,
+ "armies": 1,
+ "tribes": 2,
+ "spies": 1,
+ "number": 7
+ },
+ {
+ "name": "Kabul Bazaar",
+ "region": 'Kabul',
+ "suit": 'Economic',
+ "rank": 2,
+ "roads": 2,
+ "spies": 1,
+ "climate": 'Political',
+ "tax": 2,
+ "number": 8
+ },
+ {
+ "name": "Afghan Handicrafts",
+ "region": 'Kabul',
+ "suit": 'Economic',
+ "rank": 1,
+ "roads": 1,
+ "tax": 1,
+ "move": 1,
+ "number": 9
+ },
+ {
+ "name": "Balkh Arsenic Mine",
+ "region": 'Kabul',
+ "suit": 'Economic',
+ "rank": 1,
+ "roads": 1,
+ "climate": 'Military',
+ "build": 1,
+ "betray": 1,
+ "prize": 'Afghan',
+ "number": 10
+ },
+ {
+ "name": "Lapis Lazuli Mine",
+ "region": 'Kabul',
+ "suit": 'Economic',
+ "rank": 2,
+ "roads": 2,
+ "tax": 2,
+ "gift": 1,
+ "patriot": 'Afghan',
+ "number": 11
+ },
+ {
+ "name": "City of Ghazni",
+ "region": 'Kabul',
+ "suit": 'Economic',
+ "rank": 3,
+ "armies": 1,
+ "roads": 3,
+ "gift": 1,
+ "number": 12
+ },
+ {
+ "name": "Ghilzai Nomads",
+ "region": 'Kabul',
+ "suit": 'Economic',
+ "rank": 2,
+ "roads": 2,
+ "tax": 2,
+ "move": 2,
+ "prize": 'Russian',
+ "number": 13
+ },
+ {
+ "name": "Money Lenders",
+ "region": 'Kabul',
+ "suit": 'Economic',
+ "rank": 2,
+ "roads": 2,
+ "leveraged": 1,
+ "gift": 1,
+ "build": 1,
+ "number": 14
+ },
+ {
+ "name": "Durrani Royal Guard",
+ "region": 'Kabul',
+ "suit": 'Military',
+ "rank": 1,
+ "armies": 1,
+ "betray": 1,
+ "number": 15
+ },
+ {
+ "name": "Bala Hissar",
+ "region": 'Kabul',
+ "suit": 'Military',
+ "rank": 1,
+ "armies": 1,
+ "tax": 1,
+ "betray": 1,
+ "number": 16
+ },
+ {
+ "name": "Citadel of Ghazni",
+ "region": 'Kabul',
+ "suit": 'Military',
+ "rank": 1,
+ "armies": 1,
+ "build": 1,
+ "number": 17
+ },
+ {
+ "name": "Harry Flashman",
+ "region": 'Punjab',
+ "suit": 'Intelligence',
+ "rank": 1,
+ "spies": 1,
+ "gift": 1,
+ "betray": 1,
+ "number": 18
+ },
+ {
+ "name": "Eldred Pottinger",
+ "region": 'Punjab',
+ "suit": 'Intelligence',
+ "rank": 2,
+ "spies": 2,
+ "climate": 'Economic',
+ "move": 2,
+ "betray": 1,
+ "patriot": 'British',
+ "prize": 'Russian',
+ "number": 19
+ },
+ {
+ "name": "Henry Rawlinson",
+ "region": 'Punjab',
+ "suit": 'Intelligence',
+ "rank": 1,
+ "spies": 1,
+ "climate": 'Economic',
+ "gift": 1,
+ "move": 1,
+ "patriot": 'British',
+ "prize": 'Russian',
+ "number": 20
+ },
+ {
+ "name": "Alexander Burnes",
+ "region": 'Punjab',
+ "suit": 'Intelligence',
+ "rank": 2,
+ "spies": 2,
+ "move": 2,
+ "prize": 'Afghan',
+ "number": 21
+ },
+ {
+ "name": "George Hayward",
+ "region": 'Punjab',
+ "suit": 'Intelligence',
+ "rank": 1,
+ "spies": 1,
+ "climate": 'Political',
+ "gift": 1,
+ "move": 1,
+ "patriot": 'British',
+ "prize": 'Russian',
+ "number": 22
+ },
+ {
+ "name": "Henry Pottinger",
+ "region": 'Punjab',
+ "suit": 'Intelligence',
+ "rank": 1,
+ "spies": 1,
+ "climate": 'Economic',
+ "move": 1,
+ "battle": 1,
+ "patriot": 'British',
+ "prize": 'Russian',
+ "number": 23
+ },
+ {
+ "name": "Ranjit Singh",
+ "region": 'Punjab',
+ "suit": 'Political',
+ "rank": 2,
+ "armies": 1,
+ "tribes": 2,
+ "betray": 1,
+ "number": 24
+ },
+ {
+ "name": "Josiah Harlan",
+ "region": 'Punjab',
+ "suit": 'Political',
+ "rank": 1,
+ "tribes": 1,
+ "tax": 1,
+ "patriot": 'Afghan',
+ "prize": 'British',
+ "number": 25
+ },
+ {
+ "name": "Paolo Avitabile",
+ "region": 'Punjab',
+ "suit": 'Political',
+ "rank": 1,
+ "armies": 1,
+ "tribes": 1,
+ "betray": 1,
+ "prize": 'Afghan',
+ "number": 26
+ },
+ {
+ "name": "Maqpon Dynasty",
+ "region": 'Punjab',
+ "suit": 'Political',
+ "rank": 1,
+ "tribes": 1,
+ "leveraged": 1,
+ "build": 1,
+ "number": 27
+ },
+ {
+ "name": "Anarkali Bazaar",
+ "region": 'Punjab',
+ "suit": 'Economic',
+ "rank": 1,
+ "roads": 1,
+ "spies": 1,
+ "tax": 1,
+ "build": 1,
+ "prize": 'Afghan',
+ "number": 28
+ },
+ {
+ "name": "Khyber Pass",
+ "region": 'Punjab',
+ "suit": 'Economic',
+ "rank": 2,
+ "roads": 2,
+ "tax": 2,
+ "move": 2,
+ "number": 29
+ },
+ {
+ "name": "Sikh Merchants in Lahore",
+ "region": 'Punjab',
+ "suit": 'Economic',
+ "rank": 1,
+ "roads": 1,
+ "spies": 1,
+ "leveraged": 1,
+ "tax": 1,
+ "gift": 1,
+ "number": 30
+ },
+ {
+ "name": "Company Weapons",
+ "region": 'Punjab',
+ "suit": 'Military',
+ "rank": 1,
+ "armies": 1,
+ "climate": 'Intelligence',
+ "tax": 1,
+ "betray": 1,
+ "number": 31
+ },
+ {
+ "name": "Army of the Indus",
+ "region": 'Punjab',
+ "suit": 'Military',
+ "rank": 3,
+ "armies": 3,
+ "move": 3,
+ "patriot": 'British',
+ "prize": 'Afghan',
+ "number": 32
+ },
+ {
+ "name": "Zorawar Singh Kahluria",
+ "region": 'Punjab',
+ "suit": 'Military',
+ "rank": 2,
+ "armies": 2,
+ "gift": 1,
+ "battle": 2,
+ "number": 33
+ },
+ {
+ "name": "Sindhi Warriors",
+ "region": 'Punjab',
+ "suit": 'Military',
+ "rank": 1,
+ "armies": 1,
+ "climate": 'Economic',
+ "move": 1,
+ "battle": 1,
+ "betray": 1,
+ "prize": 'British',
+ "number": 34
+ },
+ {
+ "name": "Hari Singh Nalwa",
+ "region": 'Punjab',
+ "suit": 'Military',
+ "rank": 2,
+ "armies": 2,
+ "climate": 'Political',
+ "move": 2,
+ "battle": 2,
+ "number": 35
+ },
+ {
+ "name": "Bengal Native Infantry",
+ "region": 'Punjab',
+ "suit": 'Military',
+ "rank": 1,
+ "armies": 1,
+ "climate": 'Intelligence',
+ "battle": 1,
+ "betray": 1,
+ "patriot": 'British',
+ "prize": 'Afghan',
+ "number": 36
+ },
+ {
+ "name": "Seaforth Highlanders",
+ "region": 'Punjab',
+ "suit": 'Military',
+ "rank": 1,
+ "armies": 1,
+ "climate": 'Political',
+ "move": 1,
+ "battle": 1,
+ "patriot": 'British',
+ "prize": 'Afghan',
+ "number": 37
+ },
+ {
+ "name": "Akali Sikhs",
+ "region": 'Punjab',
+ "suit": 'Military',
+ "rank": 2,
+ "armies": 2,
+ "battle": 2,
+ "prize": 'British',
+ "number": 38
+ },
+ {
+ "name": "William Moorcroft",
+ "region": 'Kandahar',
+ "suit": 'Intelligence',
+ "rank": 1,
+ "spies": 1,
+ "move": 1,
+ "battle": 1,
+ "number": 39
+ },
+ {
+ "name": "William Hay Macnaghten",
+ "region": 'Kandahar',
+ "suit": 'Intelligence',
+ "rank": 2,
+ "spies": 2,
+ "leveraged": 1,
+ "tax": 2,
+ "move": 2,
+ "patriot": 'British',
+ "prize": 'Afghan',
+ "number": 40
+ },
+ {
+ "name": "Charles Masson",
+ "region": 'Kandahar',
+ "suit": 'Intelligence',
+ "rank": 2,
+ "spies": 2,
+ "build": 1,
+ "number": 41
+ },
+ {
+ "name": "Barakzai Sadars",
+ "region": 'Kandahar',
+ "suit": 'Political',
+ "rank": 1,
+ "tribes": 1,
+ "leveraged": 1,
+ "climate": 'Economic',
+ "build": 1,
+ "patriot": 'Afghan',
+ "prize": 'Afghan',
+ "number": 42
+ },
+ {
+ "name": "Giljee Nobles",
+ "region": 'Kandahar',
+ "suit": 'Political',
+ "rank": 1,
+ "armies": 1,
+ "tribes": 1,
+ "battle": 1,
+ "prize": 'Afghan',
+ "number": 43
+ },
+ {
+ "name": "Baluchi Chiefs",
+ "region": 'Kandahar',
+ "suit": 'Political',
+ "rank": 1,
+ "armies": 1,
+ "tribes": 1,
+ "tax": 1,
+ "build": 1,
+ "prize": 'Russian',
+ "number": 44
+ },
+ {
+ "name": "Haji Khan Kakar",
+ "region": 'Kandahar',
+ "suit": 'Political',
+ "rank": 1,
+ "armies": 1,
+ "tribes": 1,
+ "gift": 1,
+ "betray": 1,
+ "number": 45
+ },
+ {
+ "name": "Bank",
+ "region": 'Kandahar',
+ "suit": 'Economic',
+ "rank": 2,
+ "roads": 2,
+ "leveraged": 1,
+ "climate": 'Intelligence',
+ "gift": 1,
+ "number": 46
+ },
+ {
+ "name": "Bolan Pass",
+ "region": 'Kandahar',
+ "suit": 'Economic',
+ "rank": 2,
+ "armies": 1,
+ "roads": 2,
+ "gift": 1,
+ "move": 2,
+ "prize": 'Russian',
+ "number": 47
+ },
+ {
+ "name": "Fruit Markets",
+ "region": 'Kandahar',
+ "suit": 'Economic',
+ "rank": 1,
+ "roads": 1,
+ "climate": 'Political',
+ "build": 1,
+ "move": 1,
+ "number": 48
+ },
+ {
+ "name": "Kandahari Markets",
+ "region": 'Kandahar',
+ "suit": 'Economic',
+ "rank": 1,
+ "roads": 1,
+ "climate": 'Political',
+ "tax": 1,
+ "build": 1,
+ "number": 49
+ },
+ {
+ "name": "British Regulars",
+ "region": 'Kandahar',
+ "suit": 'Military',
+ "rank": 2,
+ "armies": 2,
+ "move": 2,
+ "battle": 2,
+ "patriot": 'British',
+ "prize": 'Russian',
+ "number": 50
+ },
+ {
+ "name": "Sir John Keane",
+ "region": 'Kandahar',
+ "suit": 'Military',
+ "rank": 1,
+ "armies": 1,
+ "battle": 1,
+ "patriot": 'British',
+ "prize": 'Russian',
+ "number": 51
+ },
+ {
+ "name": "Pashtun Mercenary",
+ "region": 'Kandahar',
+ "suit": 'Military',
+ "rank": 1,
+ "armies": 1,
+ "climate": 'Political',
+ "tax": 1,
+ "battle": 1,
+ "number": 52
+ },
+ {
+ "name": "Jezail Sharpshooters",
+ "region": 'Kandahar',
+ "suit": 'Military',
+ "rank": 2,
+ "armies": 2,
+ "spies": 1,
+ "betray": 1,
+ "prize": 'Russian',
+ "number": 53
+ },
+ {
+ "name": "Herati Bandits",
+ "region": 'Herat',
+ "suit": 'Intelligence',
+ "rank": 1,
+ "spies": 1,
+ "leveraged": 1,
+ "tax": 1,
+ "number": 54
+ },
+ {
+ "name": "Hazara Chiefs",
+ "region": 'Herat',
+ "suit": 'Political',
+ "rank": 1,
+ "armies": 1,
+ "tribes": 1,
+ "tax": 1,
+ "move": 1,
+ "battle": 1,
+ "number": 55
+ },
+ {
+ "name": "Yar Mohammad Alikozai",
+ "region": 'Herat',
+ "suit": 'Political',
+ "rank": 2,
+ "tribes": 2,
+ "climate": 'Political',
+ "betray": 1,
+ "prize": 'Afghan',
+ "number": 56
+ },
+ {
+ "name": "Exiled Durrani Nobility",
+ "region": 'Herat',
+ "suit": 'Political',
+ "rank": 1,
+ "tribes": 1,
+ "move": 1,
+ "betray": 1,
+ "prize": 'British',
+ "number": 57
+ },
+ {
+ "name": "Ishaqzai Chiefs",
+ "region": 'Herat',
+ "suit": 'Political',
+ "rank": 1,
+ "tribes": 1,
+ "climate": 'Economic',
+ "gift": 1,
+ "build": 1,
+ "patriot": 'Afghan',
+ "number": 58
+ },
+ {
+ "name": "Tajik Warband",
+ "region": 'Herat',
+ "suit": 'Military',
+ "rank": 2,
+ "armies": 2,
+ "battle": 2,
+ "betray": 1,
+ "number": 59
+ },
+ {
+ "name": "Nomadic Warlord",
+ "region": 'Herat',
+ "suit": 'Military',
+ "rank": 1,
+ "armies": 1,
+ "tax": 1,
+ "gift": 1,
+ "battle": 1,
+ "prize": 'Russian',
+ "number": 60
+ },
+ {
+ "name": "Karakul Sheep",
+ "region": 'Herat',
+ "suit": 'Economic',
+ "rank": 2,
+ "roads": 2,
+ "tax": 2,
+ "number": 61
+ },
+ {
+ "name": "Qanat System",
+ "region": 'Herat',
+ "suit": 'Economic',
+ "rank": 1,
+ "roads": 1,
+ "climate": 'Political',
+ "gift": 1,
+ "build": 1,
+ "number": 62
+ },
+ {
+ "name": "Farah Road",
+ "region": 'Herat',
+ "suit": 'Economic',
+ "rank": 3,
+ "roads": 3,
+ "move": 3,
+ "number": 63
+ },
+ {
+ "name": "Opium Fields",
+ "region": 'Herat',
+ "suit": 'Economic',
+ "rank": 2,
+ "roads": 2,
+ "spies": 1,
+ "climate": 'Military',
+ "tax": 2,
+ "number": 64
+ },
+ {
+ "name": "Minaret of Jam",
+ "region": 'Herat',
+ "suit": 'Economic',
+ "rank": 1,
+ "armies": 1,
+ "roads": 1,
+ "tax": 1,
+ "move": 1,
+ "battle": 1,
+ "number": 65
+ },
+ {
+ "name": "Baluchi Smugglers",
+ "region": 'Herat',
+ "suit": 'Economic',
+ "rank": 2,
+ "armies": 1,
+ "roads": 2,
+ "battle": 2,
+ "prize": 'Afghan',
+ "number": 66
+ },
+ {
+ "name": "Wheat Fields",
+ "region": 'Herat',
+ "suit": 'Economic',
+ "rank": 2,
+ "roads": 1,
+ "gift": 1,
+ "move": 2,
+ "number": 67
+ },
+ {
+ "name": "Ghaem Magham Farahani",
+ "region": 'Persia',
+ "suit": 'Intelligence',
+ "rank": 2,
+ "armies": 1,
+ "spies": 2,
+ "tax": 2,
+ "number": 68
+ },
+ {
+ "name": "Count Ivan Simonich",
+ "region": 'Persia',
+ "suit": 'Intelligence',
+ "rank": 2,
+ "spies": 2,
+ "move": 2,
+ "battle": 2,
+ "patriot": 'Russian',
+ "prize": 'British',
+ "number": 69
+ },
+ {
+ "name": "Alexander Griboyedov",
+ "region": 'Persia',
+ "suit": 'Intelligence',
+ "rank": 2,
+ "spies": 2,
+ "move": 2,
+ "patriot": 'Russian',
+ "prize": 'Afghan',
+ "number": 70
+ },
+ {
+ "name": "Joseph Wolff",
+ "region": 'Persia',
+ "suit": 'Intelligence',
+ "rank": 1,
+ "spies": 1,
+ "gift": 1,
+ "move": 1,
+ "patriot": 'British',
+ "prize": 'Afghan',
+ "number": 71
+ },
+ {
+ "name": "Claude Wade",
+ "region": 'Persia',
+ "suit": 'Intelligence',
+ "rank": 2,
+ "spies": 2,
+ "move": 2,
+ "prize": 'Russian',
+ "number": 72
+ },
+ {
+ "name": "Jean-François Allard",
+ "region": 'Persia',
+ "suit": 'Intelligence',
+ "rank": 1,
+ "spies": 1,
+ "climate": 'Military',
+ "build": 1,
+ "battle": 1,
+ "prize": 'British',
+ "number": 73
+ },
+ {
+ "name": "Hajj Mirza Aghasi",
+ "region": 'Persia',
+ "suit": 'Political',
+ "rank": 1,
+ "tribes": 1,
+ "climate": 'Intelligence',
+ "battle": 1,
+ "betray": 1,
+ "patriot": 'Afghan',
+ "prize": 'Russian',
+ "number": 74
+ },
+ {
+ "name": "Abbas Mirza",
+ "region": 'Persia',
+ "suit": 'Political',
+ "rank": 1,
+ "armies": 1,
+ "tribes": 1,
+ "tax": 1,
+ "battle": 1,
+ "number": 75
+ },
+ {
+ "name": "Fath-Ali Shah",
+ "region": 'Persia',
+ "suit": 'Political',
+ "rank": 2,
+ "tribes": 2,
+ "climate": 'Intelligence',
+ "number": 76
+ },
+ {
+ "name": "Mohammad Shah",
+ "region": 'Persia',
+ "suit": 'Political',
+ "rank": 1,
+ "armies": 1,
+ "tribes": 1,
+ "climate": 'Intelligence',
+ "build": 1,
+ "betray": 1,
+ "number": 77
+ },
+ {
+ "name": "Civic Improvements",
+ "region": 'Persia',
+ "suit": 'Economic',
+ "rank": 2,
+ "roads": 2,
+ "leveraged": 1,
+ "climate": 'Intelligence',
+ "build": 1,
+ "number": 78
+ },
+ {
+ "name": "Persian Slave Markets",
+ "region": 'Persia',
+ "suit": 'Economic',
+ "rank": 1,
+ "roads": 1,
+ "spies": 1,
+ "leveraged": 1,
+ "tax": 1,
+ "move": 1,
+ "prize": 'British',
+ "number": 79
+ },
+ {
+ "name": "Anglo-Persian Trade",
+ "region": 'Persia',
+ "suit": 'Economic',
+ "rank": 1,
+ "roads": 1,
+ "leveraged": 1,
+ "tax": 1,
+ "build": 1,
+ "patriot": 'British',
+ "prize": 'Russian',
+ "number": 80
+ },
+ {
+ "name": "Russo-Persian Trade",
+ "region": 'Persia',
+ "suit": 'Economic',
+ "rank": 2,
+ "roads": 2,
+ "leveraged": 1,
+ "tax": 2,
+ "build": 1,
+ "patriot": 'Russian',
+ "prize": 'British',
+ "number": 81
+ },
+ {
+ "name": "Persian Army",
+ "region": 'Persia',
+ "suit": 'Military',
+ "rank": 2,
+ "armies": 2,
+ "climate": 'Intelligence',
+ "gift": 1,
+ "battle": 2,
+ "number": 82
+ },
+ {
+ "name": "Shah's Guard",
+ "region": 'Persia',
+ "suit": 'Military',
+ "rank": 1,
+ "armies": 1,
+ "spies": 1,
+ "tax": 1,
+ "number": 83
+ },
+ {
+ "name": "Russian Regulars",
+ "region": 'Persia',
+ "suit": 'Military',
+ "rank": 2,
+ "armies": 2,
+ "climate": 'Economic',
+ "battle": 2,
+ "patriot": 'Russian',
+ "prize": 'Afghan',
+ "number": 84
+ },
+ {
+ "name": "Bukharan Jews",
+ "region": 'Transcaspia',
+ "suit": 'Intelligence',
+ "rank": 1,
+ "spies": 1,
+ "leveraged": 1,
+ "climate": 'Economic',
+ "gift": 1,
+ "build": 1,
+ "prize": 'British',
+ "number": 85
+ },
+ {
+ "name": "Jan Prosper Witkiewicz",
+ "region": 'Transcaspia',
+ "suit": 'Intelligence',
+ "rank": 2,
+ "spies": 2,
+ "build": 1,
+ "move": 2,
+ "patriot": 'Russian',
+ "number": 86
+ },
+ {
+ "name": "Imperial Surveyors",
+ "region": 'Transcaspia',
+ "suit": 'Intelligence',
+ "rank": 1,
+ "spies": 1,
+ "gift": 1,
+ "build": 1,
+ "move": 1,
+ "patriot": 'Russian',
+ "number": 87
+ },
+ {
+ "name": "Arthur Conolly",
+ "region": 'Transcaspia',
+ "suit": 'Intelligence',
+ "rank": 1,
+ "spies": 1,
+ "gift": 1,
+ "move": 1,
+ "patriot": 'British',
+ "number": 88
+ },
+ {
+ "name": "Aga Mehdi",
+ "region": 'Transcaspia',
+ "suit": 'Intelligence',
+ "rank": 1,
+ "spies": 1,
+ "move": 1,
+ "battle": 1,
+ "betray": 1,
+ "patriot": 'Russian',
+ "number": 89
+ },
+ {
+ "name": "Nasrullah Khan",
+ "region": 'Transcaspia',
+ "suit": 'Political',
+ "rank": 1,
+ "tribes": 1,
+ "tax": 1,
+ "betray": 1,
+ "prize": 'British',
+ "number": 90
+ },
+ {
+ "name": "Allah Quli Bahadur",
+ "region": 'Transcaspia',
+ "suit": 'Political',
+ "rank": 1,
+ "armies": 1,
+ "tribes": 1,
+ "spies": 1,
+ "betray": 1,
+ "prize": 'Russian',
+ "number": 91
+ },
+ {
+ "name": "Mir Murad Beg",
+ "region": 'Transcaspia',
+ "suit": 'Political',
+ "rank": 1,
+ "tribes": 1,
+ "spies": 1,
+ "climate": 'Military',
+ "tax": 1,
+ "move": 1,
+ "battle": 1,
+ "prize": 'British',
+ "number": 92
+ },
+ {
+ "name": "Madali Khan",
+ "region": 'Transcaspia',
+ "suit": 'Political',
+ "rank": 2,
+ "tribes": 2,
+ "battle": 2,
+ "betray": 1,
+ "prize": 'British',
+ "number": 93
+ },
+ {
+ "name": "Khivan Slave Markets",
+ "region": 'Transcaspia',
+ "suit": 'Economic',
+ "rank": 1,
+ "roads": 1,
+ "spies": 1,
+ "leveraged": 1,
+ "tax": 1,
+ "betray": 1,
+ "prize": 'British',
+ "number": 94
+ },
+ {
+ "name": "Supplies from Orenburg",
+ "region": 'Transcaspia',
+ "suit": 'Economic',
+ "rank": 1,
+ "roads": 1,
+ "tax": 1,
+ "move": 1,
+ "patriot": 'Russian',
+ "prize": 'Afghan',
+ "number": 95
+ },
+ {
+ "name": "Panjdeh Oasis",
+ "region": 'Transcaspia',
+ "suit": 'Economic',
+ "rank": 2,
+ "roads": 2,
+ "tax": 2,
+ "move": 2,
+ "prize": 'British',
+ "number": 96
+ },
+ {
+ "name": "Ark of Bukhara",
+ "region": 'Transcaspia',
+ "suit": 'Military',
+ "rank": 1,
+ "armies": 1,
+ "climate": 'Intelligence',
+ "build": 1,
+ "number": 97
+ },
+ {
+ "name": "European Cannons",
+ "region": 'Transcaspia',
+ "suit": 'Military',
+ "rank": 3,
+ "armies": 3,
+ "gift": 1,
+ "prize": 'British',
+ "number": 98
+ },
+ {
+ "name": "Cossacks",
+ "region": 'Transcaspia',
+ "suit": 'Military',
+ "rank": 1,
+ "armies": 1,
+ "spies": 1,
+ "battle": 1,
+ "patriot": 'Russian',
+ "prize": 'British',
+ "number": 99
+ },
+ {
+ "name": "Count Perovsky",
+ "region": 'Transcaspia',
+ "suit": 'Military',
+ "rank": 2,
+ "armies": 2,
+ "battle": 2,
+ "patriot": 'Russian',
+ "prize": 'British',
+ "number": 100
+ },
+
+ {
+ "name": "Dominance Check",
+ "if_discarded": "Dominance Check",
+ "if_purchased": "Dominance Check",
+ "number": 101,
+ },
+ {
+ "name": "Dominance Check",
+ "if_discarded": "Dominance Check",
+ "if_purchased": "Dominance Check",
+ "number": 102,
+ },
+ {
+ "name": "Dominance Check",
+ "if_discarded": "Dominance Check",
+ "if_purchased": "Dominance Check",
+ "number": 103,
+ },
+ {
+ "name": "Dominance Check",
+ "if_discarded": "Dominance Check",
+ "if_purchased": "Dominance Check",
+ "number": 104,
+ },
+
+ {
+ "name": "EVENT",
+ "if_discarded": "Military",
+ "if_purchased": "New Tactics",
+ "number": 105,
+ },
+
+ {
+ "name": "EVENT",
+ "if_discarded": "Embarrassment of Riches",
+ "if_purchased": "Koh-i-noor Recovered",
+ "number": 106,
+ },
+
+ {
+ "name": "EVENT",
+ "if_discarded": "Disregard for Customs",
+ "if_purchased": "Courtly Manners",
+ "number": 107,
+ },
+
+ {
+ "name": "EVENT",
+ "if_discarded": "Failure to Impress",
+ "if_purchased": "Rumor",
+ "number": 108,
+ },
+
+ {
+ "name": "EVENT",
+ "if_discarded": "Riots in Punjab",
+ "if_purchased": "Conflict Fatigue",
+ "number": 109,
+ },
+
+ {
+ "name": "EVENT",
+ "if_discarded": "Riots in Herat",
+ "if_purchased": "Nationalism",
+ "number": 110,
+ },
+
+ {
+ "name": "EVENT",
+ "if_discarded": "No effect",
+ "if_purchased": "Public Withdrawal",
+ "number": 111,
+ },
+
+ {
+ "name": "EVENT",
+ "if_discarded": "Riots in Kabul",
+ "if_purchased": "Nation Building",
+ "number": 112,
+ },
+
+ {
+ "name": "EVENT",
+ "if_discarded": "Riots in Persia",
+ "if_purchased": "Backing of Persian Aristocracy",
+ "number": 113,
+ },
+
+ {
+ "name": "EVENT",
+ "if_discarded": "Confidence Failure",
+ "if_purchased": "Other Persuasive Methods",
+ "number": 114,
+ },
+
+ {
+ "name": "EVENT",
+ "if_discarded": "Intelligence",
+ "if_purchased": "Pashtunwali Values",
+ "number": 115,
+ },
+
+ {
+ "name": "EVENT",
+ "if_discarded": "Political",
+ "if_purchased": "Rebuke",
+ "number": 116,
+ }
+];
+
+if (typeof module !== 'undefined')
+ module.exports = cards;
diff --git a/rules.js b/rules.js
new file mode 100644
index 0000000..8b482f9
--- /dev/null
+++ b/rules.js
@@ -0,0 +1,2382 @@
+"use strict";
+
+// TODO: layout pieces on map
+// TODO: check all card data
+
+// TODO: safe house passive
+// TODO: events
+
+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 Gift = 400;
+const Safehouse = 500;
+
+const first_region = 201;
+const last_region = 206;
+
+const player_names = [
+ "Gray",
+ "Blue",
+ "Tan",
+ "Red",
+ "Black",
+];
+
+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(c => { if (c) c.region = region_index[c.region] });
+
+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) {
+ 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_or_an(s) {
+ let x = s[0];
+ switch (s[0]) {
+ case 'A': case 'a':
+ case 'O': case 'o':
+ case 'E': case 'e':
+ case 'U': case 'u':
+ case 'I': case 'i':
+ return "an " + s;
+ }
+ return "a " + s;
+}
+
+function logbr() {
+ if (game.log.length > 0 && game.log[game.log.length-1] !== "")
+ game.log.push("");
+}
+
+function log(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 any_player_has_court_card(p, 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 true;
+ }
+ return false;
+}
+
+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 any_player_has_insurrection(p) { return any_player_has_court_card(p, 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 active_gifts() {
+ let x = active_cylinders();
+ let n = 0;
+ for (let i = x; i < x + 10; ++i)
+ if (game.pieces[i] === Gift)
+ ++n;
+ return n;
+}
+
+function gift_cost() {
+ return 2 * active_gifts() + 2;
+}
+
+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_has_spy() {
+ let x = active_cylinders();
+ for (let i = x; i < x + 10; ++i)
+ if (game.pieces[i] >= 1 && game.pieces[i] <= 100)
+ 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 (c === 91) return true; // Savvy Operator
+ if (c === 99) return true; // Irregulars
+}
+
+function rightmost_card(row, i) {
+ while (i >= 0 && game.market_cards[row][i] === 0)
+ --i;
+ return i;
+}
+
+function pay_action_cost(count) {
+ log(`Paid ${count} rupees.`);
+ player.coins -= count;
+ let ra = rightmost_card(0, 5);
+ let rb = rightmost_card(1, 5);
+ for (let i = 0; i < count; 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);
+ }
+}
+
+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 select_available_block() {
+ let b = active_coalition_blocks();
+ for (let i = b; i < b + 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() {
+ // TODO: only select armies in regions that can move
+ let b = active_coalition_blocks();
+ for (let i = b; i < b + 12; ++i)
+ if (game.pieces[i] >= first_region && game.pieces[i] <= last_region)
+ 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;
+ }
+ }
+
+ log(`${player_names[pidx]} discarded ${cards[c].name}.`);
+
+ // 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)
+ game.pieces[i] = 0;
+ }
+
+ // Return rupees for leverage
+ if (cards[c].leveraged) {
+ log(`${player_names[pidx]} returned leverage.`);
+ game.players[pidx].coins -= 2;
+ }
+
+ check_court_overthrow(pidx, cards[c].region);
+}
+
+// CHANGE LOYALTY
+
+function change_loyalty(new_loyalty) {
+ player.loyalty = new_loyalty;
+
+ log(`${player_names[game.active]} switches 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)
+ if (game.pieces[i] === Gift)
+ 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;
+ resume_actions();
+}
+
+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 {
+ if (game.active !== p)
+ clear_undo();
+ 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 for 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 for leverage \u2014 done.`;
+ gen_action('next');
+ }
+ },
+ card(c) {
+ push_undo();
+ player.coins ++;
+ if (player.hand.includes(c))
+ remove_from_array(player.hand, c);
+ else
+ discard_court_card(c);
+ if (player.hand.length + player.court.length === 0)
+ player.coins = 0;
+ },
+ next() {
+ clear_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)
+ if (cards[court[i]].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]} is overthrown in ${region_names[r]}.`);
+ for (let i = x; i < x + 10; ++i)
+ if (game.pieces[i] === r)
+ 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)
+ if (cards[court[i]].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]} is overthrown in ${region_names[r]}.`);
+ for (let i = 0; i < court.length;) {
+ if (cards[court[i]].region === r)
+ discard_court_card(court[i]);
+ else
+ ++i;
+ }
+ }
+}
+
+// BRIBES
+
+states.bribe = {
+ prompt() {
+ let p = ruler_of_region(cards[game.card].region);
+ 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 (game.undo.length === 0)
+ gen_action('refuse');
+ },
+ pay() {
+ let p = ruler_of_region(cards[game.card].region);
+ game.players[p].coins += game.count;
+ game.players[game.active].coins -= game.count;
+ end_bribe();
+ },
+ beg() {
+ clear_undo();
+ let p = ruler_of_region(cards[game.card].region);
+ 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 ${cards[game.card].name} to ${game.where}.`;
+ else
+ view.prompt = `${player_names[game.phasing]} asks you to waive the bribe to play ${cards[game.card].name}.`;
+ 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]} pledged ${loyalty} 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() {
+ clear_undo();
+ game.phasing = next_player(game.phasing);
+ set_active(game.phasing);
+ goto_actions();
+}
+
+states.actions = {
+ prompt() {
+ if (game.actions === 2)
+ view.prompt = `You have two actions.`;
+ else if (game.actions === 1)
+ view.prompt = `You have one action.`;
+ else
+ view.prompt = `You have no more actions.`;
+
+ // Pass / End turn
+ if (game.actions > 0) {
+ gen_action('pass');
+ } else {
+ gen_action('next');
+ }
+
+ // 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))
+ gen_action('purchase', 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
+ for (let i = 0; i < player.court.length; ++i) {
+ let c = player.court[i];
+ let card = cards[c];
+ if ((game.actions > 0 || is_favored_suit(c)) && !game.used_cards.includes(c)) {
+ if (card.tax)
+ gen_action('tax', c);
+ if (card.gift && active_gifts() < 3 && player.coins >= gift_cost())
+ gen_action('gift', c);
+ if (card.build && player.coins >= 2 && active_rules_any_region())
+ gen_action('build', c);
+ if (card.move) // TODO: check if any moves are possible
+ gen_action('move', c);
+ if (card.betray && player.coins >= 2 && active_has_spy())
+ gen_action('betray', c);
+ if (card.battle)
+ gen_action('battle', c);
+ }
+ }
+ },
+
+ purchase(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]);
+ }
+ }
+
+ logbr();
+
+ if (cost > 0) {
+ if (cost > 1)
+ log(`Paid ${cost} rupees.`);
+ else
+ log(`Paid ${cost} rupee.`);
+ player.coins -= cost;
+ }
+
+ if (game.market_coins[row][col] > 0) {
+ if (game.market_coins[row][col] > 1)
+ log(`Took ${game.market_coins[row][col]} rupees.`);
+ else
+ log(`Took ${game.market_coins[row][col]} rupee.`);
+ player.coins += game.market_coins[row][col];
+ }
+
+ game.market_coins[row][col] = 0;
+ game.market_cards[row][col] = 0;
+
+ if (is_dominance_check(c)) {
+ log(`Purchased Dominance Check.`);
+ do_dominance_check();
+ resume_actions();
+ } else if (is_event_card(c)) {
+ log(`Purchased event ${cards[c].if_purchased}.`);
+ log(`TODO: ${cards[c].if_purchased}`);
+ resume_actions();
+ } else {
+ log(`Purchased ${cards[c].name}.`);
+ player.hand.push(c);
+ resume_actions();
+ }
+ },
+
+ tax(c) { do_card_action_1(c, "Tax", 0); },
+ gift(c) { do_card_action_1(c, "Gift", 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); },
+ play_left(c) { do_play_1(c, 0); },
+ play_right(c) { do_play_1(c, 1); },
+
+ pass() {
+ log(`Passed.`);
+ goto_cleanup_court();
+ },
+ next() {
+ goto_cleanup_court();
+ },
+}
+
+// PLAY CARD
+
+function do_play_1(c, side) {
+ push_undo();
+ game.card = c;
+ game.where = side;
+ if (!active_has_charismatic_courtiers()) {
+ let ruler = ruler_of_region(cards[c].region);
+ if (ruler >= 0 && ruler !== game.active) {
+ game.state = 'bribe';
+ game.count = count_player_cylinders(ruler, cards[c].region);
+ game.reserve = 0;
+ return;
+ }
+ }
+ do_play_2();
+}
+
+function do_play_2() {
+ let c = game.card;
+ let side = game.where;
+ game.actions --;
+ logbr();
+ log(`Played ${cards[c].name} (${region_names[cards[c].region]}).`);
+ 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();
+ log(`${player_names[game.active]} 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();
+ log(`${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();
+ log(`${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();
+ log(`${player_names[game.active]} spy to ${cards[c].name}.`);
+ 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) {
+ log(`Leveraged 2 rupees.`);
+ player.coins += 2;
+ }
+ goto_play_climate();
+}
+
+function goto_play_climate() {
+ // TODO: manual click?
+ let card = cards[game.card];
+ if (card.climate) {
+ log(`Favored suit to ${card.climate}.`);
+ game.favored = card.climate;
+ }
+ end_action();
+}
+
+// CARD-BASED ACTION (COMMON)
+
+const card_action_table = {
+ Tax() {
+ game.state = 'tax';
+ },
+
+ Gift() {
+ pay_action_cost(gift_cost());
+ game.selected = select_available_cylinder();
+ if (game.selected < 0)
+ game.state = 'gift';
+ else
+ do_gift();
+ },
+
+ Build() {
+ game.count = Math.min(3, Math.floor(player.coins / 2));
+ game.selected = select_available_block();
+ game.state = 'build';
+ },
+
+ Move() {
+ game.selected = -1;
+ game.selected = -1;
+ game.state = 'move';
+ },
+
+ Betray() {
+ pay_action_cost(2);
+ 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()) {
+ let who = player_with_most_spies(c);
+ if (who >= 0 && who !== game.active) {
+ game.state = 'bribe';
+ game.count = count_player_cylinders(who, c);
+ game.reserve = reserve;
+ return;
+ }
+ }
+ do_card_action_2();
+}
+
+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(`Used ${cards[c].name} to ${what}.`);
+ 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();
+ log(`Taxed ${player_names[p]} player.`);
+ game.players[p].coins --;
+ player.coins ++;
+ if (--game.count === 0)
+ end_action();
+}
+
+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);
+ break;
+ }
+ }
+
+ gen_action('pass');
+ },
+ pass() {
+ push_undo();
+ end_action();
+ },
+ card(c) {
+ push_undo();
+ let [row, col] = find_card_in_market(c);
+ log(`Taxed ${cards[c].name}.`);
+ 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() {
+ view.prompt = `Select cylinder to use as Gift.`;
+ gen_select_cylinder();
+ },
+ piece(x) {
+ push_undo();
+ game.selected = x;
+ do_gift();
+ },
+}
+
+function do_gift() {
+ game.pieces[game.selected] = Gift;
+ game.used_pieces.push(game.selected);
+ end_action();
+}
+
+// CARD-BASED ACTION: BUILD
+
+states.build = {
+ prompt() {
+ view.prompt = `Build up to ${game.count} armies and/or roads.`;
+ gen_action('next');
+ if (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();
+ pay_action_cost(2);
+ if (s <= last_region)
+ log(`${player.loyalty} army to ${region_names[s]}.`);
+ else
+ log(`${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)
+ 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)
+ log(`${player.loyalty} army to ${region_names[s]}.`);
+ else
+ log(`${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';
+ 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;
+}
+
+states.move = {
+ prompt() {
+ if (game.selected >= 36) {
+ let c = game.pieces[game.selected];
+ let here = court_position_from_card(c);
+ view.prompt = `Move spy from ${cards[c].name}.`;
+
+ 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]);
+ }
+ }
+
+ } else if (game.selected >= 0) {
+ let here = game.pieces[game.selected];
+ view.prompt = `Move ${player.loyalty} army 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 (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();
+ }
+ },
+ piece(x) {
+ push_undo();
+ game.selected = x;
+ },
+ card(c) {
+ push_undo();
+ let old = game.pieces[game.selected];
+ log(`${player_names[game.active]} spy from ${cards[old].name} to ${cards[c].name}.`);
+ game.pieces[game.selected] = c;
+ game.selected = -1;
+ if (--game.count === 0)
+ end_action();
+ },
+ space(s) {
+ push_undo();
+ let old = game.pieces[game.selected];
+ log(`${player.loyalty} army from ${region_names[old]} to ${region_names[s]}.`);
+ game.pieces[game.selected] = s;
+ 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];
+ 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 = {
+ prompt() {
+ view.prompt = `You may accept ${cards[game.card].name} as ${a_or_an(cards[game.card].prize)} prize.`;
+ gen_action('accept');
+ gen_action('refuse');
+ },
+ accept() {
+ log(`${player_names[game.active]} took ${cards[game.card].name} as ${a_or_an(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) {
+ 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) {
+ return count_player_armies_in_region(game.active, where);
+}
+
+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) {
+ 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";
+}
+
+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 ${cards[where].name}.`;
+ 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 = 0; i < 10; ++i)
+ if (game.pieces[i] === where)
+ gen_action('piece', i);
+ }
+ }
+ }
+ gen_action('next');
+ }
+ },
+ card(where) {
+ push_undo();
+ log(`${player_names[game.active]} starts battle on ${cards[where].name}.`);
+ game.where = where;
+ game.count = Math.min(game.count, count_active_spies_on_card(where));
+ },
+ space(where) {
+ push_undo();
+ log(`${player_names[game.active]} starts battle in ${region_names[where]}.`);
+ game.where = where;
+ game.count = Math.min(game.count, count_active_armies_in_region(where));
+ },
+ piece(x) {
+ push_undo();
+ let where = game.pieces[x];
+ game.pieces[x] = 0;
+ if (x < 36) {
+ if (game.where >= first_region && game.where <= last_region)
+ log(`Removed ${piece_owner(x)} army from ${region_names[where]}.`);
+ else
+ log(`Removed ${piece_owner(x)} road from ${border_names[where]}.`);
+ } else {
+ if (where <= 100) {
+ log(`Removed ${piece_owner(x)} spy from ${cards[where].name}.`);
+ } else {
+ log(`Removed ${piece_owner(x)} tribe from ${region_names[where]}.`);
+ let p = Math.floor((x - 36) / 10);
+ 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(`${player_names[game.active]} blackmail spy to ${cards[c].name}.`);
+ 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();
+ },
+}
+
+// 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() {
+ 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();
+ log(`${player_names[game.active]} discarded\n${cards[c].name} from their court.`);
+ discard_court_card(c);
+ },
+ next() {
+ push_undo();
+ goto_cleanup_hand();
+ }
+}
+
+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(`${player_names[game.active]} discarded\n${cards[c].name} from their hand.`);
+ remove_from_array(player.hand, c);
+ },
+ next() {
+ push_undo();
+ goto_discard_events();
+ }
+}
+
+function do_discard_event(row, c) {
+ game.market_cards[row][0] = 0;
+ logbr();
+ log(`Discarded event\n${cards[c].if_discarded}.`);
+ logbr();
+ if (is_dominance_check(c))
+ do_dominance_check();
+ else
+ log(`TODO: ${cards[c].if_discarded}`);
+ goto_discard_events();
+}
+
+function goto_discard_events() {
+ if (is_event_card(game.market_cards[0][0])) {
+ do_discard_event(0, game.market_cards[0][0]);
+ } else if (is_event_card(game.market_cards[1][0])) {
+ do_discard_event(1, game.market_cards[1][0]);
+ } else {
+ goto_refill_market();
+ }
+}
+
+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;
+ }
+ }
+
+ // 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 (instability > 0 && is_dominance_check(c)) {
+ log(`Instability!`);
+ discard_instability_cards();
+ do_dominance_check();
+ goto_refill_market();
+ }
+ }
+ }
+ }
+ }
+
+ goto_next_player();
+}
+
+// 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);
+ for (let i = x; i < x + 10; ++i)
+ if (game.pieces[i] === Gift)
+ ++n;
+ 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 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 do_dominance_check() {
+ 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 ++;
+ }
+
+ if (n_afghan >= n_british+4 && n_afghan >= n_russian+4)
+ success = Afghan;
+ else if (n_british >= n_afghan+4 && n_british >= n_russian+4)
+ success = British;
+ else if (n_russian >= n_british+4 && n_russian >= n_afghan+4)
+ success = Russian;
+
+ let final = is_final_dominance_check();
+ if (final)
+ log(`Final Dominance Check.`);
+
+ let score = new Array(game.players.length).fill(0);
+ if (success) {
+ logbr();
+ log(`Dominant ${success} Coalition.`);
+ for (let p = 0; p < game.players.length; ++p) {
+ if (game.players[p].loyalty === success) {
+ score[p] = count_influence_points(p);
+ log(`${player_names[p]} had ${score[p]} influence.`);
+ }
+ }
+ 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(`Unsuccessful Check.`);
+ for (let p = 0; p < game.players.length; ++p) {
+ score[p] = count_cylinders_in_play(p);
+ log(`${player_names[p]} had ${score[p]} cylinders.`);
+ }
+ if (final)
+ assign_vp([6, 2], score, score.slice());
+ else
+ assign_vp([3, 1], score, score.slice());
+ }
+
+ // Clear the board.
+ for (let i = 0; i < 36; ++i)
+ game.pieces[i] = 0;
+
+ // Prince Akbar Khan
+ if (any_player_has_insurrection()) {
+ log(`Insurrection placed two Afghan armies in Kabul.`);
+ game.pieces[0] = Kabul;
+ game.pieces[1] = Kabul;
+ }
+
+ // Check instant victory
+ let vps = game.players.map((pp,i) => pp.vp).sort((a,b)=>b-a);
+ if (vps[0] >= vps[1] + 4)
+ goto_game_over();
+
+ if (final)
+ goto_game_over();
+}
+
+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 goto_game_over() {
+ 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);
+ game.state = 'game_over';
+}
+
+// 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,
+
+ active: 0,
+ state: "none",
+ used_cards: [],
+ used_pieces: [],
+ count: 0,
+ reserve: 0,
+ 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: [],
+ }
+ }
+
+ prepare_deck();
+
+ for (let row = 0; row < 2; ++row)
+ for (let col = 0; col < 6; ++col)
+ game.market_cards[row][col] = game.deck.pop();
+
+ // Starting loyalty, starting with a random player.
+ game.state = 'loyalty';
+ game.active = random(player_count);
+ log(`Random start player is ${player_names[game.active]}.`);
+
+ 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);
+ goto_game_over();
+ return save_game();
+}
+
+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,
+ };
+
+ 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;
+}