diff options
-rw-r--r-- | play.html | 3 | ||||
-rw-r--r-- | play.js | 49 | ||||
-rw-r--r-- | rules.js | 588 |
3 files changed, 369 insertions, 271 deletions
@@ -24,6 +24,9 @@ header.your_turn { background-color: orange; } .role_info { border-bottom: 1px solid black; } #log { background-color: ghostwhite; } +#log .h1 { background-color: silver; font-weight: bold; padding-top:2px; padding-bottom:2px; text-align: center; } +#log .h2 { background-color: gainsboro; padding-top:2px; padding-bottom:2px; text-align: center; } +#log .h3 { background-color: whitesmoke; padding-top:2px; padding-bottom:2px; text-align: center; } .role_info { padding: 15px; } @@ -624,6 +624,55 @@ function on_update() { action_button("undo", "Undo") } +function sub_hex_name(match, p1, offset, string) { + let x = p1 | 0 + let n = hex_name[x] + return `<span class="hex" onmouseenter="on_focus_hex_tip(${x})" onmouseleave="on_blur_hex_tip(${x})">${n}</span>` +} + +function sub_unit_name(match, p1, offset, string) { + let u = p1 | 0 + return units[u].name +} + +function on_log(text) { + let p = document.createElement("div") + text = text.replace(/&/g, "&") + text = text.replace(/</g, "<") + text = text.replace(/>/g, ">") + + text = text.replace(/#(\d+)/g, sub_hex_name) + text = text.replace(/%(\d+)/g, sub_unit_name) + + if (text.match(/^\.h1/)) { + text = text.substring(4) + p.className = 'h1' + } + if (text.match(/^\.h2/)) { + text = text.substring(4) + if (text.startsWith('Axis')) + p.className = 'h2 axis' + else if (text.startsWith('Allied')) + p.className = 'h2 allied' + else + p.className = 'h2' + } + if (text.match(/^\.h3/)) { + text = text.substring(4) + p.className = 'h3' + } + + if (text.indexOf("\n") < 0) { + p.innerHTML = text + } else { + text = text.split("\n") + p.appendChild(on_log_line(text[0])) + for (let i = 1; i < text.length; ++i) + p.appendChild(on_log_line(text[i], "indent")) + } + return p +} + drag_element_with_mouse("#battle", "#battle_header") drag_element_with_mouse("#pursuit", "#pursuit_header") scroll_with_middle_mouse("main") @@ -1,5 +1,7 @@ "use strict" +// TODO: search_path with actual unit speed + // TODO: partial moves during regroup (to allow deciding entry hex-side) // TODO: first_friendly_unit / for_each_friendly_unit @@ -10,6 +12,46 @@ // unit state: location (8 bits), supply source (3 bits), steps (2 bits), disrupted (1 bit) +// SEQUENCE +// -------- +// Supply check +// Turn option +// Movement +// declare moves +// normal moves +// rout +// retreats +// declare full/partial retreats +// probe combat +// pursuit fire +// withdraw +// rout +// pursuit fire +// withdraw +// forced marches +// rout +// pursuit fire +// withdraw +// refuse battle +// pursuit fire +// withdraw +// rout +// pursuit fire +// withdraw +// Combat +// declare active +// declare assault +// resolve +// defensive fire +// rout +// offensive fire +// rout +// Blitz Movement +// Blitz Combat +// Final supply check +// rout +// Reveal supply cards -> next player + const max = Math.max const min = Math.min const abs = Math.abs @@ -449,6 +491,12 @@ function has_undisrupted_enemy_unit(x) { return has_undisrupted_allied_unit(x) } +function is_new_battle_hex(a) { + if (is_battle_hex(a)) + return !set_has(game.axis_hexes, a) && !set_has(game.allied_hexes, a) + return false +} + function claim_hexside_control(side) { if (game.active === AXIS) { set_add(game.axis_sides, side) @@ -472,12 +520,6 @@ function release_hex_control(a) { }) } -function is_new_battle_hex(a) { - if (is_battle_hex(a)) - return !set_has(game.axis_hexes, a) && !set_has(game.allied_hexes, a) - return false -} - function claim_hex_control_for_defender(a) { // a new battle hex: claim hex and hexsides for defender @@ -595,6 +637,15 @@ function count_hp_in_battle() { return hp } +function count_hp_in_battle_of_class(tc) { + let hp = 0 + for_each_undisrupted_enemy_unit_in_hex(game.battle, u => { + if (unit_class(u) === tc) + hp += unit_hp(u) + }) + return hp +} + function count_normal_steps_in_pursuit() { let steps = 0 for_each_undisrupted_enemy_unit_in_hex(game.pursuit, u => { @@ -898,6 +949,15 @@ const path_cost = [ new Array(hexcount), new Array(hexcount), new Array(hexcount const path_valid = new Array(hexcount) const path_enemy = new Array(hexcount) +function print_path(who, from, to, road) { + let p = [ hex_name[to] ] + while (to && to !== from) { + to = path_from[road][to] + p.unshift(hex_name[to]) + } + log(unit_name(who) + " moved " + p.join(", ") + ".") +} + function search_move(start, start_cost, start_road) { // recon=4, forced march=+1, rommel bonus=+1 let limit = 6 @@ -1084,6 +1144,8 @@ function search_withdraw_bfs(from, cost, start, road, max_cost, sline, sdist) { } } +// TODO: search_retreat where first hexside exit is restricted to friendly + function can_move_to(to, road, speed) { // TODO: engagement & hexside limit if (road >= 4 && path_cost[4][to] <= speed + 4) @@ -1152,13 +1214,13 @@ function adjacent_hex_has_undisrupted_friendly_unit(here) { } function max_speed_of_undisrupted_friendly_unit_in_hex(from) { - let max = 0 + let max_speed = 0 for_each_undisrupted_friendly_unit_in_hex(from, u => { let s = unit_speed(u) - if (s > max) - max = s + if (s > max_speed) + max_speed = s }) - return max + return max_speed } function find_valid_regroup_destinations(from, rommel) { @@ -1179,50 +1241,18 @@ function set_active_player() { } function set_passive_player() { - if (game.phasing === AXIS) + if (game.active === AXIS) game.active = ALLIED else game.active = AXIS } -// Supply check -// Turn option - -// Movement -// declare moves -// normal moves -// rout -// retreats -// declare full/partial retreats -// probe combat -// pursuit fire -// withdraw -// rout -// pursuit fire -// withdraw -// forced marches -// rout -// pursuit fire -// withdraw -// refuse battle -// pursuit fire -// withdraw -// rout -// pursuit fire -// withdraw -// Combat -// declare active -// declare assault -// resolve -// defensive fire -// rout -// offensive fire -// rout -// Blitz Movement -// Blitz Combat -// Final supply check -// rout -// Reveal supply cards -> next player +function set_enemy_player() { + if (game.active === AXIS) + game.active = ALLIED + else + game.active = AXIS +} function end_player_turn() { // TODO: end when both pass @@ -1235,6 +1265,8 @@ function end_player_turn() { } function goto_player_turn() { + log_h2(game.phasing) + // paranoid resetting of state game.side_limit = {} game.rommel = 0 @@ -1254,7 +1286,7 @@ function goto_initial_supply_check() { if (snet[x]) { set_unit_supply(u, ssrc) if (is_unit_disrupted(u) && set_has(game.recover, u) && !is_battle_hex(x)) { - log(`${unit_name(u)} recovered at ${hex_name[x]}`) + log(`%${u} recovered at #${x}`) set_delete(game.recover, u) clear_unit_disrupted(u) } @@ -1464,25 +1496,25 @@ states.regroup_move_destination = { function goto_move_who() { if (game.rommel === 1) { if (game.from1 && game.to1) - log(`Regroup move from ${game.from1} to ${game.to1} (Rommel).`) + log(`Regroup move from #${game.from1} to #${game.to1} (Rommel).`) else if (game.from1) - log(`Group move from ${game.from1} (Rommel).`) + log(`Group move from #${game.from1} (Rommel).`) } else { if (game.from1 && game.to1) - log(`Regroup move from ${game.from1} to ${game.to1}.`) + log(`Regroup move from #${game.from1} to #${game.to1}.`) else if (game.from1) - log(`Group move from ${game.from1}.`) + log(`Group move from #${game.from1}.`) } if (game.rommel === 2) { if (game.from2 && game.to2) - log(`Regroup move from ${game.from2} to ${game.to2} (Rommel).`) + log(`Regroup move from #${game.from2} to #${game.to2} (Rommel).`) else if (game.from2) - log(`Group move from ${game.from2} (Rommel).`) + log(`Group move from #${game.from2} (Rommel).`) } else { if (game.from2 && game.to2) - log(`Regroup move from ${game.from2} to ${game.to2}.`) + log(`Regroup move from #${game.from2} to #${game.to2}.`) else if (game.from2) - log(`Group move from ${game.from2}.`) + log(`Group move from #${game.from2}.`) } game.state = 'move_who' } @@ -1555,25 +1587,6 @@ function end_move_phase() { goto_refuse_battle() } -function print_path(who, from, to, road) { - let p = [ hex_name[to] ] - while (to && to !== from) { - to = path_from[road][to] - p.unshift(hex_name[to]) - } - log(unit_name(who) + " moved " + p.join(", ") + ".") -} - -function print_pathX(who, start, to, road) { - let from = path_from[road][to] - log(`M ${hex_name[from]} to ${hex_name[to]}`) - while (from && from !== start) { - to = from - from = path_from[road][from] - log(`M ${hex_name[from]} to ${hex_name[to]}`) - } -} - function apply_move(move, who, from, to) { let speed = unit_speed(who) + (move === game.rommel ? 1 : 0) let road = pick_path(to, game.move_road, speed) @@ -1869,7 +1882,7 @@ states.retreat_select_who = { }) game.retreat_units = game.selected game.selected = [] - goto_pursuit_fire(game.retreat) + goto_retreat_pursuit_fire(game.retreat) }, partial_retreat() { clear_undo() @@ -1898,7 +1911,7 @@ states.provoke_probe_combat = { if (shielded) goto_retreat_who() else - goto_pursuit_fire(game.retreat) + goto_retreat_pursuit_fire(game.retreat) }, } @@ -1938,13 +1951,30 @@ states.retreat_to = { prompt() { view.prompt = `Retreat: Select destination.` let who = game.selected[0] + gen_action_unit(who) - //if (game.turn_option === 'pass') { - { + + if (game.turn_option === 'pass') { search_withdraw(game.retreat, true) - // TODO: regroup - gen_withdraw_group_move(who, game.retreat) + } else { + search_retreat(game.retreat) } + + if (from === game.from1 && !game.to1) + for (let to of all_hexes) + if (to != from && can_move_group_1(who, from, to)) + gen_action_hex(to) + + if (from === game.from2 && !game.to2) + for (let to of all_hexes) + if (to != from && can_move_group_2(who, from, to)) + gen_action_hex(to) + + if (can_move_regroup_1(who, from, game.to1)) + gen_action_hex(game.to1) + + if (can_move_regroup_2(who, from, game.to2)) + gen_action_hex(game.to2) }, unit(who) { pop_undo() @@ -1989,7 +2019,7 @@ states.refuse_battle = { hex(x) { push_undo() set_delete(game.active_battles, x) - goto_pursuit_fire(x) + goto_refuse_pursuit_fire(x) }, next() { goto_combat_phase() @@ -2144,213 +2174,203 @@ function end_combat_phase() { // === BATTLES === +// Normal Battle: +// passive fire +// active hits +// active fire +// passive hits + +function roll_fire(who, fp, tc) { + let roll = random(6) + 1 + log(`${who} fired ${firepower_name[fp]} ${roll} at ${class_name[tc]}`) + if (roll >= fp) + return 1 + return 0 +} + function goto_battle(x) { clear_undo() game.battle = x - goto_defensive_fire() -} + game.fired = [] -function goto_defensive_fire() { + // goto defensive fire set_passive_player() - game.fired = [] + game.state = 'battle_fire' game.hits = [ 0, 0, 0, 0 ] - game.state = 'defensive_fire' } -function goto_offensive_fire() { - set_active_player() - game.fired = [] - game.hits = [ 0, 0, 0, 0 ] - game.state = 'offensive_fire' -} +function apply_battle_fire(tc) { + let firing = game.selected[0] + game.selected.length = 0 -const xxx_fire = { - prompt() { - view.prompt = `Fire!` + let fp = FIREPOWER_MATRIX[unit_class(firing)][tc] + let cv = unit_cv(firing) - let arty = false - for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { - if (is_artillery_unit(u)) { - if (!is_unit_fired(u)) { - gen_action_unit(u) - arty = true - } - } - }) + set_unit_fired(firing) - if (!arty) { - for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { - if (!is_unit_fired(u)) - gen_action_unit(u) - }) + for (let i = 0; i < cv; ++i) + game.hits[tc] += roll_fire(firing, fp, tc) + + // clamp to available hit points + game.hits[tc] = min(game.hits[tc], count_hp_in_battle_of_class(tc)) + + // end firing when all done + let done = true + for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { + if (!is_unit_fired(u)) + done = false + }) + if (done) { + set_enemy_player() + game.state = 'battle_hits' + } +} + +function gen_battle_fire() { + let arty = false + for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { + if (is_artillery_unit(u)) { + if (!is_unit_fired(u)) { + gen_action_unit(u) + arty = true + } } - }, - unit(who) { - game.selected = [ who ] - game.state = game.state + '_target' - }, + }) + if (!arty) { + for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { + if (!is_unit_fired(u)) + gen_action_unit(u) + }) + } } -const xxx_fire_target = { - prompt() { - view.prompt = `Select a target class.` +function gen_battle_target() { + let hp = count_hp_in_battle() + for (let i = 0; i < 4; ++i) + hp[i] -= game.hits[i] - let hp = count_hp_in_battle() - for (let i = 0; i < 4; ++i) - hp[i] -= game.hits[i] + let who = game.selected[0] + let fc = unit_class(who) - let who = game.selected[0] - let fc = unit_class(who) + gen_action_unit(who) // deselect - gen_action_unit(who) // deselect + // armor must target armor if possible + if (fc === ARMOR && hp[ARMOR] > 0) { + gen_action('armor') + return + } - // armor must target armor if possible - if (fc === ARMOR && hp[ARMOR] > 0) { - gen_action('armor') - return - } + // infantry must target infantry if possible + if (fc === INFANTRY && hp[INFANTRY] > 0) { + gen_action('infantry') + return + } - // infantry must target infantry if possible - if (fc === INFANTRY && hp[INFANTRY] > 0) { - gen_action('infantry') - return + if (hp[ARMOR] > 0) + gen_action('armor') + if (hp[INFANTRY] > 0) + gen_action('infantry') + if (hp[ANTITANK] > 0) + gen_action('antitank') + + // only artillery may target artillery if other units are alive + if (hp[ARTILLERY] > 0) { + if (fc === ARTILLERY || + (hp[ARTILLERY] <= 0 && hp[INFANTRY] <= 0 && hp[ANTITANK] <= 0)) + gen_action('artillery') + } +} + +function gen_battle_hits() { + let normal_steps = count_normal_steps_in_battle() + let elite_steps = count_elite_steps_in_battle() + + let done = true + for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { + let c = unit_class(u) + if (is_unit_elite(u)) { + if (game.hits[c] >= 2) { + gen_action_unit(u) + done = false + } + } else { + if (game.hits[c] >= 1) { + // If mixed elite and non-elite: must assign ALL damage. + if (elite_steps[c] > 0 && normal_steps[c] === 1 && (game.hits[c] & 1) === 0) { + // Eliminating the last non-elite must not leave an odd + // number of hits remaining. + } else { + gen_action_unit(u) + done = false + } + } } + }) + if (done) + gen_action_next() +} + +function apply_battle_hit(who) { + game.hits[unit_class(who)] -= reduce_unit(who) +} - if (hp[ARMOR] > 0) - gen_action('armor') - if (hp[INFANTRY] > 0) - gen_action('infantry') - if (hp[ANTITANK] > 0) - gen_action('antitank') - - // only artillery may target artillery if other units are alive - if (hp[ARTILLERY] > 0) { - if (fc === ARTILLERY || - (hp[ARTILLERY] <= 0 && hp[INFANTRY] <= 0 && hp[ANTITANK] <= 0)) - gen_action('artillery') +states.battle_fire = { + prompt() { + if (game.active === game.phasing) + view.prompt = `Offensive Fire!` + else + view.prompt = `Defensive Fire!` + if (game.selected.length > 0) { + gen_battle_target() + } else { + gen_battle_fire() } }, - unit(u) { - game.selected = [] - resume_fire() + unit(who) { + if (game.selected.length > 0) + game.selected.length = 0 + else + game.selected.push(who) }, armor() { - let who = game.selected[0] - game.selected = [] - fire_at(who, ARMOR) + apply_battle_fire(ARMOR) }, infantry() { - let who = game.selected[0] - game.selected = [] - fire_at(who, INFANTRY) + apply_battle_fire(INFANTRY) }, antitank() { - let who = game.selected[0] - game.selected = [] - fire_at(who, ANTITANK) + apply_battle_fire(ANTITANK) }, artillery() { - let who = game.selected[0] - game.selected = [] - fire_at(who, ARTILLERY) + apply_battle_fire(ARTILLERY) }, } -const xxx_fire_hits = { +states.battle_hits = { prompt() { + if (game.active === game.phasing) + view.prompt = `Apply hits from Defensive Fire.` + else + view.prompt = `Apply hits from Offensive Fire.` view.prompt = `Apply hits.` - - let normal_steps = count_normal_steps_in_battle() - let elite_steps = count_elite_steps_in_battle() - - let done = true - for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { - let c = unit_class(u) - if (is_unit_elite(u)) { - if (game.hits[c] >= 2) { - gen_action_unit(u) - done = false - } - } else { - if (game.hits[c] >= 1) { - // If mixed elite and non-elite: must assign ALL damage. - if (elite_steps[c] > 0 && normal_steps[c] === 1 && (game.hits[c] & 1) === 0) { - // Eliminating the last non-elite must not leave an odd - // number of hits remaining. - } else { - gen_action_unit(u) - done = false - } - } - } - }) - if (done) - gen_action_next() + gen_battle_hits() }, unit(who) { push_undo() - let c = unit_class(who) - game.hits[c] -= reduce_unit(who) + apply_battle_hit(who) }, next() { clear_undo() - if (game.state === 'defensive_fire_hits') { - goto_offensive_fire() + if (game.active === game.phasing) { + // goto offensive fire + game.state = 'battle_fire' + game.hits = [ 0, 0, 0, 0 ] } else { end_battle() } }, } -function roll_fire(who, fp, tc) { - let roll = random(6) + 1 - log(`${who} fired ${firepower_name[fp]} ${roll} at ${class_name[tc]}`) - if (roll >= fp) - return 1 - return 0 -} - -function fire_at(firing, tc) { - let fp = FIREPOWER_MATRIX[unit_class(firing)][tc] - let cv = unit_cv(firing) - - set_unit_fired(firing) - - for (let i = 0; i < cv; ++i) - game.hits[tc] += roll_fire(firing, fp, tc) - - resume_fire() -} - -function resume_fire() { - game.state = game.state.replace("_target", "") - let done = true - for_each_undisrupted_friendly_unit_in_hex(game.battle, u => { - if (!is_unit_fired(u)) - done = false - }) - if (done) - goto_fire_hits() -} - -function goto_fire_hits() { - // clamp number of hits to available hp - let hp = [ 0, 0, 0, 0 ] - for (let u = 0; u < units.length; ++u) - if (is_enemy_unit(u) && unit_hex(u) === game.battle) - hp[unit_class(u)] += unit_hp(u) - for (let i = 0; i < 4; ++i) - game.hits[i] = min(game.hits[i], hp[i]) - - if (game.state === 'defensive_fire') { - set_active_player() - game.state = 'defensive_fire_hits' - } else { - set_passive_player() - game.state = 'offensive_fire_hits' - } -} - function end_battle() { clear_undo() set_active_player() @@ -2363,37 +2383,47 @@ function end_battle() { end_combat_phase() } -states.defensive_fire = xxx_fire -states.offensive_fire = xxx_fire -states.defensive_fire_target = xxx_fire_target -states.offensive_fire_target = xxx_fire_target -states.defensive_fire_hits = xxx_fire_hits -states.offensive_fire_hits = xxx_fire_hits +function end_probe() { + game.battle = 0 + resume_retreat() +} // === PURSUIT FIRE === -function goto_retreat_fire(where) { +function goto_retreat_pursuit_fire(where) { clear_undo() set_passive_player() game.hits = 0 game.pursuit = where if (can_pursuit_fire()) - game.state = 'pursuit_fire' + game.state = 'retreat_pursuit_fire' else goto_pursuit_hits() } -function goto_pursuit_fire(where) { +function goto_refuse_pursuit_fire(where) { clear_undo() set_active_player() game.hits = 0 game.pursuit = where if (can_pursuit_fire()) - game.state = 'pursuit_fire' + game.state = 'refuse_pursuit_fire' else goto_pursuit_hits() } +function goto_pursuit_hits() { + if (game.hits > 0) { + set_passive_player() + let hp = count_hp_in_pursuit() + if (game.hits > hp) + game.hits = hp + game.state = 'pursuit_hits' + } else { + end_pursuit_fire() + } +} + function slowest_undisrupted_enemy_unit_speed(where) { let r = 4 for_each_undisrupted_enemy_unit_in_hex(where, u => { @@ -2433,7 +2463,7 @@ function roll_pursuit_fire(n) { } } -states.pursuit_fire = { +const xxx_pursuit_fire = { inactive: "pursuit fire (fire)", prompt() { view.prompt = `Pursuit Fire.` @@ -2457,19 +2487,7 @@ states.pursuit_fire = { } } -function goto_pursuit_hits() { - if (game.hits > 0) { - set_passive_player() - let hp = count_hp_in_pursuit() - if (game.hits > hp) - game.hits = hp - game.state = 'pursuit_hits' - } else { - end_pursuit_fire() - } -} - -states.pursuit_hits = { +const xxx_pursuit_hits = { inactive: "pursuit fire (hits)", prompt() { view.prompt = `Pursuit Fire: Apply ${game.hits} hits.` @@ -2569,6 +2587,7 @@ states.free_deployment = { }, hex(x) { push_undo() + log(`Deployed ${game.selected.length} at #${x}.`) for (let i = 0; i < game.selected.length; ++i) { let u = game.selected[i] set_unit_hex(u, x) @@ -2577,10 +2596,12 @@ states.free_deployment = { }, next() { clear_undo() - if (game.active === AXIS) + if (game.active === AXIS) { game.active = ALLIED - else + log_h2("Allied Deployment") + } else { end_free_deployment() + } } } @@ -2594,6 +2615,10 @@ function end_free_deployment() { // TODO: mulligan + log_br() + log_h1(`Month ${game.month}`) + log_br() + // No buildup first month // No initiative first month goto_player_turn() @@ -3026,12 +3051,15 @@ function setup(name) { let scenario = SCENARIOS[name] game.month = scenario.start + log_h1(name) + SETUP[name](-scenario.start) game.bardia = setup_fortress(scenario, BARDIA) game.benghazi = setup_fortress(scenario, BENGHAZI) game.tobruk = setup_fortress(scenario, TOBRUK) + log_h2("Axis Deployment") game.active = 'Axis' game.state = 'free_deployment' game.selected = [] @@ -3296,7 +3324,7 @@ function clear_undo() { game.undo = [] } -function logbr() { +function log_br() { if (game.log.length > 0 && game.log[game.log.length-1] !== "") game.log.push("") } @@ -3305,6 +3333,24 @@ function log(msg) { game.log.push(msg) } +function log_h1(msg) { + log_br() + log(".h1 " + msg) + log_br() +} + +function log_h2(msg) { + log_br() + log(".h2 " + msg) + log_br() +} + +function log_h3(msg) { + log_br() + log(".h3 " + msg) + log_br() +} + function gen_action(action, argument) { if (argument !== undefined) { if (!(action in view.actions)) { @@ -3323,7 +3369,7 @@ function goto_game_over(result, victory) { game.active = "None" game.result = result game.victory = victory - logbr() + log_br() log(game.victory) } |