diff options
-rw-r--r-- | play.css | 20 | ||||
-rw-r--r-- | play.html | 1 | ||||
-rw-r--r-- | play.js | 216 | ||||
-rw-r--r-- | rules.js | 335 |
4 files changed, 69 insertions, 503 deletions
@@ -205,6 +205,15 @@ TWOD.piece.cylinder { border: 1px solid black; } +#combat { + position: absolute; + width: 52px; + height: 52px; + border: 4px solid #c00; + background-color: #c008; + border-radius: 50%; +} + .panel { background-color: #444; width: clamp(824px, calc(100% - 30px), 1636px); @@ -279,17 +288,6 @@ TWOD.piece.cylinder { .space.objective.action.imperial { background-color: #fbe30080; } .space.objective.action.france { background-color: #ed1c2380; } -@media (pointer: fine) { - -.move line { stroke: white; } -.battle line { stroke: #c00; } -.retreat line { stroke: #800; opacity: 33%; } -line { stroke-linecap: round; } -line.road { stroke-width: 6px; } -line.major_road { stroke-width: 8px; } - -} - .marker.conquest.austria { background-image: url(images/conquest_austria.2x.png) } .marker.conquest.france { background-image: url(images/conquest_france.2x.png) } .marker.conquest.imperial { background-image: url(images/conquest_imperial.2x.png) } @@ -38,7 +38,6 @@ <div id="mapwrap"> <div id="map"> - <svg id="roads" class="move" viewBox="0 0 2485 1654"></svg> <div id="spaces"></div> <div id="markers"></div> <div id="pieces"></div> @@ -1,14 +1,14 @@ "use strict" // vim: set nowrap: +/* globals data, view, action_button, action_button_with_argument, confirm_action_button, send_action +*/ // TODO: sort selected generals above deselected generals when detaching? // TODO: show battle icon overlay (instead of roads) // TODO: remove roads and path highlighting code // TODO: tooltips -const svgNS = "http://www.w3.org/2000/svg" - function toggle_pieces() { document.getElementById("pieces").classList.toggle("hide") } @@ -47,7 +47,6 @@ set_add_all(all_objectives, data.type.objective_russia) const objective1 = [ [], [], [], [], [], [], [] ] const objective2 = [ [], [], [], [], [], [], [] ] -const protect = [ [], [], [], [], [], [], [] ] for (let s of data.type.objective_prussia) set_add(objective1[P_PRUSSIA], s) for (let s of data.type.objective_russia) set_add(objective1[P_RUSSIA], s) @@ -63,7 +62,6 @@ const power_class = [ "prussia", "hanover", "russia", "sweden", "austria", "impe const power_name = [ "Prussia", "Hanover", "Russia", "Sweden", "Austria", "Imperial Army", "France" ] const GENERAL_POWER = [ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 3, 4, 4, 4, 4, 4, 5, 6, 6, 6 ] -const TRAIN_POWER = [ 0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6 ] const all_powers = [ 0, 1, 2, 3, 4, 5, 6 ] @@ -90,6 +88,10 @@ const all_power_trains = [ const RESERVE = 4 let suit_class = [ "S", "C", "H", "D", "R" ] +function to_deck(c) { + return c >> 7 +} + function to_suit(c) { return (c >> 4) & 7 } @@ -304,12 +306,9 @@ function register_action(target, action, id) { } function on_click_action(evt, target) { - if (evt.button === 0) { - if (send_action(target.my_action, target.my_id)) { - hide_move_path() + if (evt.button === 0) + if (send_action(target.my_action, target.my_id)) evt.stopPropagation() - } - } } function process_actions() { @@ -323,33 +322,6 @@ function is_action(action, arg) { return !!(view.actions && view.actions[action] && set_has(view.actions[action], arg)) } -function make_road(c1, c2, type) { - let e = document.createElementNS(svgNS, "line") - e.setAttribute("class", type) - let x1 = data.cities.x[c1] - let y1 = data.cities.y[c1] - let x2 = data.cities.x[c2] - let y2 = data.cities.y[c2] - - let v = Math.hypot(x2 - x1, y2 - y1) - let dx = (x2 - x1) / v - let dy = (y2 - y1) / v - let r = 18 - x1 += r * dx - y1 += r * dy - x2 -= r * dx - y2 -= r * dy - - e.setAttribute("x1", x1) - e.setAttribute("y1", y1) - e.setAttribute("x2", x2) - e.setAttribute("y2", y2) - e.setAttribute("visibility", "hidden") - ui.roads[c1][c2] = e - ui.roads[c2][c1] = e - ui.roads_element.appendChild(e) -} - function create_piece(action, id, style) { let e = document.createElement("div") e.className = style @@ -365,10 +337,6 @@ function create_marker(style) { return e } -function make_tc_id(n, suit, value) { - return (n << 7) | (suit << 4) | value -} - function make_tc_deck(n) { for (let suit = 0; suit <= 3; ++suit) { for (let value = 2; value <= 13; ++value) { @@ -493,6 +461,9 @@ function on_init() { make_tc_deck_back("deck_5"), ] + ui.combat = document.createElement("div") + ui.combat.id = "combat" + ui.tcbreak = document.createElement("div") ui.tcbreak.className = "draw-break" @@ -500,23 +471,6 @@ function on_init() { for (let fc = 0; fc <= 18; ++fc) ui.fate[fc] = make_fate_card(fc) - if (1) { - for (let a = 0; a <= last_city; ++a) - ui.roads[a] = [] - for (let a = 0; a <= last_city; ++a) { - for (let b of cities.major_roads[a]) { - if (a < b) { - make_road(a, b, "major_road") - } - } - for (let b of cities.roads[a]) { - if (a < b) { - make_road(a, b, "road") - } - } - } - } - for (let a = 0; a <= last_city; ++a) { let e = ui.cities[a] = document.createElement("div") let x = cities.x[a] @@ -569,117 +523,18 @@ function on_init() { on_init() -/* SHOW PATH FOR MOVES */ +/* TOOLTIPS */ function on_focus_city(evt) { - document.getElementById("status").textContent = evt.target.my_name - if (view) { - if (view.move_minor) - show_move_path(evt.target.my_id) - if (view.retreat) - show_retreat_path(evt.target.my_id) - } } function on_blur_city() { - document.getElementById("status").textContent = "" - hide_move_path() } function on_focus_piece(evt) { - document.getElementById("status").textContent = evt.target.my_name - if (view && view.move_minor) - show_move_path(view.pos[evt.target.my_id]) } function on_blur_piece() { - document.getElementById("status").textContent = "" - hide_move_path() -} - -var _move_path = [] -var _battle_road = null - -function hide_move_path() { - if (_move_path) { - for (let i = 1; i < _move_path.length; ++i) { - let x = _move_path[i-1] - let y = _move_path[i] - ui.roads[x][y].setAttribute("visibility", "hidden") - } - _move_path = null - } -} - -function show_move_path(x) { - hide_move_path() - - if (map_get(view.move_major, x, -1) >= 0) { - _move_path = [] - while (x >= 0) { - _move_path.push(x) - x = map_get(view.move_major, x, -1) - } - } - - else - - if (map_get(view.move_minor, x, -1) >= 0) { - _move_path = [] - while (x >= 0) { - _move_path.push(x) - x = map_get(view.move_minor, x, -1) - } - } - - if (_move_path) { - for (let i = 1; i < _move_path.length; ++i) { - let x = _move_path[i-1] - let y = _move_path[i] - ui.roads[x][y].setAttribute("visibility", "visible") - } - } -} - -function show_retreat_path(x) { - hide_move_path() - _move_path = map_get(view.retreat, x, null) - if (_move_path) { - _move_path = _move_path.slice() - _move_path.push(x) - for (let i = 1; i < _move_path.length; ++i) { - let x = _move_path[i-1] - let y = _move_path[i] - ui.roads[x][y].setAttribute("visibility", "visible") - } - } -} - -function show_battle_path() { - _battle_road = ui.roads[view.attacker][view.defender] - _battle_road.setAttribute("visibility", "visible") -} - -function hide_battle_path() { - if (_battle_road) { - _battle_road.setAttribute("visibility", "hidden") - _battle_road = null - } -} - -function update_path() { - hide_move_path() - hide_battle_path() - if (view.move_major) { - ui.roads_element.setAttribute("class", "move") - } else if (view.retreat) { - ui.roads_element.setAttribute("class", "retreat") - } else if (view.attacker !== undefined) { - ui.roads_element.setAttribute("class", "battle") - show_battle_path() - } else { - ui.roads_element.setAttribute("class", null) - } } /* UPDATE UI */ @@ -796,6 +651,14 @@ function layout_train(id, s) { e.classList.toggle("selected", set_has(view.selected, id)) } +function layout_combat_marker() { + let x = (data.cities.x[view.attacker] + data.cities.x[view.defender]) >> 1 + let y = (data.cities.y[view.attacker] + data.cities.y[view.defender]) >> 1 + ui.combat.style.left = x - 30 + "px" + ui.combat.style.top = y - 30 + "px" + ui.combat.style.zIndex = y +} + function create_conquest(style, s) { let x = data.cities.x[s] let y = data.cities.y[s] @@ -807,18 +670,6 @@ function create_conquest(style, s) { return e } -function to_deck(c) { - return c >> 7 -} - -function to_suit(c) { - return (c >> 4) & 7 -} - -function to_value(c) { - return c & 15 -} - function update_favicon() { let favicon = document.querySelector('link[rel="icon"]') switch (params.role) { @@ -859,7 +710,9 @@ function colorize(text) { } function on_update() { - ui.prompt.innerHTML = colorize(view.prompt) + let text = colorize(view.prompt) + if (text !== view.prompt) + ui.prompt.innerHTML = text ui.header.classList.toggle("prussia", view.power === P_PRUSSIA) ui.header.classList.toggle("hanover", view.power === P_HANOVER) @@ -876,8 +729,6 @@ function on_update() { for (let t = 24; t <= 34; ++t) layout_train(t, view.pos[t]) - update_path() - let back = [ 0, 0, 0, 0, 0 ] for (let i = 0; i < 5; ++i) @@ -916,6 +767,11 @@ function on_update() { for (let s of view.retro) ui.markers_element.appendChild(ui.retro[s]) + if (view.attacker !== undefined && view.defender !== undefined) { + ui.markers_element.appendChild(ui.combat) + layout_combat_marker() + } + /* troops 1-8, reserve 1-10 with modifiers +1 and +5 */ for (let v = 16; v >= 0; --v) action_button_with_argument("value", v, v) @@ -1066,19 +922,3 @@ function set_add_all(set, other) { for (let item of other) set_add(set, item) } - -function map_get(map, key, missing) { - let a = 0 - let b = (map.length >> 1) - 1 - while (a <= b) { - let m = (a + b) >> 1 - let x = map[m<<1] - if (key < x) - b = m - 1 - else if (key > x) - a = m + 1 - else - return map[(m<<1)+1] - } - return missing -} @@ -1,5 +1,8 @@ "use strict" +// TODO: prussian offensive (no capture when not offensive) +// TODO: final score summary at game end (FWC rules) + const R_FREDERICK = "Frederick" const R_ELISABETH = "Elisabeth" const R_MARIA_THERESA = "Maria Theresa" @@ -364,13 +367,6 @@ function clear_fate_effect() { game.fx = 0 } -function may_unstack() { - // TODO: 3-piece stack? - if (game.fx === NEXT_TURN_CHEVERT_MAY_NOT_UNSTACK) - return !set_has(game.selected, GEN_CHEVERT) - return true -} - function forbid_play_value_10_or_more() { if (game.fx === NEXT_TURN_IF_PRUSSIA_AND_FRANCE_FIGHT_EACH_OTHER_THEY_MAY_NOT_USE_TCS_WITH_VALUES_OF_10_OR_MORE) { let a = get_stack_power(game.attacker) @@ -1463,9 +1459,9 @@ states.movement = { game.major = 0 if (is_supply_train(p)) - resume_move_supply_train() + game.state = "move_supply_train" else - resume_move_general() + game.state = "move_general" }, confirm_end_movement() { this.end_movement() @@ -1483,7 +1479,6 @@ function format_move(max) { return ` up to ${n} cities.` } -// TODO: also force moving if in such a position and can move away function forbid_stopping_at(from) { switch (game.fx) { case NEXT_TURN_SOUBISE_AND_HILDBURGHAUSEN_MAY_NOT_ATTACK_WITH_THE_SAME_TC_SYMBOL: @@ -1500,22 +1495,6 @@ function forbid_stopping_at(from) { return false } -function forbid_attack(from, to) { - switch (game.fx) { - case NEXT_TURN_SOUBISE_AND_HILDBURGHAUSEN_MAY_NOT_ATTACK_WITH_THE_SAME_TC_SYMBOL: - return game.pos[GEN_SOUBISE] === from && game.ia_attack === get_space_suit(from) - case NEXT_TURN_NO_GENERAL_MAY_BE_ATTACKED_IN_THE_CITY_OF_HALLE: - return to === HALLE - case NEXT_TURN_CUMBERLAND_MAY_NOT_MOVE_INTO_ATTACK_POSITION: - return game.pos[GEN_CUMBERLAND] === from - case NEXT_TURN_SOUBISE_MAY_NOT_MOVE_INTO_ATTACK_POSITION: - return game.pos[GEN_SOUBISE] === from - case NEXT_TURN_FRIEDRICH_MAY_NOT_MOVE_INTO_ATTACK_POSITION: - return game.pos[GEN_FRIEDRICH] === from - } - return false -} - function forbid_capture(s) { switch (game.fx) { case NEXT_TURN_CUMBERLAND_MAY_NOT_MOVE_INTO_ATTACK_POSITION: @@ -1548,10 +1527,6 @@ function can_move_train_to(to) { return !has_any_piece(to) } -function can_continue_train_from(_) { - return true -} - function can_move_general_in_theory(p, to) { if (has_friendly_supply_train(to)) return false @@ -1595,65 +1570,6 @@ function can_continue_general_from(from) { return true } -function search_move(from, range, road_type, can_move_to, can_continue_from) { - let seen = [ from, -1 ] - let queue = [ from << 4 ] - while (queue.length > 0) { - let item = queue.shift() - let here = item >> 4 - let dist = (item & 15) + 1 - for (let next of data.cities[road_type][here]) { - if (map_has(seen, next)) - continue - if (!can_move_to(next)) - continue - if (dist <= range) { - map_set(seen, next, here) - if (can_continue_from(next)) - queue.push((next << 4) | dist) - } - } - } - return seen -} - -function resume_move_supply_train() { - if (game.count === 2 + game.major) { - end_move_piece() - } else { - let here = game.pos[game.selected[0]] - game.state = "move_supply_train" - if (game.major && game.count < 3) - game.move_major = search_move(here, 3 - game.count, "major_roads", can_move_train_to, can_continue_train_from) - else - game.move_major = [] - if (game.count < 2) - game.move_minor = search_move(here, 2 - game.count, "adjacent", can_move_train_to, can_continue_train_from) - else - game.move_minor = [] - } -} - -function resume_move_general() { - let range = movement_range() - if (game.count === range + game.major) { - end_move_piece() - } else { - game.state = "move_general" - /* NEW - let here = game.pos[game.selected[0]] - if (game.major && game.count < range+1) - game.move_major = search_move(here, range+1 - game.count, "major_roads", can_move_general_to, can_continue_general_from) - else - game.move_major = [] - if (game.count < range) - game.move_minor = search_move(here, range - game.count, "adjacent", can_move_general_to, can_continue_general_from) - else - game.move_minor = [] - */ - } -} - function move_general_to(to) { let pow = game.power let who = game.selected[0] @@ -1738,77 +1654,7 @@ function move_general_immediately(to) { } } -states.move_supply_train_NEW = { - inactive: "move", - prompt() { - prompt("Move supply train" + format_move(2)) - view.selected = game.selected - - let who = game.selected[0] - let here = game.pos[who] - - if (game.move_major) - map_for_each_key(game.move_major, s => { if (s !== here) gen_action_space(s) }) - if (game.move_minor) - map_for_each_key(game.move_minor, s => { if (s !== here) gen_action_space(s) }) - - view.move_major = game.move_major - view.move_minor = game.move_minor - - /* - if (game.count < 2 + game.major) - for (let next of data.cities.major_roads[here]) - if (!has_any_piece(next)) - gen_action_space(next) - if (game.count < 2) - for (let next of data.cities.roads[here]) - if (!has_any_piece(next)) - gen_action_space(next) - */ - - if (game.count > 0) { - gen_action_piece(who) - view.actions.stop = 1 - } - }, - piece(_) { - this.stop() - }, - stop() { - end_move_piece() - }, - space(to) { - let who = game.selected[0] - - set_add(game.moved, who) - game.pos[who] = to - - log("P" + who + " to S" + to) - - let m = map_get(game.move_major, to, 0) - if (m > 0) { - while (m > 0) { - // TODO: reverse - log(">S" + m) - m = map_get(game.move_major, m) - ++ game.count - } - } else { - m = map_get(game.move_minor, to, 0) - while (m > 0) { - // TODO: reverse - log(">S" + m) - m = map_get(game.move_minor, m) - ++ game.count - } - game.major = 0 - } - - resume_move_supply_train() - }, -} - -states.move_supply_train_OLD = { +states.move_supply_train = { inactive: "move", prompt() { prompt("Move supply train" + format_move(2)) @@ -1854,7 +1700,7 @@ states.move_supply_train_OLD = { }, } -states.move_general_NEW = { +states.move_general = { inactive: "move", prompt() { prompt("Move " + format_selected() + format_move(movement_range())) @@ -1864,110 +1710,20 @@ states.move_general_NEW = { let here = game.pos[who] if (game.count === 0) { - if (may_unstack()) { - if (game.selected.length > 1) - view.actions.detach = 1 - else - view.actions.detach = 0 - } - - let s_take = count_stacked_take() - let s_give = count_stacked_give() - let u_take = count_unstacked_take() - let u_give = count_unstacked_give() - if (s_take > 0 && u_give > 0) - view.actions.take = 1 - if (s_give > 0 && u_take > 0) - view.actions.give = 1 - } else { - if (forbid_stopping_at(here)) { - view.actions.stop = 0 + if (game.fx === NEXT_TURN_CHEVERT_MAY_NOT_UNSTACK && game.pos[GEN_CHEVERT] === here) { + view.prompt += " Chevert may not unstack." + if (count_generals(here) === 3) { + // two options: leave alone, or leave with chevert + // to leave with chevert, detach non-chevert + // to leave alone, detach non-chevert, then detach chevert + if (game.selected.length === 3) + for (let p of game.selected) + if (p !== GEN_CHEVERT) + gen_action_piece(p) + if (game.selected.length === 2) + gen_action_piece(GEN_CHEVERT) + } } else { - gen_action_piece(who) - view.actions.stop = 1 - } - } - - view.move_major = game.move_major - view.move_minor = game.move_minor - - if (game.move_major) - map_for_each_key(game.move_major, s => { if (s !== here) gen_action_space_or_piece(s) }) - if (game.move_minor) - map_for_each_key(game.move_minor, s => { if (s !== here) gen_action_space_or_piece(s) }) - }, - take() { - game.state = "move_take" - }, - give() { - game.state = "move_give" - }, - detach() { - game.state = "move_detach" - }, - piece(p) { - if (p === game.selected[0]) - this.stop() - else - this.space(game.pos[p]) - }, - stop() { - for (let p of game.selected) - set_add(game.moved, p) - end_move_piece() - }, - space(to) { - let who = game.selected[0] - let path = [ to ] - - let m = map_get(game.move_major, to, -1) - if (m >= 0) { - while (m >= 0) { - path.unshift(m) - m = map_get(game.move_major, m, -1) - ++game.count - } - } else { - m = map_get(game.move_minor, to, -1) - while (m >= 0) { - path.unshift(m) - m = map_get(game.move_minor, m, -1) - ++game.count - } - game.major = 0 - } - - log("P" + who + " " + path.map(s => "S" + s).join(" > ")) - - let stop = false - path.shift() // skip start space - for (let s of path) - stop ||= move_general_to(s) - - if (stop) - this.stop() - else - resume_move_general() - }, -} - -states.move_general_OLD = { - inactive: "move", - prompt() { - prompt("Move " + format_selected() + format_move(movement_range())) - view.selected = game.selected - - let who = game.selected[0] - let here = game.pos[who] - - if (game.count === 0) { - if (may_unstack()) { - /* - if (game.selected.length > 1) - view.actions.detach = 1 - else - view.actions.detach = 0 - */ if (game.selected.length > 1) for (let p of game.selected) gen_action_piece(p) @@ -2007,9 +1763,6 @@ states.move_general_OLD = { give() { game.state = "move_give" }, - detach() { - game.state = "move_detach" - }, piece(p) { if (game.count === 0) { if (set_has(game.selected, p)) @@ -2042,24 +1795,6 @@ states.move_general_OLD = { }, } -states.move_general = states.move_general_OLD -states.move_supply_train = states.move_supply_train_OLD -//states.move_general = states.move_general_NEW -//states.move_supply_train = states.move_supply_train_NEW - -states.move_detach = { - inactive: "move", - prompt() { - prompt("Move " + format_selected() + ". Detach general from stack.") - for (let p of game.selected) - gen_action_piece(p) - }, - piece(p) { - set_delete(game.selected, p) - game.state = "move_general" - }, -} - states.move_take = { inactive: "move", prompt() { @@ -2332,11 +2067,11 @@ function prompt_combat(value, extra = null) { } function inactive_attack() { - return "attack " + format_combat(game.count) + return "combat " + format_combat(game.count) } function inactive_defend() { - return "defend " + format_combat(-game.count) + return "combat " + format_combat(-game.count) } function goto_combat() { @@ -2357,10 +2092,8 @@ function goto_combat() { for (let a of from) { for (let b of to) { if (set_has(data.cities.adjacent[a], b)) { - if (!forbid_attack(a, b)) { - game.combat.push(a) - game.combat.push(b) - } + game.combat.push(a) + game.combat.push(b) } } } @@ -2897,7 +2630,7 @@ function search_retreat_possible_dfs(result, seen, here, range) { if (has_any_piece(next)) continue if (range === 1) { - map_set(result, next, seen.slice()) + set_add(result, next) } else { seen.push(next) search_retreat_possible_dfs(result, seen, next, range - 1) @@ -2924,12 +2657,9 @@ function search_retreat(loser, winner, range) { } let result = [] - map_for_each(possible, (s, path) => { - if (map_get(distance, s, -1) === max) { + for (let s of possible) + if (map_get(distance, s, -1) === max) result.push(s) - result.push(path) - } - }) return result } @@ -2938,15 +2668,14 @@ states.retreat = { prompt() { prompt("Retreat " + format_selected() + " " + Math.abs(game.count) + " cities.") view.selected = game.selected - view.retreat = game.retreat - map_for_each_key(game.retreat, gen_action_space) + for (let s of game.retreat) + gen_action_space(s) }, space(to) { push_undo() log("Retreated to S" + to + ".") - for (let p of game.selected) { + for (let p of game.selected) game.pos[p] = to - } delete game.retreat game.state = "retreat_done" }, @@ -3578,7 +3307,7 @@ function goto_flip_5_or_6_from_nearest_train(power, list) { } states.flip_5_or_6_from_nearest_train = { - inactive: "flip general out of supply", + inactive: "flip generals out of supply", prompt() { prompt("Flip " + format_selected() + " out of supply.") for (let p of game.selected) @@ -3592,7 +3321,7 @@ states.flip_5_or_6_from_nearest_train = { } states.flip_any_one_prussian_general_or_stack_in_austria_or_saxony = { - inactive: "flip general out of supply", + inactive: "flip any one Prussian general/stack in Austria or Saxony out of supply", prompt() { prompt("Flip any one Prussian general/stack in Austria or Saxony out of supply.") for (let p of game.selected) |