diff options
-rw-r--r-- | play.js | 2 | ||||
-rw-r--r-- | rules.js | 1320 |
2 files changed, 679 insertions, 643 deletions
@@ -548,6 +548,8 @@ function on_update() { action_button("britain_first", "Britain") action_button("america_first", "America") action_button("surrender", "Surrender") + action_button("stop", "Stop") + action_button("next", "Next") action_button("done", "Done") action_button("pass", "Pass") @@ -1,8 +1,8 @@ "use strict" -// TODO: capture washington is messy -// TODO: campaign messed up who is who after battle -// TODO: retreat with 0 CU after battle +// TODO: capture washington +// TODO: campaign messed up who is who after battle ? +// TODO: retreat with 0 CU after battle ? /* washington's capture - winning general with no CU on enemy PC (no undo anyway) @@ -766,7 +766,6 @@ function capture_washington() { game.french_alliance = 0 } - // set flag to handle washington's capture! game.captured_washington = 1 } @@ -1378,6 +1377,7 @@ function end_strategy_card() { states.end_strategy_card = { prompt() { view.prompt = "Done." + view.actions.next = 1 }, next() { clear_undo() @@ -1430,6 +1430,7 @@ states.discard_event_pc_action = { gen_american_discard_event_pc_action() }, space(s) { + push_undo() if (game.active === P_BRITAIN) { if (has_no_pc(s)) place_british_pc(s) @@ -1448,6 +1449,7 @@ states.discard_event_pc_action = { end_strategy_card() }, pass() { + push_undo() end_strategy_card() }, } @@ -1513,15 +1515,21 @@ states.ops_pc = { else place_american_pc(s) } - --game.count + if (--game.count === 0) + end_ops_pc() }, pass() { - if (game.active === P_BRITAIN) - gen_british_pc_ops_end() - end_strategy_card() + push_undo() + end_ops_pc() }, } +function end_ops_pc() { + if (game.active === P_BRITAIN) + gen_british_pc_ops_end() + end_strategy_card() +} + function gen_british_pc_ops_start() { game.british_pc_space_list = [] for (let space of all_spaces) { @@ -1579,6 +1587,10 @@ function goto_ops_reinforcements(c) { } } +// TODO: british reinforcements: pick general +// TODO: british reinforcements: pick number of CU +// TODO: british reinforcements: pick destination + states.ops_british_reinforcements_who = { prompt() { view.prompt = "Reinforcements: choose an available general or pass to bring only CU." @@ -1620,8 +1632,9 @@ states.ops_british_reinforcements_where = { }, space(space) { place_british_reinforcements(game.who, game.count, space) - end_strategy_card() delete game.who + // capture george washington happens in end_strategy_card + end_strategy_card() }, } @@ -1655,8 +1668,8 @@ states.ops_american_reinforcements_where = { place_french_reinforcements(game.who, space) else place_american_reinforcements(game.who, game.count, space) - end_strategy_card() delete game.who + end_strategy_card() }, } @@ -1707,6 +1720,13 @@ function gen_american_reinforcements_where(general) { /* PLAY OPS CARD TO MOVE A GENERAL */ +// TODO: new procedure: +// 1) pick general +// 2) take CU +// 3) drop CU / take CU / move to space +// 4) confirm move if intercept/battle +// 5) goto battle/intercept/3 + function goto_ops_general(c) { play_card(c, "to activate a general") if (game.active === P_BRITAIN) { @@ -1896,34 +1916,33 @@ function goto_ops_general_move(g, marblehead) { states.ops_general_move = { prompt() { - view.prompt = "Move " + game.move.who + " with " + view.prompt = "Move " + data.generals[game.move.who].name + " with " if (game.move.carry_british > 0) { - view.prompt += game.move.carry_british + " British CU." + view.prompt += game.move.carry_british + " CU." } else if (game.move.carry_french + game.move.carry_american > 0) { if (game.move.carry_french > 0) { if (game.move.carry_american > 0) { view.prompt += game.move.carry_french + " French CU and " view.prompt += game.move.carry_american + " American CU." } else { - view.prompt += game.move.carry_french + " French CU." + view.prompt += game.move.carry_french + " CU." } } else { - view.prompt += game.move.carry_american + " American CU." + view.prompt += game.move.carry_american + " CU." } } else { view.prompt += game.move.carry_american + " no CU." } - if (game.count === 1) - view.prompt += " " + game.move.count + " move left." - else if (game.count > 1) - view.prompt += " " + game.move.count + " moves left." + view.prompt += " " + game.move.count + " MP left." // Cannot stop on enemy general if (!has_enemy_general(location_of_general(game.move.who))) - view.actions.pass = 1 + view.actions.stop = 1 - gen_carry_cu() - gen_move_general() + if (view.move.count > 0) { + gen_carry_cu() + gen_move_general() + } }, pickup_british_cu() { @@ -1962,11 +1981,11 @@ states.ops_general_move = { let from = location_of_general(game.move.who) let cu = game.move.carry_british + game.move.carry_american + game.move.carry_french - let intercept = false + let may_intercept = false if (game.active === P_BRITAIN) { let is_sea_move = path_type(from, to) === "sea" if (has_american_pc(to) && cu > 0 && !is_sea_move && !has_british_cu(to)) - intercept = can_intercept_to(to) + may_intercept = can_intercept_to(to) } game.move.from = from @@ -1974,10 +1993,10 @@ states.ops_general_move = { move_army(game.move.who, from, to) + // TODO: overrun after intercept? + // TODO: disperse continental congress after intercept? + if (cu > 0) { - if (has_enemy_general(to) && !has_enemy_cu(to)) { - capture_enemy_general(to) - } if (game.active === P_BRITAIN && game.congress === to && !has_enemy_cu(to)) { disperse_continental_congress() } @@ -1986,28 +2005,132 @@ states.ops_general_move = { } } - if (intercept) + if (may_intercept) goto_intercept() else resume_moving() }, - pass() { - clear_undo() - let where = location_of_general(game.move.who) + stop() { + push_undo() end_move() - if (count_friendly_generals(where) > 1) - goto_remove_general(where) - else - end_strategy_card() }, } function resume_moving() { - if (has_enemy_cu(game.move.to)) { + // TODO: if general moving alone was captured by intercepting army + if (has_enemy_cu(game.move.to)) goto_start_battle() + else if (game.move.count === 0) + end_move() +} + +function end_move() { + let where = location_of_general(game.move.who) + if (has_general_moved(game.move.who)) { + mark_moved_british_cu(where, game.move.carry_british) + mark_moved_american_cu(where, game.move.carry_american) + mark_moved_french_cu(where, game.move.carry_french) + } + delete game.move + + if (count_friendly_generals(where) > 1) + goto_remove_general(where) + else + end_strategy_card() +} + +function path_type(from, to) { + if (set_has(SPACES[from].path, to)) + return "path" + if (set_has(SPACES[from].wilderness, to)) + return "wilderness" + return "sea" +} + +function gen_carry_cu() { + let where = location_of_general(game.move.who) + if (game.active === P_BRITAIN) { + if (game.move.carry_british > 0) + gen_action("drop_british_cu") + if (game.move.carry_british < 5 && game.move.carry_british < count_unmoved_british_cu(where)) + gen_action("pickup_british_cu") + } else { + let carry_total = game.move.carry_french + game.move.carry_american + if (game.move.carry_french > 0) + gen_action("drop_french_cu") + if (game.move.carry_american > 0) + gen_action("drop_american_cu") + if (carry_total < 5 && game.move.carry_french < count_unmoved_french_cu(where)) + gen_action("pickup_french_cu") + if (carry_total < 5 && game.move.carry_american < count_unmoved_american_cu(where)) + gen_action("pickup_american_cu") + } +} + +function movement_cost(from, to) { + switch (path_type(from, to)) { + case "sea": + return 4 /* must be a sea connection if no direct path */ + case "wilderness": + return 3 + case "path": + return 1 + default: + throw "IMPOSSIBLE" + } +} + +function gen_move_general() { + let from = location_of_general(game.move.who) + let alone = game.move.carry_british + game.move.carry_american + game.move.carry_french === 0 + for (let to of SPACES[from].adjacent) { + let mp = 1 + if (path_type(from, to) === "wilderness") { + if ((from === QUEBEC && to === FALMOUTH) || (to === QUEBEC && from === FALMOUTH)) + if (game.move.who !== ARNOLD) + continue + mp = 3 + } + + if (alone) { + if (has_enemy_cu(to)) + continue + if (has_enemy_pc(to)) + continue + // TODO: more robust check for not stopping (or allow undo in case he gets stuck) + if (has_enemy_general(to) && game.count - mp === 0) + continue + } + + if (game.move.mobility && has_enemy_cu(to)) { + if (game.move.count - mp >= 1) + gen_action_space(to) + } else { + if (game.move.count - mp >= 0) + gen_action_space(to) + } + } + if (game.active === P_BRITAIN && game.count === 4) { + if (is_non_blockaded_port(from)) { + for (let to of all_spaces) { + if (to !== from) { + if (is_non_blockaded_port(to)) { + if (!has_american_pc(to) && !has_american_or_french_cu(to)) { + // don't leave alone + if (alone && has_enemy_general(to)) + continue + // TODO: duplicate action if can move by normal road + gen_action_space(to) + } + } + } + } + } } } +/* INTERCEPT */ + function can_intercept_to(to) { for (let space of SPACES[to].adjacent) { if (has_american_army(space)) { @@ -2037,7 +2160,7 @@ function goto_intercept() { states.intercept = { prompt() { - view.prompt = "Intercept " + game.move.who + " in " + game.move.to + "?" + view.prompt = "Intercept " + game.move.who + " at " + game.move.to + "?" view.actions.pass = 1 gen_intercept() }, @@ -2060,6 +2183,10 @@ states.intercept = { pickup_max_american_cu(game.intercept, location_of_general(g)) intercept_army(g, location_of_general(g), game.move.to) + if (has_enemy_general(game.move.to) && !has_enemy_cu(game.move.to)) { + capture_enemy_general(game.move.to) + } + if (count_friendly_generals(game.move.to) > 1) goto_remove_general_after_intercept() else @@ -2083,99 +2210,461 @@ function end_intercept() { resume_moving() } -function end_move() { - let where = location_of_general(game.move.who) - if (has_general_moved(game.move.who)) { - mark_moved_british_cu(where, game.move.carry_british) - mark_moved_american_cu(where, game.move.carry_american) - mark_moved_french_cu(where, game.move.carry_french) +/* RETREAT BEFORE BATTLE */ + +function can_retreat_before_battle() { + if (game.move.did_intercept) + return false + // can't retreat if attempted (successful or not) interception! + let g = find_american_or_french_general(game.move.to) + if (g && !has_general_moved(g)) + return true + return false +} + +function goto_start_battle() { + clear_undo() + + game.combat = { + attacker: game.active, + a_bonus: 0, + b_bonus: 0, + b_draw_after_battle: false, + a_draw_after_battle: false, + british_losses: 0, } - delete game.move + + if (game.active === P_BRITAIN && can_retreat_before_battle()) + goto_retreat_before_battle() + else + goto_play_attacker_battle_card() } -function path_type(from, to) { - if (set_has(SPACES[from].path, to)) - return "path" - if (set_has(SPACES[from].wilderness, to)) - return "wilderness" - return "sea" +function goto_retreat_before_battle() { + game.active = P_AMERICA + game.state = "retreat_before_battle" } -function gen_carry_cu() { - let where = location_of_general(game.move.who) - if (game.active === P_BRITAIN) { - if (game.move.carry_british > 0) - gen_action("drop_british_cu") - if (game.move.carry_british < 5 && game.move.carry_british < count_unmoved_british_cu(where)) - gen_action("pickup_british_cu") +states.retreat_before_battle = { + prompt() { + view.prompt = "Attempt retreat before battle?" + view.actions.pass = 1 + gen_defender_retreat() + }, + space(to) { + let who = find_american_or_french_general(game.move.to) + let agility = GENERALS[who].agility + if (GENERALS[who].bonus) + agility += 2 + let roll = roll_d6() + if (roll <= agility) { + logp("successfully retreated before battle: " + roll + " <= " + agility) + pickup_max_american_cu(game.move.to) + move_army(who, game.move.to, to) + goto_remove_general_after_retreat_before_battle(to) + } else { + logp("failed to retreat before battle: " + roll + " > " + agility) + end_retreat_before_battle() + } + }, + pass() { + end_retreat_before_battle() + }, +} + +function goto_remove_general_after_retreat_before_battle(where) { + if (count_friendly_generals(where) > 1) { + game.state = "remove_general_after_retreat_before_battle" + game.where = where } else { - let carry_total = game.move.carry_french + game.move.carry_american - if (game.move.carry_french > 0) - gen_action("drop_french_cu") - if (game.move.carry_american > 0) - gen_action("drop_american_cu") - if (carry_total < 5 && game.move.carry_french < count_unmoved_french_cu(where)) - gen_action("pickup_french_cu") - if (carry_total < 5 && game.move.carry_american < count_unmoved_american_cu(where)) - gen_action("pickup_american_cu") + end_remove_general_after_retreat_before_battle() } } -function movement_cost(from, to) { - switch (path_type(from, to)) { - case "sea": - return 4 /* must be a sea connection if no direct path */ - case "wilderness": - return 3 - case "path": - return 1 - default: - throw "IMPOSSIBLE" +states.remove_general_after_retreat_before_battle = { + prompt() { + view.prompt = "Remove a general to the reinforcements box." + gen_remove_general(game.where) + }, + general(g) { + if (game.active === P_BRITAIN) + move_general(g, BRITISH_REINFORCEMENTS) + else + move_general(g, AMERICAN_REINFORCEMENTS) + end_remove_general_after_retreat_before_battle() + }, +} + +function end_remove_general_after_retreat_before_battle() { + let b_cu = count_british_cu(game.move.to) + let a_cu = count_american_and_french_cu(game.move.to) + if (a_cu === 0) { + end_battle() + } else if (b_cu >= 4 && a_cu === 1) { + overrun(game.move.to) + end_battle() + } else { + end_retreat_before_battle() } } -function gen_move_general() { - let from = location_of_general(game.move.who) - let alone = game.move.carry_british + game.move.carry_american + game.move.carry_french === 0 - for (let to of SPACES[from].adjacent) { - let mp = 1 - if (path_type(from, to) === "wilderness") { - if ((from === QUEBEC && to === FALMOUTH) || (to === QUEBEC && from === FALMOUTH)) - if (game.move.who !== ARNOLD) - continue - mp = 3 +/* BATTLE CARDS */ + +function goto_play_attacker_battle_card() { + game.active = game.combat.attacker + game.state = "play_attacker_battle_card" +} + +states.play_attacker_battle_card = { + prompt() { + view.prompt = "Attack: Play or discard event for DRM." + view.actions.pass = 1 + gen_battle_card() + }, + card_battle_play(c) { + play_card(c, "for +2 DRM") + if (game.active === P_BRITAIN) { + if (CARDS[c].event === "remove_benedict_arnold") + remove_benedict_arnold() + game.combat.b_draw_after_battle = true + game.combat.b_bonus += 2 + } else { + game.combat.a_draw_after_battle = true + game.combat.a_bonus += 2 + } + goto_play_defender_battle_card() + }, + card_battle_discard(c) { + discard_card(c, "for +1 DRM") + if (game.active === P_BRITAIN) { + game.combat.b_draw_after_battle = true + game.combat.b_bonus += 1 + } else { + game.combat.a_draw_after_battle = true + game.combat.a_bonus += 1 } + goto_play_defender_battle_card() + }, + pass() { + goto_play_defender_battle_card() + }, +} - if (alone) { - if (has_enemy_cu(to)) - continue - if (has_enemy_pc(to)) - continue - // TODO: more robust check for not stopping (or allow undo in case he gets stuck) - if (has_enemy_general(to) && game.count - mp === 0) - continue +function goto_play_defender_battle_card() { + game.state = "play_defender_battle_card" + game.active = ENEMY[game.combat.attacker] +} + +states.play_defender_battle_card = { + prompt() { + view.prompt = "Defend: Play or discard event for DRM." + view.actions.pass = 1 + gen_battle_card() + }, + card_battle_play(c) { + play_card(c, "for +2 DRM") + if (game.active === P_BRITAIN) { + if (CARDS[c].event === "remove_benedict_arnold") + remove_benedict_arnold() + game.combat.b_draw_after_battle = true + game.combat.b_bonus += 2 + } else { + game.combat.a_draw_after_battle = true + game.combat.a_bonus += 2 } + resolve_battle() + }, + card_battle_discard(c) { + discard_card(c, "for +1 DRM") + if (game.active === P_BRITAIN) { + game.combat.b_draw_after_battle = true + game.combat.b_bonus += 1 + } else { + game.combat.a_draw_after_battle = true + game.combat.a_bonus += 1 + } + resolve_battle() + }, + pass() { + resolve_battle() + }, +} - if (game.move.mobility && has_enemy_cu(to)) { - if (game.move.count - mp >= 1) - gen_action_space(to) +function gen_battle_card() { + for (let c of active_hand()) { + let card = CARDS[c] + if (game.active === P_BRITAIN) { + switch (card.type) { + case "british-battle": + case "british-event-or-battle": + gen_action_card("card_battle_play", c) + break + case "british-event": + case "american-event": + case "american-battle": + gen_action_card("card_battle_discard", c) + break + } } else { - if (game.move.count - mp >= 0) - gen_action_space(to) + switch (card.type) { + case "british-battle": + case "british-event-or-battle": + case "british-event": + case "american-event": + gen_action_card("card_battle_discard", c) + break + case "american-battle": + gen_action_card("card_battle_play", c) + break + } } } - if (game.active === P_BRITAIN && game.count === 4) { +} + +/* RESOLVE BATTLE */ + +function roll_loser_combat_losses(log) { + let roll = roll_d6() + let losses = 0 + log.push("Loser Combat Loss Roll: " + roll) + switch (roll) { + case 1: + case 2: + case 3: + losses = 1 + break + case 4: + case 5: + losses = 2 + break + case 6: + losses = 3 + break + } + return losses +} + +function roll_winner_combat_losses(log, losing_general) { + let agility = losing_general !== NOBODY ? GENERALS[losing_general].agility : 0 + let roll = roll_d6() + log.push("Enemy Agility Rating: " + agility) + log.push("Winner Combat Loss Roll: " + roll) + let losses = 0 + switch (agility) { + case 0: + losses = roll === 1 ? 1 : 0 + break + case 1: + losses = roll <= 2 ? 1 : 0 + break + case 2: + losses = roll <= 3 ? 1 : 0 + break + case 3: + losses = roll <= 4 ? 1 : 0 + break + } + return losses +} + +function apply_british_combat_losses(max) { + let n = Math.min(count_british_cu(game.move.to), max) + remove_british_cu(game.move.to, n) + return n +} + +function apply_american_combat_losses(max) { + let n = Math.min(count_american_cu(game.move.to), max) + remove_american_cu(game.move.to, n) + return n +} + +function apply_french_combat_losses(max) { + let n = Math.min(count_french_cu(game.move.to), max) + remove_french_cu(game.move.to, n) + return n +} + +function apply_american_and_french_combat_losses(max) { + let n = apply_american_combat_losses(max) + if (n < max) + n += apply_french_combat_losses(max - n) + return n +} + +function resolve_battle() { + let a_log = [] + let b_log = [] + + game.active = ENEMY[game.active] + let b_g = find_british_general(game.move.to) + let b_cu = count_british_cu(game.move.to) + let a_g = find_american_or_french_general(game.move.to) + let a_cu = count_american_and_french_cu(game.move.to) + let b_br = 0 + let a_br = 0 + + if (b_g !== NOBODY) + b_log.push("General: " + b_g) + if (a_g !== NOBODY) + a_log.push("General: " + a_g) + + if (b_g !== NOBODY) { + let roll = roll_d6() + if (roll <= 3) + b_br = Math.min(Math.floor(GENERALS[b_g].battle / 2), b_cu) + else + b_br = Math.min(GENERALS[b_g].battle, b_cu) + b_log.push("Actual Battle Rating Roll: " + roll) + } + + if (a_g !== NOBODY) { + let roll = roll_d6() + if (roll <= 3) + a_br = Math.min(Math.floor(GENERALS[a_g].battle / 2), a_cu) + else + a_br = Math.min(GENERALS[a_g].battle, a_cu) + a_log.push("Actual Battle Rating Roll: " + roll) + } + + b_log.push("+" + b_cu + " CU") + a_log.push("+" + a_cu + " CU") + b_log.push("+" + b_br + " Actual Battle Rating") + a_log.push("+" + a_br + " Actual Battle Rating") + + let b_drm = b_cu + b_br + game.combat.b_bonus + if (has_flag(F_REGULARS)) { + b_log.push("+1 British Regulars' Advantage") + b_drm += 1 + } + if (is_non_blockaded_port(game.move.to)) { + if (is_fortified_port(game.move.to)) { + if (has_british_pc(game.move.to)) { + b_log.push("+1 Royal Navy Support") + b_drm += 1 + } + } else { + b_log.push("+1 Royal Navy Support") + b_drm += 1 + } + } + if (is_british_militia(game.move.to)) { + b_log.push("+1 Militia") + b_drm += 1 + } + if (game.combat.b_bonus === 2) + b_log.push("+2 Battle Card") + else if (game.combat.b_bonus === 1) + b_log.push("+1 Discard of an Event Card") + + let a_drm = a_cu + a_br + game.combat.a_bonus + if (is_american_militia(game.move.to)) { + a_log.push("+1 Militia") + a_drm += 1 + } + if (is_american_winter_offensive()) { + a_log.push("+2 American Winter Offensive") + a_drm += 2 + } + if (game.combat.a_bonus === 2) + a_log.push("+2 Battle Card") + else if (game.combat.a_bonus === 1) + a_log.push("+1 Discard of an Event Card") + if (game.move.did_intercept) { + a_log.push("+1 Interception") + a_drm += 1 + } + + let b_roll = roll_d6() + let a_roll = roll_d6() + + b_log.push("Battle Roll: " + b_roll) + b_log.push("Battle Total: " + (b_roll + b_drm)) + a_log.push("Battle Roll: " + a_roll) + a_log.push("Battle Total: " + (a_roll + a_drm)) + + let victor + if (game.active === P_BRITAIN) + victor = b_roll + b_drm >= a_roll + a_drm ? P_BRITAIN : P_AMERICA + else + victor = b_roll + b_drm >= a_roll + a_drm ? P_BRITAIN : P_AMERICA + + let a_lost_cu, b_lost_cu + if (victor === P_BRITAIN) { + b_lost_cu = roll_winner_combat_losses(b_log, a_g) + a_lost_cu = roll_loser_combat_losses(a_log) + } else { + b_lost_cu = roll_loser_combat_losses(b_log) + a_lost_cu = roll_winner_combat_losses(a_log, b_g) + } + + game.combat.british_losses = apply_british_combat_losses(b_lost_cu) + let american_losses = apply_american_and_french_combat_losses(a_lost_cu) + + b_log.push("Losses: " + game.combat.british_losses + " CU") + a_log.push("Losses: " + american_losses + " CU") + + // Special case: winning general with no CU on enemy PC is captured + if (victor === P_BRITAIN) { + if (b_g && count_british_cu(game.move.to) === 0 && has_american_pc(game.move.to)) + capture_british_general(game.move.to) + } else { + if (a_g && count_american_and_french_cu(game.move.to) === 0 && has_british_pc(game.move.to)) + capture_american_or_french_general(game.move.to) + } + + log("BRITISH BATTLE REPORT:\n" + b_log.join("\n")) + log("AMERICAN BATTLE REPORT:\n" + a_log.join("\n")) + log(victor + " victory in " + game.move.to + "!") + + if (victor === P_AMERICA) + advance_french_alliance(1) + + goto_retreat_after_battle(victor) +} + +/* RETREAT AFTER BATTLE */ + +function can_defender_retreat(to) { + if (to === game.move.from) + return false + if (has_enemy_pc(to)) + return false + if (has_enemy_cu(to)) + return false + return true +} + +function can_attacker_retreat() { + let to = game.move.from + if (has_enemy_pc(to)) + return false + if (has_enemy_cu(to)) + return false + return true +} + +function gen_defender_retreat() { + let from = game.move.to + for (let to of SPACES[from].adjacent) { + if (can_defender_retreat(to)) + gen_action_space(to) + } + if (game.active === P_BRITAIN) { + let can_sea_retreat = false if (is_non_blockaded_port(from)) { + if (is_fortified_port(from)) { + if (has_british_pc(from) && is_non_blockaded_port(from)) + can_sea_retreat = true + } else { + can_sea_retreat = true + } + } + if (can_sea_retreat) { for (let to of all_spaces) { - if (to !== from) { - if (is_non_blockaded_port(to)) { - if (!has_american_pc(to) && !has_american_or_french_cu(to)) { - // don't leave alone - if (alone && has_enemy_general(to)) - continue - // TODO: duplicate action if can move by normal road - gen_action_space(to) - } + if (to !== from && is_non_blockaded_port(to)) { + if (!has_american_pc(to) && !has_american_or_french_cu(to)) { + gen_action_space(to) } } } @@ -2183,6 +2672,84 @@ function gen_move_general() { } } +function gen_attacker_retreat() { + if (can_attacker_retreat()) + gen_action_space(game.move.from) +} + +function end_retreat_before_battle() { + goto_play_attacker_battle_card() +} + + +function goto_retreat_after_battle(victor) { + if (victor === P_BRITAIN) { + let who = find_american_or_french_general(game.move.to) + if (who === NOBODY && count_american_and_french_cu(game.move.to) === 0) + return end_battle() + } else { + let who = find_british_general(game.move.to) + if (who === NOBODY && count_british_cu(game.move.to) === 0) + return end_battle() + } + game.active = ENEMY[victor] + game.state = "retreat_after_battle" +} + +states.retreat_after_battle = { + prompt() { + view.prompt = "Retreat after battle." + gen_action("surrender") + if (game.active === game.combat.attacker) + gen_attacker_retreat() + else + gen_defender_retreat() + }, + space(to) { + logp("retreated to " + to) + if (game.active === P_BRITAIN) + retreat_british_army(game.move.to, to) + else + retreat_american_army(game.move.to, to) + if (count_friendly_generals(to) > 1) + goto_remove_general_after_retreat(to) + else + end_battle() + }, + surrender() { + logp("surrendered") + + if (game.active === P_BRITAIN) + surrender_british_army(game.move.to) + else + surrender_american_army(game.move.to) + + end_battle() + }, +} + +/* END BATTLE */ + +function end_battle() { + game.active = game.combat.attacker + + if (game.active === P_BRITAIN && game.congress === game.move.to) + disperse_continental_congress() + + if (game.combat.british_losses >= 3) + lose_regular_advantage() + + // TODO: delay until end of campaign + if (game.combat.b_draw_after_battle) + set_add(game.b_hand, deal_card()) + if (game.combat.a_draw_after_battle) + set_add(game.a_hand, deal_card()) + + delete game.combat + + end_move() +} + /* CAMPAIGN */ function goto_campaign(c) { @@ -2219,6 +2786,7 @@ events.remove_random_american_card = function (c, card) { } function remove_random_card(hand) { + clear_undo() if (hand.length > 0) { let i = random(hand.length) let c = hand[i] @@ -2280,7 +2848,7 @@ events.remove_british_pc_from = function (c, card) { states.remove_british_pc_from = { prompt() { - view.prompt = "Remove British PC markers from " + game.where.join(", ") + ". " + game.count + " left." + view.prompt = "Remove British PC markers from " + game.where.map(x=>data.colony_name[x]).join(", ") + ". " + game.count + " left." view.actions.pass = 1 gen_remove_british_pc_from(game.where) }, @@ -2679,540 +3247,6 @@ function do_event(c) { throw new Error("Event not implemented yet: " + card.event) } -/* BATTLE */ - -function can_retreat_before_battle() { - if (game.move.did_intercept) - return false - // can't retreat if attempted (successful or not) interception! - let g = find_american_or_french_general(game.move.to) - if (g && !has_general_moved(g)) - return true - return false -} - -function goto_start_battle() { - clear_undo() - - game.combat = { - attacker: game.active, - a_bonus: 0, - b_bonus: 0, - b_draw_after_battle: false, - a_draw_after_battle: false, - british_losses: 0, - } - - if (game.active === P_BRITAIN && can_retreat_before_battle()) - goto_retreat_before_battle() - else - goto_play_attacker_battle_card() -} - -function goto_retreat_before_battle() { - game.active = P_AMERICA - game.state = "retreat_before_battle" -} - -states.retreat_before_battle = { - prompt() { - view.prompt = "Attempt retreat before battle?" - view.actions.pass = 1 - gen_defender_retreat() - }, - space(to) { - let who = find_american_or_french_general(game.move.to) - let agility = GENERALS[who].agility - if (GENERALS[who].bonus) - agility += 2 - let roll = roll_d6() - if (roll <= agility) { - logp("successfully retreated before battle: " + roll + " <= " + agility) - pickup_max_american_cu(game.move.to) - move_army(who, game.move.to, to) - goto_remove_general_after_retreat_before_battle(to) - } else { - logp("failed to retreat before battle: " + roll + " > " + agility) - end_retreat_before_battle() - } - }, - pass() { - end_retreat_before_battle() - }, -} - -function goto_remove_general_after_retreat_before_battle(where) { - if (count_friendly_generals(where) > 1) { - game.state = "remove_general_after_retreat_before_battle" - game.where = where - } else { - end_remove_general_after_retreat_before_battle() - } -} - -states.remove_general_after_retreat_before_battle = { - prompt() { - view.prompt = "Remove a general to the reinforcements box." - gen_remove_general(game.where) - }, - general(g) { - if (game.active === P_BRITAIN) - move_general(g, BRITISH_REINFORCEMENTS) - else - move_general(g, AMERICAN_REINFORCEMENTS) - end_remove_general_after_retreat_before_battle() - }, -} - -function end_remove_general_after_retreat_before_battle() { - let b_cu = count_british_cu(game.move.to) - let a_cu = count_american_and_french_cu(game.move.to) - if (a_cu === 0) { - end_battle() - } else if (b_cu >= 4 && a_cu === 1) { - overrun(game.move.to) - end_battle() - } else { - end_retreat_before_battle() - } -} - -function can_defender_retreat(to) { - if (to === game.move.from) - return false - if (has_enemy_pc(to)) - return false - if (has_enemy_cu(to)) - return false - return true -} - -function can_attacker_retreat() { - let to = game.move.from - if (has_enemy_pc(to)) - return false - if (has_enemy_cu(to)) - return false - return true -} - -function gen_defender_retreat() { - let from = game.move.to - for (let to of SPACES[from].adjacent) { - if (can_defender_retreat(to)) - gen_action_space(to) - } - if (game.active === P_BRITAIN) { - let can_sea_retreat = false - if (is_non_blockaded_port(from)) { - if (is_fortified_port(from)) { - if (has_british_pc(from) && is_non_blockaded_port(from)) - can_sea_retreat = true - } else { - can_sea_retreat = true - } - } - if (can_sea_retreat) { - for (let to of all_spaces) { - if (to !== from && is_non_blockaded_port(to)) { - if (!has_american_pc(to) && !has_american_or_french_cu(to)) { - gen_action_space(to) - } - } - } - } - } -} - -function gen_attacker_retreat() { - if (can_attacker_retreat()) - gen_action_space(game.move.from) -} - -function end_retreat_before_battle() { - goto_play_attacker_battle_card() -} - -function goto_play_attacker_battle_card() { - game.active = game.combat.attacker - game.state = "play_attacker_battle_card" -} - -states.play_attacker_battle_card = { - prompt() { - view.prompt = "Attack: Play or discard event for DRM." - view.actions.pass = 1 - gen_battle_card() - }, - card_battle_play(c) { - play_card(c, "for +2 DRM") - if (game.active === P_BRITAIN) { - if (CARDS[c].event === "remove_benedict_arnold") - remove_benedict_arnold() - game.combat.b_draw_after_battle = true - game.combat.b_bonus += 2 - } else { - game.combat.a_draw_after_battle = true - game.combat.a_bonus += 2 - } - goto_play_defender_battle_card() - }, - card_battle_discard(c) { - discard_card(c, "for +1 DRM") - if (game.active === P_BRITAIN) { - game.combat.b_draw_after_battle = true - game.combat.b_bonus += 1 - } else { - game.combat.a_draw_after_battle = true - game.combat.a_bonus += 1 - } - goto_play_defender_battle_card() - }, - pass() { - goto_play_defender_battle_card() - }, -} - -function goto_play_defender_battle_card() { - game.state = "play_defender_battle_card" - game.active = ENEMY[game.combat.attacker] -} - -states.play_defender_battle_card = { - prompt() { - view.prompt = "Defend: Play or discard event for DRM." - view.actions.pass = 1 - gen_battle_card() - }, - card_battle_play(c) { - play_card(c, "for +2 DRM") - if (game.active === P_BRITAIN) { - if (CARDS[c].event === "remove_benedict_arnold") - remove_benedict_arnold() - game.combat.b_draw_after_battle = true - game.combat.b_bonus += 2 - } else { - game.combat.a_draw_after_battle = true - game.combat.a_bonus += 2 - } - resolve_battle() - }, - card_battle_discard(c) { - discard_card(c, "for +1 DRM") - if (game.active === P_BRITAIN) { - game.combat.b_draw_after_battle = true - game.combat.b_bonus += 1 - } else { - game.combat.a_draw_after_battle = true - game.combat.a_bonus += 1 - } - resolve_battle() - }, - pass() { - resolve_battle() - }, -} - -function gen_battle_card() { - for (let c of active_hand()) { - let card = CARDS[c] - if (game.active === P_BRITAIN) { - switch (card.type) { - case "british-battle": - case "british-event-or-battle": - gen_action_card("card_battle_play", c) - break - case "british-event": - case "american-event": - case "american-battle": - gen_action_card("card_battle_discard", c) - break - } - } else { - switch (card.type) { - case "british-battle": - case "british-event-or-battle": - case "british-event": - case "american-event": - gen_action_card("card_battle_discard", c) - break - case "american-battle": - gen_action_card("card_battle_play", c) - break - } - } - } -} - -function roll_loser_combat_losses(log) { - let roll = roll_d6() - let losses = 0 - log.push("Loser Combat Loss Roll: " + roll) - switch (roll) { - case 1: - case 2: - case 3: - losses = 1 - break - case 4: - case 5: - losses = 2 - break - case 6: - losses = 3 - break - } - return losses -} - -function roll_winner_combat_losses(log, losing_general) { - let agility = losing_general !== NOBODY ? GENERALS[losing_general].agility : 0 - let roll = roll_d6() - log.push("Enemy Agility Rating: " + agility) - log.push("Winner Combat Loss Roll: " + roll) - let losses = 0 - switch (agility) { - case 0: - losses = roll === 1 ? 1 : 0 - break - case 1: - losses = roll <= 2 ? 1 : 0 - break - case 2: - losses = roll <= 3 ? 1 : 0 - break - case 3: - losses = roll <= 4 ? 1 : 0 - break - } - return losses -} - -function apply_british_combat_losses(max) { - let n = Math.min(count_british_cu(game.move.to), max) - remove_british_cu(game.move.to, n) - return n -} - -function apply_american_combat_losses(max) { - let n = Math.min(count_american_cu(game.move.to), max) - remove_american_cu(game.move.to, n) - return n -} - -function apply_french_combat_losses(max) { - let n = Math.min(count_french_cu(game.move.to), max) - remove_french_cu(game.move.to, n) - return n -} - -function apply_american_and_french_combat_losses(max) { - let n = apply_american_combat_losses(max) - if (n < max) - n += apply_french_combat_losses(max - n) - return n -} - -function resolve_battle() { - let a_log = [] - let b_log = [] - - game.active = ENEMY[game.active] - let b_g = find_british_general(game.move.to) - let b_cu = count_british_cu(game.move.to) - let a_g = find_american_or_french_general(game.move.to) - let a_cu = count_american_and_french_cu(game.move.to) - let b_br = 0 - let a_br = 0 - - if (b_g !== NOBODY) - b_log.push("General: " + b_g) - if (a_g !== NOBODY) - a_log.push("General: " + a_g) - - if (b_g !== NOBODY) { - let roll = roll_d6() - if (roll <= 3) - b_br = Math.min(Math.floor(GENERALS[b_g].battle / 2), b_cu) - else - b_br = Math.min(GENERALS[b_g].battle, b_cu) - b_log.push("Actual Battle Rating Roll: " + roll) - } - - if (a_g !== NOBODY) { - let roll = roll_d6() - if (roll <= 3) - a_br = Math.min(Math.floor(GENERALS[a_g].battle / 2), a_cu) - else - a_br = Math.min(GENERALS[a_g].battle, a_cu) - a_log.push("Actual Battle Rating Roll: " + roll) - } - - b_log.push("+" + b_cu + " CU") - a_log.push("+" + a_cu + " CU") - b_log.push("+" + b_br + " Actual Battle Rating") - a_log.push("+" + a_br + " Actual Battle Rating") - - let b_drm = b_cu + b_br + game.combat.b_bonus - if (has_flag(F_REGULARS)) { - b_log.push("+1 British Regulars' Advantage") - b_drm += 1 - } - if (is_non_blockaded_port(game.move.to)) { - if (is_fortified_port(game.move.to)) { - if (has_british_pc(game.move.to)) { - b_log.push("+1 Royal Navy Support") - b_drm += 1 - } - } else { - b_log.push("+1 Royal Navy Support") - b_drm += 1 - } - } - if (is_british_militia(game.move.to)) { - b_log.push("+1 Militia") - b_drm += 1 - } - if (game.combat.b_bonus === 2) - b_log.push("+2 Battle Card") - else if (game.combat.b_bonus === 1) - b_log.push("+1 Discard of an Event Card") - - let a_drm = a_cu + a_br + game.combat.a_bonus - if (is_american_militia(game.move.to)) { - a_log.push("+1 Militia") - a_drm += 1 - } - if (is_american_winter_offensive()) { - a_log.push("+2 American Winter Offensive") - a_drm += 2 - } - if (game.combat.a_bonus === 2) - a_log.push("+2 Battle Card") - else if (game.combat.a_bonus === 1) - a_log.push("+1 Discard of an Event Card") - if (game.move.did_intercept) { - a_log.push("+1 Interception") - a_drm += 1 - } - - let b_roll = roll_d6() - let a_roll = roll_d6() - - b_log.push("Battle Roll: " + b_roll) - b_log.push("Battle Total: " + (b_roll + b_drm)) - a_log.push("Battle Roll: " + a_roll) - a_log.push("Battle Total: " + (a_roll + a_drm)) - - let victor - if (game.active === P_BRITAIN) - victor = b_roll + b_drm >= a_roll + a_drm ? P_BRITAIN : P_AMERICA - else - victor = b_roll + b_drm >= a_roll + a_drm ? P_BRITAIN : P_AMERICA - - let a_lost_cu, b_lost_cu - if (victor === P_BRITAIN) { - b_lost_cu = roll_winner_combat_losses(b_log, a_g) - a_lost_cu = roll_loser_combat_losses(a_log) - } else { - b_lost_cu = roll_loser_combat_losses(b_log) - a_lost_cu = roll_winner_combat_losses(a_log, b_g) - } - - game.combat.british_losses = apply_british_combat_losses(b_lost_cu) - let american_losses = apply_american_and_french_combat_losses(a_lost_cu) - - b_log.push("Losses: " + game.combat.british_losses + " CU") - a_log.push("Losses: " + american_losses + " CU") - - // Special case: winning general with no CU on enemy PC is captured - if (victor === P_BRITAIN) { - if (b_g && count_british_cu(game.move.to) === 0 && has_american_pc(game.move.to)) - capture_british_general(game.move.to) - } else { - if (a_g && count_american_and_french_cu(game.move.to) === 0 && has_british_pc(game.move.to)) - capture_american_or_french_general(game.move.to) - } - - log("BRITISH BATTLE REPORT:\n" + b_log.join("\n")) - log("AMERICAN BATTLE REPORT:\n" + a_log.join("\n")) - log(victor + " victory in " + game.move.to + "!") - - if (victor === P_AMERICA) - advance_french_alliance(1) - - goto_retreat_after_battle(victor) -} - -function goto_retreat_after_battle(victor) { - if (victor === P_BRITAIN) { - let who = find_american_or_french_general(game.move.to) - if (who === NOBODY && count_american_and_french_cu(game.move.to) === 0) - return end_battle() - } else { - let who = find_british_general(game.move.to) - if (who === NOBODY && count_british_cu(game.move.to) === 0) - return end_battle() - } - game.active = ENEMY[victor] - game.state = "retreat_after_battle" -} - -states.retreat_after_battle = { - prompt() { - view.prompt = "Retreat after battle." - gen_action("surrender") - if (game.active === game.combat.attacker) - gen_attacker_retreat() - else - gen_defender_retreat() - }, - space(to) { - logp("retreated to " + to) - if (game.active === P_BRITAIN) - retreat_british_army(game.move.to, to) - else - retreat_american_army(game.move.to, to) - if (count_friendly_generals(to) > 1) - goto_remove_general_after_retreat(to) - else - end_battle() - }, - surrender() { - // End battle here, so if Washington is captured we can handle the interrupt state. - let active = game.active - // TODO: ugly clean this up - end_battle() - - logp("surrendered") - if (active === P_BRITAIN) - surrender_british_army(game.move.to) - else - surrender_american_army(game.move.to) - }, -} - -function end_battle() { - game.active = game.combat.attacker - - if (game.active === P_BRITAIN && game.congress === game.move.to) - disperse_continental_congress() - - if (game.combat.british_losses >= 3) - lose_regular_advantage() - - // TODO: delay until end of campaign - if (game.combat.b_draw_after_battle) - set_add(game.b_hand, deal_card()) - if (game.combat.a_draw_after_battle) - set_add(game.a_hand, deal_card()) - - delete game.combat - - end_move() - end_strategy_card() -} - /* WINTER ATTRITION PHASE */ function apply_single_winter_attrition(owner, space) { |