summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2021-06-21 17:55:25 +0200
committerTor Andersson <tor@ccxvii.net>2022-11-16 19:19:38 +0100
commitefa3fcb3353053a00475d0562c858e0a62e5139a (patch)
treee0349e5c24e282db9056591a34f8dd1e858e94a0
parent5671af5f24c9e43b6620a929aa140b9b85a275f2 (diff)
downloadcrusader-rex-efa3fcb3353053a00475d0562c858e0a62e5139a.tar.gz
crusader: Field battles.
-rw-r--r--data.js33
-rw-r--r--rules.js755
2 files changed, 436 insertions, 352 deletions
diff --git a/data.js b/data.js
index 17f9eed..5620bd2 100644
--- a/data.js
+++ b/data.js
@@ -148,43 +148,42 @@ const PORTS = [];
army(rc, "Saracen", name, home, move, steps, combat, order, plural);
}
- frank(13, "Barbarossa", "Germania", 2, 4, "B3", "Crusaders", 0);
- frank(23, "Frederik", "Germania", 2, 3, "B2", "Crusaders", 0);
- frank(33, "Leopold", "Germania", 2, 3, "B3", "Crusaders", 0);
frank(11, "Richard", "England", 3, 4, "B4", "Crusaders", 0);
- frank(21, "Robert", "Normandy", 2, 3, "B3", "Crusaders", 0);
- frank(31, "Crossbows", "Aquitaine", 2, 3, "A2", "Crusaders", 1);
-
frank(12, "Philippe", "France", 2, 4, "B3", "Crusaders", 0);
- frank(22, "Hugues", "Bourgogne", 2, 4, "B2", "Crusaders", 0);
- frank(32, "Fileps", "Flanders", 2, 3, "B3", "Crusaders", 0);
-
- frank(42, "Pilgrims", "Genoa", 2, 4, "C2", "Pilgrims", 1);
- frank(43, "Pilgrims", "Sicily", 2, 3, "C2", "Pilgrims", 1);
- frank(52, "Pilgrims", "Brittany", 2, 4, "C2", "Pilgrims", 1);
-
+ frank(13, "Barbarossa", "Germania", 2, 4, "B3", "Crusaders", 0);
frank(14, "Templars", "Jerusalem", 3, 3, "B3", "Military Orders", 1);
frank(15, "Templars", "Antioch", 3, 3, "B3", "Military Orders", 1);
frank(16, "Templars", "Gaza", 3, 3, "B3", "Military Orders", 1);
- frank(17, "Templars", "Tartus", 3, 3, "B3", "Military Orders", 1);
+ frank(17, "Templars", "Tartus", 3, 2, "B3", "Military Orders", 1);
+
+ frank(21, "Robert", "Normandy", 2, 3, "B3", "Crusaders", 0);
+ frank(22, "Hugues", "Bourgogne", 2, 4, "B2", "Crusaders", 0);
+ frank(23, "Frederik", "Germania", 2, 3, "B2", "Crusaders", 0);
frank(24, "Hospitallers", "Jerusalem", 3, 4, "B3", "Military Orders", 1);
frank(25, "Hospitallers", "Acre", 3, 3, "B3", "Military Orders", 1);
frank(26, "Hospitallers", "Krak", 3, 2, "B3", "Military Orders", 1);
-
frank(27, "Reynald", "Sidon", 2, 3, "B2", "Outremers", 0);
+
+ frank(31, "Crossbows", "Aquitaine", 2, 3, "A2", "Crusaders", 1);
+ frank(32, "Fileps", "Flanders", 2, 3, "B3", "Crusaders", 0);
+ frank(33, "Leopold", "Germania", 2, 3, "B3", "Crusaders", 0);
frank(34, "Conrad", "Tyre", 2, 4, "B3", "Outremers", 0);
frank(35, "Balian", "Nablus", 2, 3, "B2", "Outremers", 0);
frank(36, "Walter", "Caesarea", 2, 3, "B2", "Outremers", 0);
frank(37, "Raymond", "Tiberias", 2, 3, "B2", "Outremers", 0);
+
+ frank(41, "Turcopole", "Antioch", 3, 3, "A2", "Turcopoles", 0);
+ frank(42, "Pilgrims", "Genoa", 2, 4, "C2", "Pilgrims", 1);
+ frank(43, "Pilgrims", "Sicily", 2, 3, "C2", "Pilgrims", 1);
frank(44, "King Guy", "Jerusalem", 2, 4, "B2", "Outremers", 0);
frank(45, "Reynald", "Kerak", 3, 2, "B3", "Outremers", 0);
frank(46, "Bohemond", "Antioch", 2, 4, "B2", "Outremers", 0);
frank(47, "Raymond", "Tripoli", 2, 4, "B2", "Outremers", 0);
- frank(53, "Josselin", "Saone", 2, 3, "B2", "Outremers", 0);
- frank(41, "Turcopole", "Antioch", 3, 3, "A2", "Turcopoles", 0);
frank(51, "Turcopole", "Beirut", 3, 3, "A2", "Turcopoles", 0);
+ frank(52, "Pilgrims", "Brittany", 2, 4, "C2", "Pilgrims", 1);
+ frank(53, "Josselin", "Saone", 2, 3, "B2", "Outremers", 0);
army(54, "Assassins", "Assassins", "Masyaf", 0, 3, "A3", "Assassins", 1);
diff --git a/rules.js b/rules.js
index dea8d08..bf67092 100644
--- a/rules.js
+++ b/rules.js
@@ -6,6 +6,8 @@
// TODO: optional rule - iron bridge
// TODO: optional rule - force marches
+// TODO: strict move order for group moves?
+
exports.scenarios = [
"Third Crusade"
];
@@ -25,6 +27,7 @@ const S_POOL = "SP";
// serif cirled numbers
const DIE_HIT = [ 0, '\u2776', '\u2777', '\u2778', '\u2779', '\u277A', '\u277B' ];
const DIE_MISS = [ 0, '\u2460', '\u2461', '\u2462', '\u2463', '\u2464', '\u2465' ];
+const DIE_SELF = '\u2465!';
const ATTACK_MARK = "*";
const RESERVE_MARK_1 = "\u2020";
@@ -242,7 +245,10 @@ function is_block_on_map(who) {
}
function can_activate(who) {
- return block_owner(who) == game.active && is_block_on_map(who) && !game.moved[who];
+ return block_owner(who) == game.active &&
+ is_block_on_map(who) &&
+ !is_block_in_castle(who) &&
+ !game.moved[who];
}
function road_id(a, b) {
@@ -303,12 +309,22 @@ function count_enemy_in_field(where) {
return count;
}
-function count_enemy_excluding_reserves(where) {
+function count_friendly_in_field_excluding_reserves(where) {
+ let p = game.active;
+ let count = 0;
+ for (let b in BLOCKS)
+ if (game.location[b] == where && block_owner(b) == p)
+ if (!is_block_in_castle(b) && !is_battle_reserve(b))
+ ++count;
+ return count;
+}
+
+function count_enemy_in_field_excluding_reserves(where) {
let p = ENEMY[game.active];
let count = 0;
for (let b in BLOCKS)
if (game.location[b] == where && block_owner(b) == p)
- if (!is_battle_reserve(b))
+ if (!is_block_in_castle(b) && !is_battle_reserve(b))
++count;
return count;
}
@@ -341,15 +357,8 @@ function is_friendly_port(where) {
return TOWNS[where].port && is_friendly_town(where);
}
-function have_contested_towns() {
- for (let where in TOWNS)
- if (is_contested_town(where))
- return true;
- return false;
-}
-
function count_pinning(where) {
- return count_enemy_excluding_reserves(where);
+ return count_enemy_in_field_excluding_reserves(where);
}
function count_pinned(where) {
@@ -429,7 +438,7 @@ function can_block_sea_move(who) {
}
function can_block_continue(who, from, to) {
- if (is_contested_town(to))
+ if (is_contested_field(to))
return false;
if (game.distance >= block_move(who))
return false;
@@ -437,7 +446,7 @@ function can_block_continue(who, from, to) {
}
function can_block_retreat_to(who, to) {
- if (is_friendly_town(to) || is_vacant_town(to)) {
+ if (is_friendly_field(to) || is_vacant_town(to)) {
let from = game.location[who];
if (can_block_use_road(from, to)) {
if (road_was_last_used_by_enemy(from, to))
@@ -459,7 +468,7 @@ function can_block_retreat(who) {
}
function can_block_regroup_to(who, to) {
- if (is_friendly_town(to) || is_vacant_town(to)) {
+ if (is_friendly_field(to) || is_vacant_town(to)) {
let from = game.location[who];
if (can_block_use_road(from, to))
return true;
@@ -478,7 +487,7 @@ function can_block_regroup(who) {
}
function can_block_use_road_to_muster(from, to) {
- return can_block_use_road(from, to) && is_friendly_or_vacant_town(to);
+ return can_block_use_road(from, to) && is_friendly_or_vacant_field(to);
}
function can_block_muster_with_3_moves(n0, muster) {
@@ -558,6 +567,24 @@ function is_defender(who) {
return false;
}
+function is_field_attacker(who) {
+ if (game.location[who] == game.where && block_owner(who) == game.attacker[game.where])
+ return !is_battle_reserve(who) && !is_block_in_castle(who);
+ return false;
+}
+
+function is_field_defender(who) {
+ if (game.location[who] == game.where && block_owner(who) != game.attacker[game.where])
+ return !is_battle_reserve(who) && !is_block_in_castle(who);
+ return false;
+}
+
+function is_field_combatant(who) {
+ if (game.location[who] == game.where)
+ return !is_battle_reserve(who) && !is_block_in_castle(who);
+ return false;
+}
+
function disband(who) {
if (block_plural(who))
log(block_name(who) + " disband.");
@@ -587,22 +614,6 @@ function reduce_block(who) {
}
}
-function count_attackers() {
- let count = 0;
- for (let b in BLOCKS)
- if (is_attacker(b))
- ++count;
- return count;
-}
-
-function count_defenders() {
- let count = 0;
- for (let b in BLOCKS)
- if (is_defender(b))
- ++count;
- return count;
-}
-
// GAME TURN
function start_year() {
@@ -711,7 +722,9 @@ function reveal_cards() {
let fp = fc.moves;
let sp = sc.moves;
if (fp == sp) {
- if (roll_d6() > 3)
+ let die = roll_d6();
+ log("Random first player.");
+ if (die > 3)
++fp;
else
++sp;
@@ -766,7 +779,7 @@ function move_block(who, from, to) {
game.location[who] = to;
game.road_limit[road_id(from, to)] = road_limit(from, to) + 1;
game.distance ++;
- if (is_contested_town(to)) {
+ if (is_contested_field(to)) {
game.last_used[road_id(from, to)] = game.active;
if (!game.attacker[to]) {
game.attacker[to] = game.active;
@@ -808,8 +821,8 @@ function goto_move_phase(moves) {
}
function end_move_phase() {
- game.moves = 0;
clear_undo();
+ game.moves = 0;
print_turn_log(game.active + " moves:");
end_player_turn();
}
@@ -901,6 +914,7 @@ states.group_move_to = {
function end_move() {
if (game.distance > 0) {
+ lift_siege(game.origin);
let to = game.location[game.who];
if (!game.activated.includes(game.origin)) {
logp("activates " + game.origin + ".");
@@ -965,6 +979,8 @@ states.sea_move_to = {
log_move_start(from);
log_move_continue("Sea");
+ lift_siege(from);
+
// English Crusaders attack!
if (is_contested_town(to)) {
game.attacker[to] = FRANK;
@@ -985,7 +1001,7 @@ function can_muster_anywhere() {
if (game.moves > 0)
return true;
for (let where of game.mustered) {
- if (is_friendly_town(where))
+ if (is_friendly_field(where))
if (can_muster_to(where))
return true;
}
@@ -1001,13 +1017,13 @@ states.muster = {
gen_action(view, 'end_muster');
if (game.moves > 0) {
for (let where in TOWNS) {
- if (is_friendly_town(where))
+ if (is_friendly_field(where))
if (can_muster_to(where))
gen_action(view, 'town', where);
}
} else {
for (let where of game.mustered) {
- if (is_friendly_town(where))
+ if (is_friendly_field(where))
if (can_muster_to(where))
gen_action(view, 'town', where);
}
@@ -1077,6 +1093,7 @@ states.muster_move_1 = {
log_move_start(from);
log_move_continue(to);
move_block(game.who, from, to);
+ lift_siege(from);
if (to == game.where) {
end_muster_move();
} else {
@@ -1177,7 +1194,7 @@ function count_blocks_in_castle(where) {
return n;
}
-function count_enemies_in_field_and_reserve(where) {
+function count_enemy_in_field_and_reserve(where) {
let n = 0;
for (let b in BLOCKS)
if (block_owner(b) != game.active)
@@ -1186,7 +1203,7 @@ function count_enemies_in_field_and_reserve(where) {
return n;
}
-function count_friends_in_field_and_reserve(where) {
+function count_friendly_in_field_and_reserve(where) {
let n = 0;
for (let b in BLOCKS)
if (block_owner(b) == game.active)
@@ -1195,6 +1212,12 @@ function count_friends_in_field_and_reserve(where) {
return n;
}
+function is_contested_battle_field() {
+ let f = count_friendly_in_field_excluding_reserves(game.where);
+ let e = count_enemy_in_field_excluding_reserves(game.where);
+ return f > 0 && e > 0;
+}
+
function count_reserves(where) {
let n = 0;
for (let b in BLOCKS)
@@ -1227,9 +1250,35 @@ function besieging_player(where) {
return ENEMY[besieged_player(where)];
}
+function lift_siege(where) {
+ if (is_under_siege(where) && !is_contested_town(where)) {
+ log("Siege lifted in " + where + ".");
+ console.log("SIEGE LIFTED IN", where);
+ for (let b in BLOCKS)
+ if (is_block_in_castle_in(b, where))
+ remove_from_array(game.castle, b);
+ }
+}
+
+function lift_all_sieges() {
+ for (let t in TOWNS)
+ lift_siege(t);
+}
+
function goto_combat_phase() {
game.moved = {};
- if (have_contested_towns()) {
+ game.combat_list = [];
+ for (let where in TOWNS)
+ if (is_contested_town(where))
+ game.combat_list.push(where);
+ resume_combat_phase();
+}
+
+function resume_combat_phase() {
+ reset_road_limits();
+ game.moved = {};
+
+ if (game.combat_list.length > 0) {
game.active = game.p1;
game.state = 'combat_phase';
} else {
@@ -1240,13 +1289,13 @@ function goto_combat_phase() {
states.combat_phase = {
prompt: function (view, current) {
if (is_inactive_player(current))
- return view.prompt = "Waiting for " + game.active + " to choose a battle or siege.";
+ return view.prompt = "Waiting for " + game.active + " to choose the next battle or siege.";
view.prompt = "Choose the next battle or siege!";
- for (let where in TOWNS)
- if (is_contested_town(where))
- gen_action(view, 'town', where);
+ for (let where of game.combat_list)
+ gen_action(view, 'town', where);
},
town: function (where) {
+ remove_from_array(game.combat_list, where);
start_combat(where);
},
}
@@ -1269,23 +1318,17 @@ function start_combat(where) {
game.active = ENEMY[game.attacker[game.where]];
game.state = 'combat_deployment';
} else {
+ game.attacker[game.where] = besieging_player(game.where);
console.log("CONTINUE SIEGE");
log("Siege continues from previous turn.");
- resume_combat();
+ next_combat_round();
}
} else {
console.log("START NON-SIEGE");
- resume_combat();
+ next_combat_round();
}
}
-function lift_siege(where) {
- console.log("SIEGE LIFTED IN", where);
- for (let b in BLOCKS)
- if (is_block_in_castle_in(b, game.where))
- remove_from_array(game.castle, b);
-}
-
function end_combat() {
console.log("END COMBAT IN", game.where);
@@ -1296,14 +1339,13 @@ function end_combat() {
lift_siege(game.where);
}
+ delete game.storming;
+ delete game.sallying;
game.where = null;
game.flash = "";
game.battle_round = 0;
- reset_road_limits();
- game.moved = {};
- delete game.storming;
- delete game.sallying;
- goto_combat_phase();
+
+ resume_combat_phase();
}
// COMBAT DEPLOYMENT
@@ -1312,7 +1354,7 @@ states.combat_deployment = {
show_battle: true,
prompt: function (view, current) {
if (is_inactive_player(current))
- return view.prompt = "Waiting for " + game.active + " to deploy units.";
+ return view.prompt = "Waiting for " + game.active + " to deploy blocks.";
view.prompt = "Deploy blocks on the field and in the castle.";
let max = castle_limit(game.where);
let n = count_blocks_in_castle(game.where);
@@ -1341,15 +1383,15 @@ states.combat_deployment = {
clear_undo();
let n = count_blocks_in_castle(game.where);
if (n == 1)
- log("1 unit withdraws.");
+ log(game.active + " withdraws 1 block.");
else
- log(n + " units withdraw.");
+ log(game.active + " withdraws " + n + " blocks.");
game.active = game.attacker[game.where];
- if (count_enemies_in_field_and_reserve(game.where) == 0) {
+ if (count_enemy_in_field_and_reserve(game.where) == 0) {
console.log("DEFENDER REFUSED FIELD BATTLE");
return goto_regroup();
}
- resume_combat();
+ next_combat_round();
},
undo: pop_undo
}
@@ -1357,6 +1399,7 @@ states.combat_deployment = {
// REGROUP AFTER FIELD BATTLE/SIEGE VICTORY
function goto_regroup() {
+ lift_siege(game.where);
console.log("REGROUP", game.active);
reset_road_limits();
game.state = 'regroup';
@@ -1386,7 +1429,7 @@ states.regroup = {
clear_undo();
print_turn_log(game.active + " regroups:");
if (is_contested_town(game.where))
- resume_combat();
+ next_combat_round();
else
end_combat();
},
@@ -1417,8 +1460,8 @@ states.regroup_to = {
// COMBAT ROUND
-function resume_combat() {
- console.log("RESUME COMBAT");
+function next_combat_round() {
+ console.log("NEXT COMBAT ROUND");
switch (game.combat_round) {
case 0: return goto_combat_round(1);
case 1: return goto_combat_round(2);
@@ -1434,17 +1477,20 @@ function bring_on_reserves(reserves) {
}
function goto_combat_round(combat_round) {
- console.log("COMBAT ROUND", combat_round);
game.combat_round = combat_round;
+ game.moved = {};
+
+ console.log("COMBAT ROUND", combat_round);
+ log("~ Combat Round " + combat_round + " ~");
- let was_contested = is_contested_field(game.where);
+ let was_contested = is_contested_battle_field();
if (combat_round == 2)
bring_on_reserves(game.reserves1);
if (combat_round == 3)
bring_on_reserves(game.reserves2);
- if (is_contested_field(game.where)) {
+ if (is_contested_battle_field()) {
if (is_under_siege(game.where)) {
if (!was_contested) {
log("Relief forces arrive!");
@@ -1457,9 +1503,10 @@ function goto_combat_round(combat_round) {
}
let old_attacker = game.attacker[game.where];
game.attacker[game.where] = besieged_player(game.where);
- console.log("NEW ATTACKER IS", game.attacker[game.where]);
- if (old_attacker != game.attacker[game.where])
+ if (old_attacker != game.attacker[game.where]) {
+ console.log("NEW ATTACKER IS", game.attacker[game.where]);
log(game.attacker[game.where] + " is now the attacker.");
+ }
}
return goto_field_battle();
}
@@ -1510,9 +1557,9 @@ states.declare_storm = {
goto_declare_sally();
} else {
if (n == 1)
- log("1 unit storms the castle.");
+ log(game.active + " storms with 1 block.");
else
- log(n + " units storm the castle.");
+ log(game.active + " storms with " + n + " blocks.");
goto_storm_battle();
}
},
@@ -1522,7 +1569,7 @@ states.declare_storm = {
function goto_declare_sally() {
game.active = besieged_player(game.where);
game.state = 'declare_sally';
- game.was_contested = is_contested_field(game.where);
+ game.was_contested = is_contested_battle_field();
}
states.declare_sally = {
@@ -1556,19 +1603,21 @@ states.declare_sally = {
clear_undo();
let n = game.sallying.length;
console.log("SALLY DECLARATION", n);
- if (n == 1)
- log("1 unit sally.");
+ if (n == 0)
+ log(game.active + " declines to sally.");
+ else if (n == 1)
+ log(game.active + " sallies " + n + "block.");
else
- log(n + " units sally.");
- if (is_contested_field(game.where)) {
+ log(game.active + " sallies " + n + "blocks.");
+ if (is_contested_battle_field()) {
if (!game.was_contested) {
log(game.active + " is now the attacker.");
console.log("NEW ATTACKER IS", game.active);
- game.attacker[where] = game.active;
+ game.attacker[game.where] = game.active;
}
goto_field_battle();
} else if (count_reserves(game.where) > 0) {
- resume_combat();
+ next_combat_round();
} else {
goto_siege_attrition();
}
@@ -1580,89 +1629,110 @@ states.declare_sally = {
function goto_retreat_after_combat() {
console.log("RETREAT AFTER COMBAT");
-// withdraw all sallying units to castle.
-// withdraw all storming units to field.
-// if field is contested then
-// attacker must retreat
-// defender may regroup
-// end
- goto_siege_attrition();
-}
-// SIEGE ATTRITION
+ // withdraw all sallying blocks to castle.
+ for (let b of game.sallying)
+ game.castle.push(b);
+ game.sallying.length = 0;
-function goto_siege_attrition() {
- console.log("SIEGE ATTRITION");
-}
-
-// FIELD AND STORM BATTLE
+ // withdraw all storming blocks to the field.
+ game.storming.length = 0;
-function goto_field_battle() {
- console.log("FIELD BATTLE");
+ if (is_contested_field(game.where)) {
+ game.active = game.attacker[game.where];
+ game.state = 'retreat';
+ game.turn_log = [];
+ } else if (is_under_siege(game.where)) {
+ goto_siege_attrition();
+ } else {
+ end_combat();
+ }
}
-function goto_storm_battle() {
- console.log("STORM BATTLE");
+states.retreat = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to retreat.";
+ view.prompt = "Retreat: Choose an army to move.";
+ gen_action_undo(view);
+ let can_retreat = false;
+ for (let b in BLOCKS) {
+ if (game.location[b] == game.where && !is_block_in_castle(b) && can_block_retreat(b)) {
+ gen_action(view, 'block', b);
+ can_retreat = true;
+ }
+ }
+ if (!is_contested_field(game.where) || !can_retreat)
+ gen_action(view, 'end_retreat');
+ },
+ end_retreat: function () {
+ clear_undo();
+ for (let b in BLOCKS)
+ if (game.location[b] == game.where && !is_block_in_castle(b) && block_owner(b) == game.active)
+ eliminate_block(b);
+ print_turn_log(game.active + " retreats:");
+ game.active = ENEMY[game.active];
+ console.log("ATTACKER RETREATED FROM THE FIELD");
+ goto_regroup();
+ },
+ block: function (who) {
+ push_undo();
+ game.who = who;
+ game.state = 'retreat_to';
+ },
+ undo: pop_undo
}
-// OLD CRUFT
-
-/*
-
-function resume_battle() {
- game.who = null;
- if (game.victory)
- return goto_game_over();
- game.state = 'battle_round';
- pump_battle_round();
+states.retreat_to = {
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to retreat.";
+ view.prompt = "Retreat: Move the army to a friendly or neutral town.";
+ gen_action_undo(view);
+ gen_action(view, 'block', game.who);
+ let can_retreat = false;
+ for (let to of TOWNS[game.where].exits) {
+ if (can_block_retreat_to(game.who, to)) {
+ gen_action(view, 'town', to);
+ can_retreat = true;
+ }
+ }
+ if (!can_retreat)
+ gen_action(view, 'eliminate');
+ },
+ town: function (to) {
+ let from = game.where;
+ game.turn_log.push([from, to]);
+ move_block(game.who, game.where, to);
+ game.who = null;
+ game.state = 'retreat';
+ },
+ eliminate: function () {
+ eliminate_block(game.who);
+ game.who = null;
+ game.state = 'retreat';
+ },
+ block: pop_undo,
+ undo: pop_undo
}
-function end_battle() {
- game.flash = "";
- game.battle_round = 0;
- reset_road_limits();
- game.moved = {};
-
- game.active = game.attacker[game.where];
- let victor = game.active;
- if (is_contested_town(game.where))
- victor = ENEMY[game.active];
- else if (is_enemy_town(game.where))
- victor = ENEMY[game.active];
- log(victor + " wins the battle in " + game.where + "!");
+// SIEGE ATTRITION
- goto_retreat();
+function goto_siege_attrition() {
+ console.log("SIEGE ATTRITION");
+ end_combat();
}
-function start_battle_round() {
- if (++game.battle_round <= 3) {
- log("~ Battle Round " + game.battle_round + " ~");
-
- reset_road_limits();
- game.moved = {};
+// FIELD BATTLE
- if (game.battle_round == 2) {
- if (count_defenders() == 0) {
- log("Defending main force was eliminated.");
- log("The attacker is now the defender.");
- game.attacker[game.where] = ENEMY[game.attacker[game.where]];
- } else if (count_attackers() == 0) {
- log("Attacking main force was eliminated.");
- }
- bring_on_reserves(2);
- }
-
- if (game.battle_round == 3) {
- bring_on_reserves(3);
- }
-
- pump_battle_round();
- } else {
- end_battle();
- }
+function goto_field_battle() {
+ console.log("FIELD BATTLE");
+ resume_field_battle();
}
-function pump_battle_round() {
+function resume_field_battle() {
+ game.state = 'field_battle';
+
function filter_battle_blocks(ci, is_candidate) {
let output = null;
for (let b in BLOCKS) {
@@ -1677,6 +1747,21 @@ function pump_battle_round() {
return output;
}
+ game.active = game.p1;
+
+ if (is_friendly_field(game.where)) {
+ console.log("FIELD BATTLE WON BY", game.active);
+ log("Field battle won by", game.active);
+ return goto_regroup();
+ }
+
+ if (is_enemy_field(game.where)) {
+ game.active = ENEMY[game.active];
+ console.log("FIELD BATTLE WON BY", game.active);
+ log("Field battle won by", game.active + ".");
+ return goto_regroup();
+ }
+
function battle_step(active, initiative, candidate) {
game.battle_list = filter_battle_blocks(initiative, candidate);
if (game.battle_list) {
@@ -1686,37 +1771,127 @@ function pump_battle_round() {
return false;
}
- if (is_friendly_town(game.where) || is_enemy_town(game.where)) {
- end_battle();
- } else if (count_attackers() == 0 || count_defenders() == 0) {
- start_battle_round();
- } else {
- let attacker = game.attacker[game.where];
- let defender = ENEMY[attacker];
+ let attacker = game.attacker[game.where];
+ let defender = ENEMY[attacker];
- if (battle_step(defender, 'A', is_defender)) return;
- if (battle_step(attacker, 'A', is_attacker)) return;
- if (battle_step(defender, 'B', is_defender)) return;
- if (battle_step(attacker, 'B', is_attacker)) return;
- if (battle_step(defender, 'C', is_defender)) return;
- if (battle_step(attacker, 'C', is_attacker)) return;
+ if (battle_step(defender, 'A', is_field_defender)) return;
+ if (battle_step(attacker, 'A', is_field_attacker)) return;
+ if (battle_step(defender, 'B', is_field_defender)) return;
+ if (battle_step(attacker, 'B', is_field_attacker)) return;
+ if (battle_step(defender, 'C', is_field_defender)) return;
+ if (battle_step(attacker, 'C', is_field_attacker)) return;
- start_battle_round();
- }
+ next_combat_round();
}
-function retreat_with_block(b) {
- game.who = b;
- game.state = 'retreat_in_battle';
+states.field_battle = {
+ show_battle: true,
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to choose a combat action.";
+ view.prompt = "Field Battle: Choose a combat action.";
+ for (let b of game.battle_list) {
+ gen_action(view, 'block', b); // take default action
+ gen_action(view, 'battle_fire', b);
+ if (game.sallying.includes(b)) {
+ // Only sallying forces may withdraw into the castle
+ gen_action(view, 'battle_withdraw');
+ } else {
+ if (can_block_retreat(b)) {
+ gen_action(view, 'battle_retreat', b);
+ // Turcopoles and Nomads can Harry (fire and retreat)
+ if (block_type(b) == 'turcopoles' || block_type(b) == 'nomads')
+ gen_action(view, 'battle_harry', b);
+ }
+ }
+ // All Frank B blocks are knights who can Charge
+ if (block_owner(b) == FRANK && block_initiative(b) == 'B')
+ gen_action(view, 'battle_charge', b);
+ }
+ },
+ block: field_fire_with_block,
+ battle_fire: field_fire_with_block,
+ battle_withdraw: field_withdraw_with_block,
+ battle_charge: charge_with_block,
+ battle_harry: harry_with_block,
+ battle_retreat: retreat_with_block,
}
-function roll_attack(active, b, verb) {
+// STORM BATTLE
+
+function goto_storm_battle() {
+ log("TODO: storm battle");
+ console.log("STORM BATTLE");
+ next_combat_round();
+}
+
+// BATTLE HITS
+
+function goto_field_battle_hits() {
+ game.active = ENEMY[game.active];
+ game.battle_list = list_field_victims();
+ console.log("victim list = ", game.battle_list);
+ if (game.battle_list.length == 0)
+ resume_field_battle();
+ else
+ game.state = 'field_battle_hits';
+}
+
+function list_field_victims() {
+ let max = 0;
+ for (let b in BLOCKS)
+ if (block_owner(b) == game.active && is_field_combatant(b) && game.steps[b] > max)
+ max = game.steps[b];
+ let list = [];
+ for (let b in BLOCKS)
+ if (block_owner(b) == game.active && is_field_combatant(b) && game.steps[b] == max)
+ list.push(b);
+ return list;
+}
+
+states.field_battle_hits = {
+ show_battle: true,
+ prompt: function (view, current) {
+ if (is_inactive_player(current))
+ return view.prompt = "Waiting for " + game.active + " to assign hits.";
+ view.prompt = "Assign " + game.hits + (game.hits != 1 ? " hits" : " hit") + " to your armies.";
+ for (let b of game.battle_list) {
+ gen_action(view, 'battle_hit', b);
+ gen_action(view, 'block', b);
+ }
+ },
+ battle_hit: apply_field_battle_hit,
+ block: apply_field_battle_hit,
+}
+
+function apply_field_battle_hit(who) {
+ if (block_plural(who))
+ game.flash = block_name(who) + " take a hit.";
+ else
+ game.flash = block_name(who) + " takes a hit.";
+ reduce_block(who, 'combat');
+ game.hits--;
+ if (game.hits == 0)
+ resume_field_battle();
+ else {
+ game.battle_list = list_field_victims();
+ if (game.battle_list.length == 0)
+ resume_field_battle();
+ else
+ game.flash += " " + game.hits + (game.hits == 1 ? " hit left." : " hits left.");
+ }
+}
+
+// BATTLE ACTIONS
+
+function roll_attack(active, b, verb, is_charge) {
game.hits = 0;
let fire = block_fire_power(b, game.where);
let printed_fire = block_printed_fire_power(b);
let rolls = [];
let steps = game.steps[b];
let name = block_name(b) + " " + BLOCKS[b].combat;
+ let self = 0;
if (fire > printed_fire)
name += "+" + (fire - printed_fire);
for (let i = 0; i < steps; ++i) {
@@ -1725,7 +1900,12 @@ function roll_attack(active, b, verb) {
rolls.push(DIE_HIT[die]);
++game.hits;
} else {
- rolls.push(DIE_MISS[die]);
+ if (is_charge && die == 6) {
+ rolls.push(DIE_SELF);
+ ++self;
+ } else {
+ rolls.push(DIE_MISS[die]);
+ }
}
}
@@ -1746,192 +1926,92 @@ function roll_attack(active, b, verb) {
game.flash += "and scores " + game.hits + " hits.";
}
+ if (self > 0) {
+ if (self == 1)
+ game.flash += " " + self + " self hit.";
+ else
+ game.flash += " " + self + " self hits.";
+ self = Math.min(self, game.steps[b]);
+ while (self-- > 0)
+ reduce_block(b);
+ }
+
log(active[0] + ": " + name + " " + verb + " " + rolls.join("") + ".");
}
-function fire_with_block(b) {
+function field_fire_with_block(b) {
game.moved[b] = true;
- console.log ("fire", block_plural(b));
if (block_plural(b))
- roll_attack(game.active, b, "fire");
+ roll_attack(game.active, b, "fire", 0);
else
- roll_attack(game.active, b, "fires");
+ roll_attack(game.active, b, "fires", 0);
if (game.hits > 0) {
- game.active = ENEMY[game.active];
- goto_battle_hits();
+ goto_field_battle_hits();
} else {
- resume_battle();
+ resume_field_battle();
}
}
-states.battle_round = {
- show_battle: true,
- prompt: function (view, current) {
- if (is_inactive_player(current))
- return view.prompt = "Waiting for " + game.active + " to choose a combat action.";
- view.prompt = "Battle: Charge, Fire, Harry, or Retreat with an army.";
- for (let b of game.battle_list) {
- gen_action(view, 'block', b); // take default action
- gen_action(view, 'battle_fire', b);
- gen_action(view, 'battle_retreat', b);
- // Turcopoles and Nomads can Harry (fire and retreat)
- if (block_type(b) == 'turcopoles' || block_type(b) == 'nomads')
- gen_action(view, 'battle_harry', b);
- // All Frank B blocks are knights who can Charge
- if (block_owner(b) == FRANK && block_initiative(b) == 'B')
- gen_action(view, 'battle_charge', b);
- }
- },
- battle_charge: function (who) {
- charge_with_block(who);
- },
- battle_fire: function (who) {
- fire_with_block(who);
- },
- battle_harry: function (who) {
- harry_with_block(who);
- },
- battle_retreat: function (who) {
- retreat_with_block(who);
- },
- block: function (who) {
- fire_with_block(who);
- },
-}
-
-function goto_battle_hits() {
- game.battle_list = list_victims(game.active);
- if (game.battle_list.length == 0)
- resume_battle();
- else
- game.state = 'battle_hits';
-}
-
-function apply_hit(who) {
- if (block_plural(who))
- game.flash = block_name(who) + " take a hit.";
+function charge_with_block(b) {
+ game.moved[b] = true;
+ if (block_plural(b))
+ roll_attack(game.active, b, "charge", 1);
else
- game.flash = block_name(who) + " takes a hit.";
- reduce_block(who, 'combat');
- game.hits--;
- if (game.hits == 0)
- resume_battle();
- else {
- game.battle_list = list_victims(game.active);
- if (game.battle_list.length == 0)
- resume_battle();
- else
- game.flash += " " + game.hits + (game.hits == 1 ? " hit left." : " hits left.");
+ roll_attack(game.active, b, "charges", 1);
+ if (game.hits > 0) {
+ goto_field_battle_hits();
+ } else {
+ resume_field_battle();
}
}
-function list_victims(p) {
- let is_candidate = (p == game.attacker[game.where]) ? is_attacker : is_defender;
- let max = 0;
- for (let b in BLOCKS)
- if (is_candidate(b) && game.steps[b] > max)
- max = game.steps[b];
- let list = [];
- for (let b in BLOCKS)
- if (is_candidate(b) && game.steps[b] == max)
- list.push(b);
- return list;
-}
-
-states.battle_hits = {
- show_battle: true,
- prompt: function (view, current) {
- if (is_inactive_player(current))
- return view.prompt = "Waiting for " + game.active + " to assign hits.";
- view.prompt = "Assign " + game.hits + (game.hits != 1 ? " hits" : " hit") + " to your armies.";
- for (let b of game.battle_list) {
- gen_action(view, 'battle_hit', b);
- gen_action(view, 'block', b);
- }
- },
- battle_hit: function (who) {
- apply_hit(who);
- },
- block: function (who) {
- apply_hit(who);
- },
+function field_withdraw_with_block(b) {
+ log(game.active[0] + ": " + b + " withdraws.");
+ game.moved[b] = true;
+ remove_from_array(game.sallying, b);
+ game.castle.push(b);
+ resume_field_battle();
}
-function goto_retreat() {
- game.active = game.attacker[game.where];
- if (is_contested_town(game.where)) {
- game.state = 'retreat';
- game.turn_log = [];
- clear_undo();
- } else {
- clear_undo();
- goto_regroup();
- }
+function harry_with_block(b) {
+ // TODO: fire, hits, retreat OR fire, retreat, hits order ?
+ if (block_plural(b))
+ roll_attack(game.active, b, "harry", 1);
+ else
+ roll_attack(game.active, b, "harries", 1);
+ game.who = b;
+ game.state = 'harry';
}
-states.retreat = {
+states.harry = {
prompt: function (view, current) {
if (is_inactive_player(current))
- return view.prompt = "Waiting for " + game.active + " to retreat.";
- view.prompt = "Retreat: Choose an army to move.";
- gen_action_undo(view);
- let can_retreat = false;
- for (let b in BLOCKS) {
- if (game.location[b] == game.where && can_block_retreat(b)) {
- gen_action(view, 'block', b);
- can_retreat = true;
- }
- }
- if (!is_contested_town(game.where) || !can_retreat)
- gen_action(view, 'end_retreat');
- },
- end_retreat: function () {
- for (let b in BLOCKS)
- if (game.location[b] == game.where && block_owner(b) == game.active)
- eliminate_block(b);
- print_turn_log(game.active + " retreats:");
- clear_undo();
- goto_regroup();
- },
- block: function (who) {
- push_undo();
- game.who = who;
- game.state = 'retreat_to';
- },
- undo: pop_undo
-}
-
-states.retreat_to = {
- prompt: function (view, current) {
- if (is_inactive_player(current))
- return view.prompt = "Waiting for " + game.active + " to retreat.";
- view.prompt = "Retreat: Move the army to a friendly or neutral town.";
- gen_action_undo(view);
- gen_action(view, 'block', game.who);
- let can_retreat = false;
- for (let to of TOWNS[game.where].exits) {
- if (can_block_retreat_to(game.who, to)) {
+ return view.prompt = "Waiting for " + game.active + " to harry.";
+ view.prompt = "Harry: Move the army to a friendly or vacant town.";
+ for (let to of TOWNS[game.where].exits)
+ if (can_block_retreat_to(game.who, to))
gen_action(view, 'town', to);
- can_retreat = true;
- }
- }
- if (!can_retreat)
- gen_action(view, 'eliminate');
},
town: function (to) {
- let from = game.where;
- game.turn_log.push([from, to]);
+ if (block_plural(game.who))
+ game.flash = block_name(game.who) + " retreat.";
+ else
+ game.flash = block_name(game.who) + " retreats.";
+ logp("retreats to " + to + ".");
+ game.location[game.who] = to;
move_block(game.who, game.where, to);
game.who = null;
- game.state = 'retreat';
- },
- eliminate: function () {
- eliminate_block(game.who);
- game.who = null;
- game.state = 'retreat';
+ if (game.hits > 0) {
+ goto_field_battle_hits();
+ } else {
+ resume_field_battle();
+ }
},
- block: pop_undo,
- undo: pop_undo
+}
+
+function retreat_with_block(b) {
+ game.who = b;
+ game.state = 'retreat_in_battle';
}
states.retreat_in_battle = {
@@ -1952,25 +2032,30 @@ states.retreat_in_battle = {
game.flash = block_name(game.who) + " retreats.";
logp("retreats to " + to + ".");
game.location[game.who] = to;
- resume_battle();
- },
- eliminate: function () {
- eliminate_block(game.who);
- resume_battle();
+ move_block(game.who, game.where, to);
+ game.who = null;
+ resume_field_battle();
},
- block: function (to) {
- resume_battle();
+ block: function () {
+ game.who = null;
+ resume_field_battle();
},
undo: function () {
- resume_battle();
+ game.who = null;
+ resume_field_battle();
}
}
+// OLD CRUFT
+
+/*
+
*/
// DRAW PHASE
function goto_draw_phase() {
+ delete game.combat_list;
end_game_turn();
}