summaryrefslogtreecommitdiff
path: root/rules.js
diff options
context:
space:
mode:
Diffstat (limited to 'rules.js')
-rw-r--r--rules.js531
1 files changed, 460 insertions, 71 deletions
diff --git a/rules.js b/rules.js
index 880dff1..ed56611 100644
--- a/rules.js
+++ b/rules.js
@@ -4,13 +4,16 @@
TODO
[ ] killed leader stash for buy/trash phase
-[ ] killed ardashur+shapur: two support bumps
-[ ] killed gives credits for military + senate
[ ] display of general+castra stacked with militia+castra
+[ ] todo: battle twice with militia (remove mbattled?)
[ ] emperor
[ ] expansion cards
+Frumentarii
+Demagogue
+Spiculum
+
*/
var game
@@ -401,6 +404,16 @@ const PLAY_CARD_EVENT = {
"Damnatio Memoriae": play_damnatio_memoriae,
"Damnatio Memoriae (exp)": play_damnatio_memoriae_exp,
"Pretender": play_pretender,
+
+ "Cavalry": play_cavalry,
+ "Princeps Senatus": play_princeps_senatus,
+ "Ambitus": play_ambitus,
+ "Force March": play_force_march,
+ "Frumentarii": play_frumentarii,
+ "Mobile Vulgus": play_mobile_vulgus,
+ "Spiculum": play_spiculum,
+ "Triumph": play_triumph,
+ "Demagogue": play_demagogue,
}
const CAN_PLAY_CARD_EVENT = {
@@ -414,6 +427,16 @@ const CAN_PLAY_CARD_EVENT = {
"Damnatio Memoriae": null,
"Damnatio Memoriae (exp)": null,
"Pretender": can_play_pretender,
+
+ "Cavalry": null,
+ "Princeps Senatus": can_play_princeps_senatus,
+ "Ambitus": null,
+ "Force March": can_play_force_march,
+ "Frumentarii": can_play_frumentarii,
+ "Mobile Vulgus": can_play_mobile_vulgus,
+ "Spiculum": null,
+ "Triumph": null,
+ "Demagogue": can_play_demagogue,
}
function can_play_card_event(c) {
@@ -561,6 +584,9 @@ function has_general_battled(id) { return game.battled & (1 << id) }
function set_general_battled(id) { game.battled |= (1 << id) }
function clear_general_battled(id) { game.battled &= ~(1 << id) }
+function has_general_force_marched(id) { return game.force_march & (1 << id) }
+function set_general_force_marched(id) { game.force_march |= (1 << id) }
+
function has_militia_battled(province) { return game.mbattled & (1 << province) }
function set_militia_battled(province) { game.mbattled |= (1 << province) }
function clear_militia_battled(province) { game.mbattled &= ~(1 << province) }
@@ -1116,6 +1142,13 @@ function has_card_event(event) {
return false
}
+function used_card_event(event) {
+ for (let c = event[0]; c <= event[1]; ++c)
+ if (set_has(game.used, c))
+ return true
+ return false
+}
+
// === SETUP ===
states.setup_province = {
@@ -1179,6 +1212,7 @@ function goto_start_turn() {
game.killed = 0
game.battled = 0
+ game.force_march = 0
game.mbattled = 0
game.placed = 0
@@ -1993,6 +2027,7 @@ states.take_actions = {
push_undo()
if (game.selected_governor >= 0) {
spend_senate(1)
+ game.ambitus = 0
game.count = 1
game.where = where
game.state = "place_governor"
@@ -2148,6 +2183,16 @@ function place_governor(where, new_governor) {
update_neutral_italia()
}
+function count_units_in_capital(where) {
+ let n = 0
+ let army = get_capital_general(where)
+ if (army >= 0)
+ n += count_units_in_army(army)
+ if (has_militia(where))
+ n += 1
+ return n
+}
+
function calc_needed_votes(where, pg) {
let n = get_support(where) * 2 // base number of votes
let old_governor = get_province_governor(where)
@@ -2166,6 +2211,8 @@ function calc_needed_votes(where, pg) {
if (has_militia(where))
n += 1
}
+ // Ambitus adds guaranteed votes.
+ n -= game.ambitus
return Math.max(1, n)
}
@@ -2173,19 +2220,28 @@ states.place_governor = {
inactive: "Place Governor",
prompt() {
let need = calc_needed_votes(game.where, false)
- let votes = game.count
+ let dice = game.count
if (game.where === ITALIA)
- votes += count_own_basilicas()
+ dice += count_own_basilicas()
view.color = SENATE
view.selected_region = game.where
view.selected_governor = game.selected_governor
- prompt(`Place Governor: ${game.sip} senate. Rolling ${votes} dice. ${need} votes needed.`)
+ prompt(`Place Governor: ${game.sip} senate. Rolling ${dice} dice. ${need} votes needed.`)
+ if (game.ambitus < game.count && has_card_event(CARD_P2X)) {
+ view.prompt += " Ambitus?"
+ gen_card_event(CARD_P2X)
+ }
if (game.sip >= 1)
view.actions.spend_senate = 1
else
view.actions.spend_senate = 0
view.actions.roll = 1
},
+ card(c) {
+ push_undo()
+ set_add(game.used, c)
+ play_card_event(c)
+ },
spend_senate() {
push_undo()
spend_senate(1)
@@ -2201,19 +2257,28 @@ states.praetorian_guard = {
inactive: "Praetorian Guard",
prompt() {
let need = calc_needed_votes(game.where, true)
- let votes = game.count
+ let dice = game.count
if (game.where === ITALIA)
- votes += count_own_basilicas()
+ dice += count_own_basilicas()
view.color = MILITARY
view.selected_region = game.where
view.selected_governor = game.selected_governor
- prompt(`Praetorian Guard: ${game.mip} Military. Rolling ${votes} dice. ${need} votes needed.`)
+ prompt(`Praetorian Guard: ${game.mip} military. Rolling ${dice} dice. ${need} votes needed.`)
+ if (game.ambitus < game.count && has_card_event(CARD_P2X)) {
+ view.prompt += " Ambitus?"
+ gen_card_event(CARD_P2X)
+ }
if (game.mip >= 1)
view.actions.spend_military = 1
else
view.actions.spend_military = 0
view.actions.roll = 1
},
+ card(c) {
+ push_undo()
+ set_add(game.used, c)
+ play_card_event(c)
+ },
spend_military() {
push_undo()
spend_military(1)
@@ -2296,6 +2361,7 @@ function play_praetorian_guard_auto() {
}
}
spend_military(1)
+ game.ambitus = 0
game.count = 1
game.where = ITALIA
game.state = "praetorian_guard"
@@ -2737,6 +2803,7 @@ function can_play_foederati() {
}
function play_foederati() {
+ // TODO: auto-select general on map?
game.state = "foederati"
}
@@ -2917,12 +2984,253 @@ states.pretender_breakaway = {
},
}
+
+// CARD: Princeps Senatus
+
+function can_play_princeps_senatus() {
+ return !used_card_event(CARD_S2X)
+}
+
+function play_princeps_senatus() {
+ log("Princeps Senatus.")
+}
+
+// CARD: Ambitus
+
+function play_ambitus() {
+ log("Ambitus.")
+ game.ambitus += 1
+}
+
+// CARD: Force March
+
+function can_play_force_march() {
+ if (game.mip >= 1) {
+ for (let i = 0; i < 6; ++i) {
+ let id = game.current * 6 + i
+ if (is_region(get_general_location(id)))
+ if (has_general_battled(id) && !has_general_force_marched(id))
+ return true
+ }
+ }
+ return false
+}
+
+function play_force_march() {
+ // TODO: auto-select general on map?
+ game.state = "force_march_who"
+}
+
+states.force_march_who = {
+ prompt() {
+ prompt("Force March: Choose an army you command...")
+ for (let i = 0; i < 6; ++i) {
+ let id = game.current * 6 + i
+ let where = get_general_location(id)
+ if (is_region(where) && has_general_battled(id) && !has_general_force_marched(id))
+ gen_action_general(id)
+ }
+ // TODO: Militia + Force March ?
+ },
+ general(id) {
+ log("Force March.")
+ set_general_force_marched(id)
+ game.selected_governor = -1
+ game.selected_general = id
+ game.selected_militia = -1
+ game.state = "force_march"
+ },
+}
+
+states.force_march = {
+ prompt() {
+ prompt("Force March: Move Army or Initiate Battle.")
+
+ let where = UNAVAILABLE
+ view.color = MILITARY
+ if (game.selected_general > 0) {
+ view.selected_general = game.selected_general
+ where = get_general_location(game.selected_general)
+ } else {
+ view.selected_militia = game.selected_militia
+ where = game.selected_militia
+ }
+
+ // Initiate Battle
+ gen_initiate_battle(where)
+
+ // Move Army
+ if (game.selected_general > 0) {
+ for (let to of ADJACENT[where]) {
+ if (!is_sea(to)) {
+ gen_action_region(to)
+ if (can_enter_capital(to))
+ gen_action_capital(to)
+ }
+ }
+
+ // Free Action: Enter/Leave Capital
+ if (is_province(where)) {
+ if (is_general_inside_capital(game.selected_general)) {
+ view.actions.leave = 1
+ } else if (can_enter_capital(where)) {
+ view.actions.enter = 1
+ gen_action_capital(where)
+ }
+ }
+ }
+ },
+
+ general(id) {
+ push_undo()
+ goto_battle_vs_general(get_general_location(game.selected_general), game.selected_general, id)
+ },
+
+ militia(where) {
+ push_undo()
+ goto_battle_vs_militia(where, game.selected_general)
+ },
+
+ barbarian(id) {
+ push_undo()
+ goto_battle_vs_barbarian(get_selected_region(), game.selected_general, id)
+ },
+
+ rival_emperor(id) {
+ push_undo()
+ goto_battle_vs_rival_emperor(get_selected_region(), game.selected_general, id)
+ },
+
+ region(where) {
+ push_undo()
+ move_army_to(game.selected_general, where)
+ },
+
+ capital(where) {
+ push_undo()
+ if (get_general_location(game.selected_general) !== where)
+ move_army_to(game.selected_general, where)
+ enter_capital()
+ },
+
+ enter() {
+ push_undo()
+ enter_capital()
+ game.state = "take_actions"
+ },
+
+ leave() {
+ push_undo()
+ set_general_outside_capital(game.selected_general)
+ remove_general_castra(game.selected_general)
+ game.state = "take_actions"
+ },
+}
+
+// CARD: Frumentarii
+
+function can_play_frumentarii() {
+ return !used_card_event(CARD_S3X)
+}
+
+function play_frumentarii() {
+ log("Frumentarii.")
+ game.frumentarii |= (1 << game.current)
+}
+
+// CARD: Mobile Vulgus
+
+function can_play_mobile_vulgus() {
+ for (let where = 0; where < 12; ++where) {
+ if (is_enemy_province(where)) {
+ let n = get_support(where) * 2 + count_units_in_capital(where)
+ if (game.pip >= n)
+ return true
+ }
+ }
+ return false
+}
+
+function play_mobile_vulgus() {
+ game.state = "mobile_vulgus_where"
+}
+
+states.mobile_vulgus_where = {
+ prompt() {
+ prompt("Mobile Vulgus: Choose a province...")
+ view.color = POPULACE
+ for (let where = 0; where < 12; ++where) {
+ if (is_enemy_province(where)) {
+ let n = get_support(where) * 2 + count_units_in_capital(where)
+ if (game.pip >= n)
+ gen_action_region(where)
+ }
+ }
+ },
+ region(where) {
+ log("Mobile Vulgus in %" + where + ".")
+ game.where = where
+ game.state = "mobile_vulgus"
+ },
+}
+
+states.mobile_vulgus = {
+ prompt() {
+ prompt("Mobile Vulgus: " + game.pip + " populace. Reduce support in " + REGION_NAME[game.where] + ".")
+ let n = get_support(game.where) * 2 + count_units_in_capital(game.where)
+ if (game.pip >= n)
+ gen_action_support(game.where, get_support(game.where) - 1)
+ view.actions.done = 1
+ },
+ support() {
+ push_undo()
+ log("Reduce support level in %" + game.where + ".")
+ let n = get_support(game.where) * 2 + count_units_in_capital(game.where)
+ spend_populace(n)
+ reduce_support(game.where)
+ if (is_neutral_province(game.where))
+ game.state = "take_actions"
+ },
+ done() {
+ push_undo()
+ game.state = "take_actions"
+ },
+}
+
+// CARD: Triumph
+
+function play_triumph() {
+ log("Triumph.")
+ // TODO
+}
+
+// CARD: Demagogue
+
+function can_play_demagogue() {
+ return !used_card_event(CARD_P4X)
+}
+
+function play_demagogue() {
+ log("Demagogue.")
+ // TODO
+}
+
// === COMBAT ===
function play_flanking_maneuver() {
game.combat.flanking = 1
}
+function play_cavalry() {
+ game.combat.cavalry = 1
+}
+
+function play_spiculum() {
+ game.combat.spiculum = 1
+ game.state = "spiculum"
+ // TODO
+}
+
function goto_battle_vs_general(where, attacker, target) {
goto_battle("general", where, attacker, target)
}
@@ -3004,6 +3312,16 @@ states.initiate_battle = {
gen_card_event(CARD_M3)
}
+ if (!game.combat.cavalry && has_card_event(CARD_M2X)) {
+ view.prompt += " Cavalry?"
+ gen_card_event(CARD_M2X)
+ }
+
+ if (!game.combat.spiculum && has_card_event(CARD_M4X)) {
+ view.prompt += " Spiculum?"
+ gen_card_event(CARD_M4X)
+ }
+
view.actions.roll = 1
},
card(c) {
@@ -3419,6 +3737,8 @@ states.combat_victory = {
prompt("Combat: There is no winner.")
else if (de || game.combat.dtaken > game.combat.ataken)
prompt("Combat: You win the battle!")
+ else if (game.combat.dtaken === game.combat.ataken && game.combat.cavalry)
+ prompt("Combat: You win the battle!")
else
prompt("Combat: Defenders win the battle!")
view.actions.done = 1
@@ -3552,8 +3872,12 @@ function goto_free_increase_support_level() {
}
}
- game.combat = null
- game.state = "take_actions"
+ if (game.combat.target === "barbarians" && has_card_event(CARD_S4X)) {
+ game.state = "triumph"
+ } else {
+ game.combat = null
+ game.state = "take_actions"
+ }
}
states.free_increase_support_level = {
@@ -3581,6 +3905,28 @@ states.free_increase_support_level = {
},
}
+states.triumph = {
+ prompt() {
+ prompt("Combat: You may play Triumph.")
+ gen_card_event(CARD_S4X)
+ view.actions.pass = 1
+ },
+ card(c) {
+ push_undo()
+ set_add(game.used, c)
+
+ award_legacy(game.current, "Triumph", game.combat.dtaken)
+
+ game.combat = null
+ game.state = "take_actions"
+ },
+ pass() {
+ push_undo()
+ game.combat = null
+ game.state = "take_actions"
+ },
+}
+
// === SUPPORT CHECK ===
function goto_support_check() {
@@ -3819,13 +4165,40 @@ function goto_buy_trash_cards() {
log("Played no cards.")
log_br()
+ //log_h3("Buy/Trash Cards")
+
+ game.pp = count_political_points()
+
+ log_h3(game.pp + " Political Points")
+ if (used_card_event(CARD_S2X)) {
+ let n = Math.min(2, game.sip)
+ if (n > 0) {
+ logi("+" + n + " for Princeps Senatus")
+ game.pp += n
+ }
+ }
+
+ game.mip = game.sip = game.pip = 0
+ for (let i = 0; i < 3; ++i) {
+ if (game.killed & (1 << i)) {
+ logi("+2 senate credits")
+ game.sip += 2
+ }
+ }
+ for (let i = 3; i < 6; ++i) {
+ if (game.killed & (1 << i)) {
+ logi("+2 military credits")
+ game.mip += 2
+ }
+ }
+
let discard = current_discard()
for (let c of game.played)
set_add(discard, c)
-
game.played.length = 0
+ game.used.length = 0
+
game.count = 0
- game.pp = count_political_points()
if (game.end)
goto_end_of_turn()
@@ -3862,47 +4235,26 @@ function find_market_with_card(c) {
return null
}
-function has_military_card_bonus() {
- let military_bonus = 0
- if (game.killed & CNIVA_BONUS)
- military_bonus += 2
- if (game.killed & ARDASHIR_BONUS)
- military_bonus += 2
- if (game.killed & SHAPUR_BONUS)
- military_bonus += 2
- return military_bonus
+function spend_military_credit(cost) {
+ let credit = Math.min(cost, game.mip)
+ game.mip -= credit
+ return cost - credit
}
-function has_senate_card_bonus() {
- let senate_bonus = 0
- if (game.killed & (1 << POSTUMUS))
- senate_bonus += 2
- if (game.killed & (1 << PRIEST_KING))
- senate_bonus += 2
- if (game.killed & (1 << ZENOBIA))
- senate_bonus += 2
- return senate_bonus
-}
-
-function spend_military_card_bonus() {
- game.killed &= ~(CNIVA_BONUS | ARDASHIR_BONUS | SHAPUR_BONUS)
-}
-
-function spend_senate_card_bonus() {
- game.killed &= ~((1 << POSTUMUS) | (1 << PRIEST_KING) | (1 << ZENOBIA))
+function spend_senate_credit(cost) {
+ let credit = Math.min(cost, game.sip)
+ game.mip -= credit
+ return cost - credit
}
states.buy_trash = {
inactive: "Buy/Trash Cards",
prompt() {
- let military_bonus = has_military_card_bonus()
- let senate_bonus = has_senate_card_bonus()
-
prompt("Buy/Trash Cards: " + game.pp + " political points.")
- if (military_bonus)
- view.prompt += " First Military card is 2 cheaper."
- if (senate_bonus)
- view.prompt += " First Senate card is 2 cheaper."
+ if (game.mip > 0)
+ view.prompt += " " + game.mip + " military credits."
+ if (game.sip > 0)
+ view.prompt += " " + game.mip + " senate credits."
let nprov = count_own_provinces()
if (game.pp >= 3) {
@@ -3916,10 +4268,10 @@ states.buy_trash = {
if (cost > nprov)
cost *= 2
cost += game.count
- if (military_bonus && card_influence(c) === MILITARY)
- cost -= 2
- if (senate_bonus && card_influence(c) === SENATE)
- cost -= 2
+ if (card_influence(c) === MILITARY)
+ cost = Math.max(0, cost - game.mip)
+ if (card_influence(c) === SENATE)
+ cost = Math.max(0, cost - game.sip)
if (game.pp >= cost)
gen_action_card(c)
}
@@ -3933,9 +4285,6 @@ states.buy_trash = {
set_delete(current_discard(), c)
game.pp -= 3
} else {
- let military_bonus = has_military_card_bonus()
- let senate_bonus = has_senate_card_bonus()
-
log("Buy " + card_name(c) + ".")
set_add(current_discard(), c)
set_delete(find_market_with_card(c), c)
@@ -3945,14 +4294,10 @@ states.buy_trash = {
cost *= 2
cost += game.count
- if (military_bonus && card_influence(c) === MILITARY) {
- spend_military_card_bonus()
- cost -= 2
- }
- if (senate_bonus && card_influence(c) === SENATE) {
- spend_senate_card_bonus()
- cost -= 2
- }
+ if (card_influence(c) === MILITARY)
+ cost = spend_military_credit(cost)
+ if (card_influence(c) === SENATE)
+ cost = spend_senate_credit(cost)
game.pp -= cost
game.count += 1
@@ -4243,6 +4588,10 @@ exports.setup = function (seed, scenario, options) {
killed: 0,
combat: null,
+ ambitus: 0,
+ frumentarii: 0,
+ force_march: 0,
+
provinces: new Array(3 * player_count).fill(1),
governors: new Array(6 * player_count).fill(UNAVAILABLE),
generals: new Array(6 * player_count).fill(UNAVAILABLE),
@@ -4264,17 +4613,57 @@ exports.setup = function (seed, scenario, options) {
game.events = setup_events()
- game.market = [
- setup_market_pile(CARD_M2),
- setup_market_pile(CARD_S2),
- setup_market_pile(CARD_P2),
- setup_market_pile(CARD_M3),
- setup_market_pile(CARD_S3),
- setup_market_pile(CARD_P3),
- setup_market_pile(CARD_M4),
- setup_market_pile(CARD_S4),
- setup_market_pile(CARD_P4),
- ]
+ switch (scenario) {
+ default:
+ case "Standard":
+ game.market = [
+ setup_market_pile(CARD_M2),
+ setup_market_pile(CARD_S2),
+ setup_market_pile(CARD_P2),
+ setup_market_pile(CARD_M3),
+ setup_market_pile(CARD_S3),
+ setup_market_pile(CARD_P3),
+ setup_market_pile(CARD_M4),
+ setup_market_pile(CARD_S4),
+ setup_market_pile(CARD_P4),
+ ]
+ break
+ case "Expansion":
+ game.market = [
+ setup_market_pile(CARD_M2),
+ setup_market_pile(CARD_S2),
+ setup_market_pile(CARD_P2),
+ setup_market_pile(CARD_M2X),
+ setup_market_pile(CARD_S2X),
+ setup_market_pile(CARD_P2X),
+ setup_market_pile(CARD_M3),
+ setup_market_pile(CARD_S3),
+ setup_market_pile(CARD_P3),
+ setup_market_pile(CARD_M3X),
+ setup_market_pile(CARD_S3X),
+ setup_market_pile(CARD_P3X),
+ setup_market_pile(CARD_M4),
+ setup_market_pile(CARD_S4B),
+ setup_market_pile(CARD_P4),
+ setup_market_pile(CARD_M4X),
+ setup_market_pile(CARD_S4X),
+ setup_market_pile(CARD_P4X),
+ ]
+ break
+ case "Random":
+ game.market = [
+ setup_market_pile(random(2) ? CARD_M2 : CARD_M2X),
+ setup_market_pile(random(2) ? CARD_S2 : CARD_S2X),
+ setup_market_pile(random(2) ? CARD_P2 : CARD_P2X),
+ setup_market_pile(random(2) ? CARD_M3 : CARD_M3X),
+ setup_market_pile(random(2) ? CARD_S3 : CARD_S3X),
+ setup_market_pile(random(2) ? CARD_P3 : CARD_P3X),
+ setup_market_pile(random(2) ? CARD_M4 : CARD_M4X),
+ setup_market_pile(random(2) ? CARD_S4B : CARD_S4X),
+ setup_market_pile(random(2) ? CARD_P4 : CARD_P4X),
+ ]
+ break
+ }
set_rival_emperor_location(POSTUMUS, AVAILABLE)
set_rival_emperor_location(PRIEST_KING, AVAILABLE)