From efa3fcb3353053a00475d0562c858e0a62e5139a Mon Sep 17 00:00:00 2001 From: Tor Andersson Date: Mon, 21 Jun 2021 17:55:25 +0200 Subject: crusader: Field battles. --- data.js | 33 ++- rules.js | 755 +++++++++++++++++++++++++++++++++++---------------------------- 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(); } -- cgit v1.2.3