diff options
-rw-r--r-- | play.html | 4 | ||||
-rw-r--r-- | rules.js | 293 |
2 files changed, 197 insertions, 100 deletions
@@ -221,7 +221,7 @@ svg .side { svg .side.axis_control { stroke: green; - stroke-width: 8px; + stroke-width: 4px; stroke-opacity: 0.8; stroke-dasharray: 54 100; stroke-dashoffset: -8; @@ -229,7 +229,7 @@ svg .side.axis_control { svg .side.allied_control { stroke: brown; - stroke-width: 8px; + stroke-width: 4px; stroke-opacity: 0.8; stroke-dasharray: 54 100; stroke-dashoffset: -8; @@ -5,15 +5,12 @@ // TODO: raiders // TODO: legal pass withdrawal moves (reduce supply net, withdraw from fortress attack) -// TODO: FORCED MARCHES // TODO: MINEFIELDS // TODO: SUPPLY COMMITMENT // TODO: BUILDUP // TODO: setup scenario specials -// TODO: failed forced march hexside control... - // TODO: when is "fired" status cleared? // RULES: disrupted units routed again in second enemy turn, will they still recover? @@ -434,7 +431,6 @@ function is_axis_unit(u) { // === MAP STATE === function update_presence() { - console.log("UPDATE PRESENCE") presence_invalid = false presence_axis.fill(0) for (let u = first_axis_unit; u <= last_axis_unit; ++u) @@ -1383,6 +1379,7 @@ function goto_player_turn() { game.to1 = game.to2 = 0 // reset moved and fired flags + game.forced = [] set_clear(game.fired) set_clear(game.moved) @@ -1474,10 +1471,10 @@ function goto_initial_supply_check_rout() { else if (n === 1) goto_rout(where, false, goto_initial_supply_check_rout) else - game.state = 'initial_supply_check_routs' + game.state = 'initial_supply_check_rout' } -states.initial_supply_check_routs = { +states.initial_supply_check_rout = { prompt() { view.prompt = `Initial Supply Check: Rout!` for (let x of all_hexes) @@ -1526,22 +1523,10 @@ function goto_final_supply_check_rout() { else if (n === 1) goto_rout(where, false, goto_final_supply_check_rout) else - game.state = 'final_supply_check_routs' + game.state = 'final_supply_check_rout' } -states.final_supply_check_routs = { - prompt() { - view.prompt = `Final Supply Check: Rout!` - for (let x of all_hexes) - if (is_friendly_rout_hex(x)) - gen_action_hex(x) - }, - hex(where) { - goto_rout(where, false, goto_final_supply_check_rout) - } -} - -states.final_supply_check_routs = { +states.final_supply_check_rout = { prompt() { view.prompt = `Final Supply Check: Rout!` for (let x of all_hexes) @@ -1700,11 +1685,9 @@ states.regroup_move_destination = { }, } -function end_move_phase() { - game.side_limit = {} +function end_movement() { game.from1 = game.from2 = game.to1 = game.to2 = 0 - // TODO: forced marches - goto_refuse_battle() + goto_forced_marches() } // === GROUP AND REGROUP MOVEMENT === @@ -1830,7 +1813,7 @@ states.move = { apply_select(who) }, forced_march(to) { - apply_move(to) + this.hex(to) }, hex(to) { apply_move(to) @@ -1859,7 +1842,7 @@ states.move = { end_move() { clear_undo() log_br() - end_move_phase() + end_movement() } } @@ -1931,27 +1914,29 @@ function apply_move(to) { let rommel2 = (game.rommel === 2) ? 1 : 0 let who = pop_selected() let from = unit_hex(who) - let speed = unit_speed(who) + 1 // forced march + let speed = unit_speed(who) push_undo() - search_move(from, speed + (rommel1 | rommel2)) + search_move(from, speed + 1 + (rommel1 | rommel2)) if (!game.to1 && game.from1 === from) - if (can_move_to(to, speed + rommel1)) - return move_unit(who, to, speed + rommel1, 1) + if (can_move_to(to, speed + 1 + rommel1)) + return move_unit(who, to, speed + 1 + rommel1, 1) if (!game.to2 && game.from2 === from) - if (can_move_to(to, speed + rommel2)) - return move_unit(who, to, speed + rommel2, 2) + if (can_move_to(to, speed + 1 + rommel2)) + return move_unit(who, to, speed + 1 + rommel2, 2) if (game.to1 === to && is_hex_or_adjacent_to(from, game.from1)) - if (can_move_to(to, speed + rommel1)) - return move_unit(who, to, speed + rommel1, 1) + if (can_move_to(to, speed + 1 + rommel1)) + return move_unit(who, to, speed + 1 + rommel1, 1) if (game.to2 === to && is_hex_or_adjacent_to(from, game.from2)) - if (can_move_to(to, speed + rommel2)) - return move_unit(who, to, speed + rommel2, 2) + if (can_move_to(to, speed + 1 + rommel2)) + return move_unit(who, to, speed + 1 + rommel2, 2) + + throw Error("bad move") } // to check usable alternate paths to enter destination hex @@ -1962,62 +1947,75 @@ function can_move_via(via, to, speed, road) { // too far if (cost + 1 > speed + road) - return false + return 0 // can't cross this hexside if (max_side === 0) - return false + return 0 // must stay on road for current bonus if (side_road[side] < road) - return false + return 0 // must stop on enemies if (has_enemy_unit(via)) - return false + return 0 // may not exceed hexside limit if (has_enemy_unit(to)) if ((game.side_limit[side] | 0) >= max_side) - return false + return 0 - return true + if (cost + 1 === speed + road) + return 2 + return 1 } function is_forced_march_move(from, to, speed) { - console.log("is_forced_march_move", from, to, speed, "=>", - path_cost[4][to], - path_cost[2][to], - path_cost[1][to], - path_cost[0][to] - ) - if (hex_road[from] >= 4 && path_cost[4][to] === speed + 4) - return true - if (hex_road[from] >= 2 && path_cost[2][to] === speed + 2) - return true - if (hex_road[from] >= 1 && path_cost[1][to] === speed + 1) - return true - if (hex_road[from] >= 0 && path_cost[0][to] === speed + 0) - return true - return false + let result = true + if (hex_road[from] >= 0 && path_cost[0][to] < speed + 0) + result = false + if (hex_road[from] >= 1 && path_cost[1][to] < speed + 1) + result = false + if (hex_road[from] >= 2 && path_cost[2][to] < speed + 2) + result = false + if (hex_road[from] >= 4 && path_cost[4][to] < speed + 4) + result = false + return result } function move_via(who, to, speed, move) { let from = unit_hex(who) game.hexside = { who: who, - via: [], to: to, + via: [], + forced: [], move: move } for_each_adjacent_hex(to, via => { - if ( - (hex_road[from] >= 4 && can_move_via(via, to, speed, 4)) || - (hex_road[from] >= 2 && can_move_via(via, to, speed, 2)) || - (hex_road[from] >= 1 && can_move_via(via, to, speed, 1)) || - (hex_road[from] >= 0 && can_move_via(via, to, speed, 0)) - ) - game.hexside.via.push(via) + let forced = false + let unforced = false + function check_road(bonus) { + if (hex_road[from] >= bonus) { + let k = can_move_via(via, to, speed, bonus) + if (k === 2) + forced = true + else if (k === 1) + unforced = true + } + } + check_road(4) + check_road(2) + check_road(1) + check_road(0) + if (unforced) { + game.hexside.via.push(via) + game.hexside.forced.push(0) + } else if (forced) { + game.hexside.via.push(via) + game.hexside.forced.push(1) + } }) return game.hexside.via.length === 1 } @@ -2025,7 +2023,7 @@ function move_via(who, to, speed, move) { function move_unit(who, to, speed, move) { let from = unit_hex(who) - if (is_forced_march_move(from, to, speed + 1)) { + if (is_forced_march_move(from, to, speed)) { if (move_via(who, to, speed, move)) { forced_march_via(who, game.hexside.via[0], to, move) delete game.hexside @@ -2036,7 +2034,10 @@ function move_unit(who, to, speed, move) { else if (has_enemy_unit(to)) { if (move_via(who, to, speed, move)) { - engage_via(who, game.hexside.via[0], to) + if (game.hexside.forced[0]) + forced_march_via(who, game.hexside.via[0], to, move) + else + engage_via(who, game.hexside.via[0], to) delete game.hexside } else { game.state = 'engage_via' @@ -2052,7 +2053,7 @@ function move_unit(who, to, speed, move) { states.forced_march_via = { prompt() { - view.prompt = `Move: Select which hex side to cross.` + view.prompt = `Move: Select which path to take.` view.selected = game.hexside.who for (let x of game.hexside.via) gen_action_hex(x) @@ -2068,8 +2069,16 @@ states.engage_via = { prompt() { view.prompt = `Move: Select which hex side to cross.` view.selected = game.hexside.who - for (let x of game.hexside.via) - gen_action_hex(x) + for (let i = 0; i < game.hexside.via.length; ++i) + if (game.hexside.forced[i]) + gen_action_forced_march(game.hexside.via[i]) + else + gen_action_hex(game.hexside.via[i]) + }, + forced_march(via) { + forced_march_via(game.hexside.who, via, game.hexside.to, game.hexside.move) + delete game.hexside + game.state = 'move' }, hex(via) { engage_via(game.hexside.who, via, game.hexside.to) @@ -2085,12 +2094,12 @@ function forced_march_via(who, via, to, move) { // remember where we should advance to / return to if ((move === 1 && game.to1) || (move === 2 && game.to2)) - game.forced[who] = to + game.forced.push([who, to, from]) else - game.forced[who] = [ to, from ] + game.forced.push([who, to]) // attempted force marches affect hexside limits - if (is_enemy_hex(to)) { + if (has_enemy_unit(to)) { let side = to_side_id(via, to) if (game.side_limit[side]) game.side_limit[side] = 2 @@ -2125,6 +2134,86 @@ function engage_via(who, via, to) { log(`>from #${from} to #${to}`) } +// === FORCED MARCHES === + +function goto_forced_marches() { + if (game.forced.length > 0) + game.state = 'forced_marches' + else + end_forced_marches() +} + +states.forced_marches = { + prompt() { + view.prompt = `Forced Marches!` + for (let [who, to] of game.forced) + gen_action_unit(who) + }, + unit(who) { + let via = unit_hex(who) + let ix = game.forced.findIndex(item => who === item[0]) + let to = game.forced[ix][1] + let from = game.forced[ix][2] || via + let roll = random(6) + 1 + if (roll >= 4) { + log(`Forced March roll ${roll} success.`) + if (has_enemy_unit(to)) { + engage_via(who, via, to, false) + } else { + set_unit_hex(who, to) + log(`>from #${via} to #${to}`) + } + } else { + log(`Forced March roll ${roll} failed!`) + if (from !== via) { + log(`>returned to #${from}`) + set_unit_hex(who, from) + } + if (is_unit_disrupted(who)) + reduce_unit(who) // was a retreating unit + else + set_unit_disrupted(who) + } + game.forced.splice(ix, 1) + + goto_forced_marches() + } +} + +function end_forced_marches() { + game.side_limit = {} + game.forced = null + goto_forced_marches_rout() +} + +function goto_forced_marches_rout() { + let n = 0, where = 0 + for (let x of all_hexes) { + if (is_friendly_rout_hex(x)) { + where = x + n++ + } + } + if (n === 0) + goto_refuse_battle() + else if (n === 1) + goto_rout(where, false, goto_forced_marches_rout) + else + game.state = 'forced_marches_rout' +} + +states.forced_marches_rout = { + prompt() { + view.prompt = `Forced Marches: Rout!` + for (let x of all_hexes) + if (is_friendly_hexside(x)) + gen_action_hex(x) + }, + hex(where) { + goto_rout(where, false, goto_forced_marches_rout) + } +} + // === RETREAT === function is_valid_retreat_hex(from) { @@ -2297,7 +2386,7 @@ states.retreat_from = { end_move() { clear_undo() log_br() - end_move_phase() + end_movement() } } @@ -2427,11 +2516,12 @@ states.retreat_move = { unit(who) { apply_select(who) }, + forced_march(to) { + this.hex(to) + }, hex(to) { - let who = pop_selected() - push_undo() - set_unit_hex(who, to) - set_unit_moved(who) + let who = game.selected + apply_move(to) set_unit_disrupted(who) }, end_retreat() { @@ -2440,7 +2530,6 @@ states.retreat_move = { } function end_retreat() { - clear_undo() if (!is_battle_hex(game.retreat)) release_hex_control(game.retreat) game.retreat_units = null @@ -2461,15 +2550,21 @@ function end_retreat_2() { if (can_select_retreat_hex()) game.state = 'retreat_from' else - end_move_phase() + end_movement() } // === REFUSE BATTLE === +function can_select_refuse_battle_hex() { + for (let x of game.active_battles) + if (can_all_refuse_battle(x)) + return true + return false +} + function goto_refuse_battle() { - clear_undo() - if (game.active_battles.length > 0) { - set_passive_player() + set_passive_player() + if (can_select_refuse_battle_hex()) { game.state = 'refuse_battle' } else { goto_combat_phase() @@ -2555,7 +2650,6 @@ states.refuse_battle_move = { } function end_refuse_battle_move() { - clear_undo() if (is_friendly_rout_hex(game.refuse)) goto_rout(game.refuse, false, end_refuse_battle_move_2) else @@ -2675,7 +2769,6 @@ states.rout_move = { } function end_rout() { - clear_undo() game.state = game.rout.state release_hex_control(game.rout.from) set_delete(game.active_battles, game.rout.from) @@ -2694,7 +2787,6 @@ function is_mandatory_combat(fortress, control_prop) { } function goto_combat_phase() { - clear_undo() set_active_player() if (game.turn_option === 'pass') { @@ -2800,17 +2892,21 @@ states.select_battle = { function end_combat_phase() { if (game.turn_option === 'blitz') { - log_h2(`Blitz Turn`) - if (game.rommel) - game.rommel = 3 - set_clear(game.fired) - game.turn_option = 'second blitz' - goto_move_phase() + goto_blitz_turn() } else { goto_final_supply_check() } } +function goto_blitz_turn() { + log_h2(`Blitz Turn`) + if (game.rommel) + game.rommel = 3 + set_clear(game.fired) + game.turn_option = 'second blitz' + goto_move_phase() +} + // === BATTLES === // Normal Battle: @@ -2878,15 +2974,19 @@ function roll_battle_fire(who, tc) { if (is_minefield_offensive_fire()) total = total / 2 - game.flash = `${unit_name(who)} fired ${firepower_name[fp]} at ${class_name[tc]}: ` + result.join(", ") + game.flash = `${unit_name(who)} fired ${firepower_name[fp]} ${result.join(", ")} at ${class_name[tc]}` log(game.flash) return total } function goto_battle(x) { - clear_undo() game.battle = x + if (is_assault_battle()) + log_h3(`Assault in #${x}`) + else + log_h3(`Battle in #${x}`) + // goto defensive fire set_passive_player() game.state = 'battle_fire' @@ -3177,7 +3277,6 @@ function end_probe_hits() { // routing moves function goto_rout_fire(where) { - clear_undo() set_enemy_player() game.hits = 0 game.pursuit = where @@ -3188,7 +3287,6 @@ function goto_rout_fire(where) { } function goto_pursuit_fire_during_retreat(where) { - clear_undo() set_passive_player() game.hits = 0 game.pursuit = where @@ -3199,7 +3297,6 @@ function goto_pursuit_fire_during_retreat(where) { } function goto_pursuit_fire_during_refuse_battle(where) { - clear_undo() set_active_player() game.hits = 0 game.pursuit = where @@ -3996,7 +4093,7 @@ exports.setup = function (seed, scenario, options) { // current turn option and selected moves turn_option: null, side_limit: {}, - forced: {}, + forced: null, rommel: 0, from1: 0, to1: 0, |