summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--rules.js536
1 files changed, 322 insertions, 214 deletions
diff --git a/rules.js b/rules.js
index c121e55..297ae2b 100644
--- a/rules.js
+++ b/rules.js
@@ -15,6 +15,8 @@
// TODO: defender retreat procedure needs love
// TODO: for_each_exit -> flat list of all exits
+// TODO: retreats with no survivors
+
// TODO: flat force definition - use sum of leader command rating
// (only allow dropping subordinate if stacking limit allows)
@@ -1410,6 +1412,17 @@ function has_unbesieged_enemy_fort_or_fortress(space) {
return is_space_unbesieged(space) && has_enemy_fort_or_fortress(space);
}
+function has_non_moving_unbesieged_friendly_units(space) {
+ let force = moving_piece();
+ for (let p = first_friendly_unit; p <= last_friendly_unit; ++p) {
+ if (is_piece_in_space(p, space) && is_piece_unbesieged(p)) {
+ if (!is_piece_in_force(p, force))
+ return true;
+ }
+ }
+ return false;
+}
+
function has_unbesieged_friendly_units(space) {
for (let p = first_friendly_unit; p <= last_friendly_unit; ++p)
if (is_piece_in_space(p, space) && is_piece_unbesieged(p))
@@ -1546,8 +1559,8 @@ function avoiding_piece() {
return game.move.avoiding;
}
-function moving_piece_came_from(here) {
- return game.move.path[here];
+function moving_piece_came_from() {
+ return game.move.came_from;
}
function battle_space() {
@@ -1992,79 +2005,6 @@ function list_intercept_spaces(is_lone_ld, is_lone_ax) {
return intercept;
}
-function gen_intercept(is_lone_ax, to) {
- if (has_unbesieged_enemy_units(to)) {
- // 6.721 exception -- can always intercept units infiltrating same space
- // TODO: infiltration
- /*
- for_each_friendly_piece_in_space(to, p => {
- // TODO: unbesieged
- gen_action_piece(p);
- });
- */
-
- for_each_exit(to, from => {
- // 6.721
- if (is_lone_ax && is_wilderness_or_mountain(to)) {
- let has_ax = false;
- let has_br_indians = false;
- for_each_friendly_unit_in_space(from, p => {
- if (is_piece_unbesieged(p)) {
- if (is_auxiliary_unit(p)) {
- gen_action_piece(p);
- if (is_british_iroquois_or_mohawk(p))
- has_br_indians = true;
- else
- has_ax = true;
- }
- }
- });
- // allow leaders to accompany intercepting auxiliary unit
- if (has_ax) {
- for_each_friendly_leader_in_space(from, p => {
- if (is_piece_unbesieged(p))
- gen_action_piece(p);
- });
- } else if (has_br_indians) {
- if (is_piece_in_space(JOHNSON, from)) {
- if (is_piece_unbesieged(JOHNSON))
- gen_action_piece(JOHNSON);
- }
- }
- } else {
- for_each_friendly_piece_in_space(from, p => {
- if (is_piece_unbesieged(p))
- gen_action_piece(p);
- });
- }
- });
- }
-}
-
-function search_naval_move(who, start_space, start_cost) {
- let move_cost = game.move.cost = {};
- let move_path = game.move.path = {};
- let candidates = (game.active === FRANCE) ? french_ports : ports;
-
- if (start_cost > 0)
- return;
-
- let from = piece_space(who);
-
- if (!candidates.includes(from) || !is_friendly_controlled_space(from))
- return;
-
- game.move.path[from] = null;
- candidates.forEach(space => {
- if (space === from)
- return;
- if (is_friendly_controlled_space(space)) {
- game.move.cost[space] = 1;
- game.move.path[space] = from;
- }
- })
-}
-
// Use Breadth First Search to find all paths.
function pq_push(queue, item, prio) {
@@ -2863,17 +2803,17 @@ function goto_move_piece(who) {
game.state = 'move';
game.move = {
moving: who,
+ came_from: 0,
+ infiltrated: 0,
intercepting: null,
intercepted: [],
did_attempt_intercept: 0,
- infiltrate: {},
avoiding: null,
avoided: [],
start_space: from,
- start_cost: -1,
- type: is_only_port_space(from) ? 'naval' : 'land',
- cost: null,
- path: null,
+ used: -1,
+ did_carry: 0,
+ type: is_only_port_space(from) ? 'naval' : 'boat',
};
game.raid = {
where: 0,
@@ -2921,7 +2861,7 @@ states.siege_or_move = {
function goto_break_siege() {
console.log("BREAK SIEGE");
let here = moving_piece_space();
- game.move.path = { [here]: here };
+ game.move.came_from = here;
goto_avoid_battle();
}
@@ -2935,98 +2875,199 @@ function may_naval_move(who) {
return true;
}
+function land_movement_cost() {
+ return game.events.foul_weather ? 2 : movement_allowance(moving_piece());
+}
+
function max_movement_cost() {
switch (game.move.type) {
- case 'boat': return (game.events.foul_weather ? 2 : 9) * 2; // use odd numbers for paths with one land connection
- case 'land': return (game.events.foul_weather ? 2 : movement_allowance(moving_piece()));
+ case 'boat': return game.events.foul_weather ? 2 : 9;
+ case 'land': return land_movement_cost();
case 'naval': return 1;
}
}
function resume_move() {
// Interrupt for Foul Weather response at first opportunity to move.
- if (game.move.start_cost < 0) {
+ if (game.move.used < 0) {
if (is_enemy_card_available(FOUL_WEATHER)) {
set_active(enemy());
game.state = 'foul_weather';
return;
}
- game.move.start_cost = 0;
+ game.move.used = 0;
}
game.state = 'move';
- if (game.move.type === null) {
- game.move.cost = {};
- game.move.path = {};
- throw Error("WHAT IS THIS");
- }
-
console.log("RESUME_MOVE");
+}
- let max_cost = max_movement_cost();
- if (game.move.start_cost < max_cost) {
- let who = moving_piece();
- const is_lone_ax = is_lone_auxiliary(who);
- const is_lone_ld = is_lone_leader(who);
- game.move.intercept = list_intercept_spaces(is_lone_ld, is_lone_ax);
- switch (game.move.type) {
- case 'boat':
- search_boat_move(who, piece_space(who), game.move.start_cost, max_cost);
- break;
- case 'land':
- search_land_move(who, piece_space(who), game.move.start_cost, max_cost);
- break;
- case 'naval':
- if (may_naval_move(who))
- search_naval_move(who, piece_space(who), game.move.start_cost);
- break;
- break;
- }
- } else {
- game.move.intercept = {};
- game.move.cost = {};
- game.move.path = {};
+function remove_enemy_forts_uc_in_path(s) {
+ if (has_enemy_fort_uc(s)) {
+ log(`remove fort u/c in ${space_name(s)}`);
+ remove_enemy_fort_uc(s);
}
}
-function print_path(path, destination, first) {
- function print_path_rec(prev, next) {
- if (path[prev] !== null)
- print_path_rec(path[prev], prev);
- else if (first)
- log("moves from " + space_name(prev));
- log("moves to " + space_name(next));
+function is_land_path(from, to) {
+ return spaces[from].land.includes(to);
+}
+
+function has_friendly_fortifications_or_cultivated(s) {
+ return has_friendly_fortifications(s) || is_originally_friendly(s);
+}
+
+function stop_move() {
+ game.move.used = 9;
+}
+
+function gen_naval_move() {
+ let from = moving_piece_space();
+ let candidates = (game.active === FRANCE) ? french_ports : ports;
+ if (!candidates.includes(from) || !is_friendly_controlled_space(from))
+ return;
+ candidates.forEach(to => {
+ if (to === from)
+ return;
+ if (is_friendly_controlled_space(to))
+ gen_action_space(to);
+ });
+}
+
+function is_carry_connection(from, to) {
+ const from_ff = has_friendly_fortifications_or_cultivated(from);
+ const to_ff = has_friendly_fortifications_or_cultivated(to);
+ return (from_ff && to_ff);
+}
+
+function can_move_by_boat(from, to) {
+ if (game.move.used < land_movement_cost())
+ return true;
+ if (is_land_path(from, to)) {
+ if (!game.move.did_carry)
+ return is_carry_connection();
+ return false;
}
- print_path_rec(path[destination], destination);
+ return true;
}
-function remove_enemy_forts_uc_in_path(path, space) {
- for (;;) {
- if (has_enemy_fort_uc(space)) {
- log(`remove fort u/c in ${space_name(space)}`);
- remove_enemy_fort_uc(space);
+function gen_regular_move() {
+ let who = moving_piece();
+ let from = moving_piece_space();
+ let is_lone_ld = is_lone_leader(who);
+ let has_dt = force_has_drilled_troops(who);
+ for_each_exit(from, to => {
+ if (is_lone_ld) {
+ // Lone leaders can never enter an enemy occupied space
+ if (has_unbesieged_enemy_units(to) || has_unbesieged_enemy_fortifications(to))
+ return; // continue;
+ } else {
+ // Must have Drilled Troops to enter an enemy fort or fortress space.
+ // TODO: Infiltrate
+ if (has_unbesieged_enemy_fort_or_fortress(to)) {
+ if (!has_dt)
+ return;
+ }
}
- let next = path[space];
- if (next === null)
- break;
- space = next;
+
+ if (game.move.type === 'boat') {
+ if (can_move_by_boat(from, to))
+ gen_action_space(to);
+ } else {
+ if (can_move_to(who, to))
+ gen_action_space(to);
+ }
+ });
+}
+
+function apply_move(to) {
+ let who = moving_piece();
+ let from = moving_piece_space();
+
+ game.move.used ++;
+ game.move.came_from = from;
+ game.raid.from[to] = from; // remember where raiders came from so they can retreat after battle
+
+ // Downgrade from Boat to Land movement if not going by river or carries.
+ if (game.move.type === 'boat') {
+ if (is_land_path(from, to)) {
+ if (!game.move.did_carry) {
+ if (is_carry_connection(from, to))
+ game.move.did_carry = 1;
+ else
+ game.move.type = 'land'
+ } else {
+ game.move.type = 'land'
+ }
+ }
+ }
+
+ if (game.move.type === 'land') {
+ const from_ff = has_friendly_fortifications_or_cultivated(from);
+ const to_ff = has_friendly_fortifications_or_cultivated(to);
+ const has_dt = force_has_drilled_troops(who);
+ const has_ax = force_has_auxiliary_unit(who);
+
+ // Must stop on mountains.
+ if (is_mountain(to) && !to_ff)
+ stop_move();
+
+ // Must stop in the next space after passing through...
+ if (game.move.used > 1 && !from_ff) {
+ // Drilled Troops that pass through wilderness must stop in the next space.
+ if (has_dt && !has_ax && is_wilderness(to))
+ if (!game.events.george_croghan)
+ stop_move();
+
+ // Auxiliaries that pass through enemy cultivated must stop in the next space.
+ if (has_ax && !has_dt && is_originally_enemy(to))
+ stop_move();
+ }
+ }
+
+ if (has_enemy_stockade(to)) {
+ // TODO: Infiltrate
+ stop_move();
+ }
+
+ if (has_unbesieged_enemy_fort_or_fortress(to)) {
+ // TODO: Infiltrate
+ stop_move();
}
+
+ move_piece_to(who, to);
+ lift_sieges_and_amphib();
+}
+
+function apply_naval_move(to) {
+ let who = moving_piece();
+ let from = moving_piece_space();
+ game.move.used = 1;
+ game.move.came_from = from;
+ game.raid.from[to] = from; // remember where raiders came from so they can retreat after battle
}
states.move = {
prompt() {
let who = moving_piece();
let from = piece_space(who);
- view.prompt = "Move " + piece_name(who) + ".";
- view.who = who;
- switch (game.move.type) {
- default: view.prompt += " Select a movement type."; break;
- case 'boat': view.prompt += " (boat)"; break;
- case 'land': view.prompt += " (land)"; break;
- case 'naval': view.prompt += " (naval)"; break;
+
+ view.prompt = `Move ${piece_name(who)} (${space_name(from)})`;
+ if (game.move.type === 'boat') {
+ if (game.move.used < land_movement_cost())
+ view.prompt += " by boat or land";
+ else
+ view.prompt += " by boat";
+ if (game.move.did_carry)
+ view.prompt += " (carried)";
+ } else {
+ view.prompt += ` by ${game.move.type}`;
}
- if (game.move.start_cost === 0) {
+ view.prompt += ` \u2014 ${game.move.used}/${max_movement_cost()}.`;
+
+ view.who = who;
+ if (game.move.used === 0) {
if (game.events.foul_weather && can_moving_force_siege_or_assault()) {
if (is_assault_possible(from))
gen_action('assault');
@@ -3037,32 +3078,31 @@ states.move = {
if (force_has_drilled_troops(who))
gen_action('play_event', GEORGE_CROGHAN);
}
- if (!is_only_port_space(from)) {
- gen_action_x('boat_move', game.move.type !== 'boat');
- gen_action_x('land_move', game.move.type !== 'land');
- }
if (is_port(from)) {
- // TODO: check valid destinations too
- if (may_naval_move(who))
- gen_action_x('naval_move', game.move.type !== 'naval');
- if (!game.events.no_amphib) {
- if (game.active === BRITAIN && has_amphibious_arrow(from)) {
- for (let card = first_amphib_card; card <= last_amphib_card; ++card)
- if (player.hand.includes(card))
- gen_action('play_event', card);
+ if (game.move.type !== 'naval') {
+ gen_action('naval_move');
+ } else {
+ // TODO: split to naval_move state
+ if (!game.events.no_amphib) {
+ if (game.active === BRITAIN && has_amphibious_arrow(from)) {
+ for (let card = first_amphib_card; card <= last_amphib_card; ++card)
+ if (player.hand.includes(card))
+ gen_action('play_event', card);
+ }
}
}
}
}
gen_action_next();
gen_action_demolish();
- if (game.move.cost) {
- for (let space_id in game.move.cost) {
- space_id = space_id | 0;
- if (space_id !== from)
- gen_action_space(space_id);
- }
+
+ if (game.move.used < max_movement_cost()) {
+ if (game.move.type === 'naval')
+ gen_naval_move();
+ else
+ gen_regular_move();
}
+
if (is_leader(who)) {
for_each_piece_in_force(who, p => {
if (p !== who)
@@ -3080,38 +3120,18 @@ states.move = {
game.state = 'amphibious_landing';
}
},
- boat_move() {
- game.move.type = 'boat';
- resume_move();
- },
- land_move() {
- game.move.type = 'land';
- resume_move();
- },
naval_move() {
+ push_undo();
game.move.type = 'naval';
resume_move();
},
space(to) {
push_undo();
- print_path(game.move.path, to, game.move.start_cost === 0);
- let who = moving_piece();
- let cost = game.move.cost[to];
- game.move.start_cost = game.move.cost[to];
-
- let from = game.move.path[to];
-
- // remember where we came from so we can retreat after battle
- game.raid.from[to] = from;
-
- // TODO: except space moved into, if it is guarded or lake schooner happens!
- if (force_has_drilled_troops(who))
- remove_enemy_forts_uc_in_path(game.move.path, to);
- move_piece_to(who, to);
- lift_sieges_and_amphib();
+ apply_move(to);
if (is_enemy_card_available(LAKE_SCHOONER)) {
+ let from = moving_piece_came_from();
if (has_enemy_fortifications(to) && is_lake_connection(from, to)) {
clear_undo();
set_active(enemy());
@@ -3140,17 +3160,7 @@ states.move = {
demolish_stockade: goto_demolish_stockade,
demolish_fieldworks: goto_demolish_fieldworks,
next() {
- let where = moving_piece_space();
- if (game.move.infiltrate[where]) {
- game.move.infiltrate = {};
- game.move.start_cost = 99;
- if (has_unbesieged_enemy_units(where))
- goto_avoid_battle();
- else
- end_move();
- } else {
- end_move();
- }
+ end_move();
},
}
@@ -3169,12 +3179,12 @@ states.foul_weather = {
play_event(c) {
play_card(c);
game.events.foul_weather = 1;
- game.move.start_cost = 0;
+ game.move.used = 0;
set_active(enemy());
resume_move();
},
pass() {
- game.move.start_cost = 0;
+ game.move.used = 0;
set_active(enemy());
resume_move();
}
@@ -3184,7 +3194,7 @@ states.lake_schooner = {
prompt() {
let p = moving_piece();
let to = piece_space(p);
- let from = game.move.path[to];
+ let from = moving_piece_came_from();
view.who = p;
view.where = from;
if (player.hand.includes(LAKE_SCHOONER)) {
@@ -3198,11 +3208,10 @@ states.lake_schooner = {
play_event(c) {
play_card(c);
let who = moving_piece();
- let to = piece_space(who);
- let from = game.move.path[to];
+ let from = moving_piece_came_from();
move_piece_to(who, from);
log(`${piece_name(who)} stops in ${space_name(from)}.`);
- game.move.start_cost = 18; // enemy stops in previously occupied space
+ stop_move();
set_active(enemy());
resume_move();
},
@@ -3233,7 +3242,7 @@ states.amphibious_landing = {
let who = moving_piece();
let from = piece_space(who);
game.move.path[to] = from;
- game.move.start_cost = 1;
+ stop_move();
move_piece_to(who, to);
lift_sieges_and_amphib();
game.state = 'move';
@@ -3272,10 +3281,8 @@ function end_move_step(final) {
console.log("END MOVE STEP");
delete game.battle;
game.move.did_attempt_intercept = 0; // reset flag for next move step
- if (game.move.infiltrate[where])
- return resume_move();
if (has_unbesieged_enemy_fortifications(where)) {
- game.move.start_cost = 99;
+ stop_move();
if (has_enemy_fort(where) || is_fortress(where)) {
place_siege_marker(where);
}
@@ -3287,7 +3294,7 @@ function end_move_step(final) {
}
}
} else if (final) {
- game.move.start_cost = 99;
+ stop_move();
}
if (!is_lone_leader(who) && has_unbesieged_enemy_leader(where))
goto_retreat_lone_leader();
@@ -3322,13 +3329,113 @@ function end_move() {
// INTERCEPT
+function can_be_intercepted() {
+ let result = false;
+
+ let who = moving_piece();
+ let here = moving_piece_space();
+ let came_from = moving_piece_came_from();
+
+ // 6.723 Leaders moving alone can NOT be intercepted
+ if (is_lone_leader(who))
+ return false;
+
+ const is_lone_ax = is_lone_auxiliary(who);
+
+ // TODO: Infiltrate in same space
+ // 6.721 exception -- can always intercept units infiltrating same space
+
+console.log("INTERCEPT CHECK", piece_name(who), space_name(here), space_name(came_from), is_lone_ax);
+
+ for_each_exit(here, from => {
+ // 6.724 may not intercept an enemy leaving their own space
+ if (from === came_from)
+ return; // continue
+
+ // 6.722
+ if (has_non_moving_unbesieged_friendly_units(here))
+ return; // continue
+ if (has_unbesieged_friendly_fortifications(here))
+ return; // continue
+
+ // 6.721
+ if (is_lone_ax && is_wilderness_or_mountain(here)) {
+ if (has_unbesieged_enemy_auxiliary(from)) {
+ console.log("INTERCEPT TO", space_name(here), "(lone ax)");
+ result = true;
+ }
+ } else {
+ console.log("INTERCEPT TO", space_name(here));
+ result = true;
+ }
+ });
+
+console.log("CAN INTERCEPT", result);
+
+ return result;
+}
+
+function gen_intercept(is_lone_ax, to) {
+ if (has_unbesieged_enemy_units(to)) {
+ // 6.721 exception -- can always intercept units infiltrating same space
+ // TODO: infiltration
+ /*
+ for_each_friendly_piece_in_space(to, p => {
+ // TODO: unbesieged
+ gen_action_piece(p);
+ });
+ */
+
+ for_each_exit(to, from => {
+ // 6.721
+ if (is_lone_ax && is_wilderness_or_mountain(to)) {
+ let has_ax = false;
+ let has_br_indians = false;
+ for_each_friendly_unit_in_space(from, p => {
+ if (is_piece_unbesieged(p)) {
+ if (is_auxiliary_unit(p)) {
+ gen_action_piece(p);
+ if (is_british_iroquois_or_mohawk(p))
+ has_br_indians = true;
+ else
+ has_ax = true;
+ }
+ }
+ });
+ // allow leaders to accompany intercepting auxiliary unit
+ if (has_ax) {
+ for_each_friendly_leader_in_space(from, p => {
+ if (is_piece_unbesieged(p))
+ gen_action_piece(p);
+ });
+ } else if (has_br_indians) {
+ if (is_piece_in_space(JOHNSON, from)) {
+ if (is_piece_unbesieged(JOHNSON))
+ gen_action_piece(JOHNSON);
+ }
+ }
+ } else {
+ for_each_friendly_piece_in_space(from, p => {
+ if (is_piece_unbesieged(p))
+ gen_action_piece(p);
+ });
+ }
+ });
+ }
+}
+
function goto_intercept() {
- let where = moving_piece_space();
- if (where in game.move.intercept) {
+ let who = moving_piece();
+
+ let here = moving_piece_space();
+ if (force_has_drilled_troops(who))
+ remove_enemy_forts_uc_in_path(here);
+
+ if (can_be_intercepted()) {
clear_undo();
set_enemy_active('intercept_who');
} else {
- goto_declare_inside(where);
+ goto_declare_inside();
}
}
@@ -3426,8 +3533,6 @@ function end_intercept_success() {
function goto_declare_inside() {
let where = moving_piece_space();
- if (game.move.infiltrate[where])
- return end_move_step(false);
if (has_unbesieged_enemy_units_that_did_not_intercept(where)) {
if (is_fortress(where) || has_enemy_fort(where)) {
console.log("DECLARE INSIDE/OUTSIDE");
@@ -3558,7 +3663,7 @@ function attempt_avoid_battle() {
function can_enemy_avoid_battle(from) {
let can_avoid = false;
for_each_exit(from, to => {
- if ((moving_piece_came_from(from) !== to)
+ if ((moving_piece_came_from() !== to)
&& !has_unbesieged_friendly_units(to)
&& !has_unbesieged_friendly_fortifications(to))
can_avoid = true;
@@ -3581,7 +3686,7 @@ states.avoid_to = {
view.prompt = "Select where to avoid battle to.";
gen_action_pass();
for_each_exit(from, to => {
- if ((moving_piece_came_from(from) !== to)
+ if ((moving_piece_came_from() !== to)
&& !has_unbesieged_enemy_units(to)
&& !has_unbesieged_enemy_fortifications(to))
gen_action_space(to);
@@ -4548,7 +4653,7 @@ function determine_winner_battle() {
} else {
log("DEFENDER WON");
let from = game.battle.where;
- let to = moving_piece_came_from(game.battle.where);
+ let to = moving_piece_came_from();
retreat_attacker(from, to);
}
}
@@ -4695,12 +4800,12 @@ function goto_retreat_defender() {
}
function can_defender_retreat_from_to(p, from, to) {
- console.log("RETREAT QUERY", piece_name(p), space_name(from), space_name(to), "atk came from", moving_piece_came_from(from));
+ console.log("RETREAT QUERY", piece_name(p), space_name(from), space_name(to), "atk came from", moving_piece_came_from());
if (has_unbesieged_enemy_units(to))
return false;
if (has_unbesieged_enemy_fortifications(to))
return false;
- if (moving_piece_came_from(from) === to)
+ if (moving_piece_came_from() === to)
return false;
if (force_has_drilled_troops(p)) {
if (is_cultivated(to) || has_friendly_fortifications(to))
@@ -5901,9 +6006,12 @@ function goto_massacre(st) {
}
function massacre_prompt() {
- view.prompt = `You may play "Massacre!".`;
- if (player.hand.includes(MASSACRE))
+ if (player.hand.includes(MASSACRE)) {
+ view.prompt = `You may play "Massacre!".`;
gen_action('play_event', MASSACRE);
+ } else {
+ view.prompt = `You don't have "Massacre!".`;
+ }
gen_action_next();
}