From b83115ce40d88c789fbbd5a891057e14d4cf553b Mon Sep 17 00:00:00 2001 From: Tor Andersson Date: Fri, 15 Dec 2023 02:58:17 +0100 Subject: stick shift special --- data.js | 3 + play.html | 20 ++++++ play.js | 12 ++++ rules.js | 192 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- tools/cards.csv | 2 +- 5 files changed, 218 insertions(+), 11 deletions(-) diff --git a/data.js b/data.js index 2f52503..2ee9bb8 100644 --- a/data.js +++ b/data.js @@ -16431,6 +16431,9 @@ cards: [ } ], "cavalry": 1, + "rules": { + "attack_choose_target": 1 + }, "rule_text": "After making its first and only attack, this card is removed from play (this does not count as a Rout).", "lore_text": "The justly famous charge of the Bayreuth Dragoons decisively broke the enemy line, securing for Frederick a major victory.", "reserve": "Commanded" diff --git a/play.html b/play.html index acdb235..86f9599 100644 --- a/play.html +++ b/play.html @@ -92,10 +92,15 @@ main { .table_reserve, .table_front, .table_pool { display: flex; justify-content: center; + align-items: start; gap: 24px; padding: 0 23px; } +.flip .table_reserve, .flip .table_front { + align-items: end; +} + .table_front { min-height: 509px; } @@ -135,6 +140,21 @@ main { margin: 12px; } +.slot_shift { + min-height: 11px; + display: flex; + flex-direction: column; + justify-content: end; + align-content: center; + flex-wrap: wrap; + gap: 1px; + margin: 12px; +} + +.slot_shift:empty { + display: none; +} + .slot_dice { display: flex; height: 36px; diff --git a/play.js b/play.js index 7eb87af..c51f2c0 100644 --- a/play.js +++ b/play.js @@ -19,6 +19,7 @@ let ui = { cards: {}, slots: {}, slot_sticks: {}, + slot_shift: {}, slot_cubes: {}, slot_dice: {}, @@ -127,6 +128,8 @@ function create_formation_slot(id, top) { if (top) { ui.slot_dice[id] = append_div(e, "slot_dice") + if (card.infantry || card.cavalry) + ui.slot_shift[id] = append_div(e, "slot_shift") e.appendChild(ui.cards[id]) } @@ -137,6 +140,8 @@ function create_formation_slot(id, top) { if (!top) { e.appendChild(ui.cards[id]) + if (card.infantry || card.cavalry) + ui.slot_shift[id] = append_div(e, "slot_shift") ui.slot_dice[id] = append_div(e, "slot_dice") } @@ -275,6 +280,12 @@ function fill_card_row(top, parent, list) { add_hit_stick(ui.slot_sticks[id]) for (let i = x; i < n; ++i) add_stick(ui.slot_sticks[id]) + + if (view.shift) { + n = map_get(view.shift, id, 0) + for (let i = 0; i < n; ++i) + add_stick(ui.slot_shift[id]) + } } } @@ -402,6 +413,7 @@ function on_update() { action_button("screen", "Screen") action_button("counterattack", "Counterattack") action_button("absorb", "Absorb") + action_button("shift", "Shift") action_button("roll", "Roll") action_button("pass", "Pass") action_button("next", "Next") diff --git a/rules.js b/rules.js index 121049f..c1f9f36 100644 --- a/rules.js +++ b/rules.js @@ -138,6 +138,9 @@ exports.view = function (state, player) { view.hits2 = game.hits2 } + if (game.shift) + view.shift = game.shift + if (game.state === "game_over") { view.prompt = game.victory } else if (player !== game.active) { @@ -316,19 +319,19 @@ const S43_VILLARS_LEFT = find_card(43, "Villars's Left") const S43_BROGLIE = find_card(43, "Broglie") const S43_PRINCE_DE_TINGRY = find_card(43, "Prince de Tingry") -const S44_HOTHENFRIEDBERG = find_scenario(44) +const S44_HOHENFRIEDBERG = find_scenario(44) const S44_CHARLES = find_card(44, "Charles") const S44_FREDERICK_II = find_card(44, "Frederick II") const S44_BAYREUTH_DRAGOONS = find_card(44, "Bayreuth Dragoons") const S44_LEOPOLDS_L = find_card(44, "Leopold's Left") const S44_LEOPOLDS_C = find_card(44, "Leopold's Center") const S44_LEOPOLDS_R = find_card(44, "Leopold's Right") +const S44_DU_MOULIN = find_card(44, "Du Moulin") +const S44_SAXON_HORSE = find_card(44, "Saxon Horse") // === SETUP === exports.setup = function (seed, scenario, options) { - // TODO: "Random" - scenario = parseInt(scenario) scenario = data.scenarios.findIndex(s => s.number === scenario) if (scenario < 0) @@ -381,6 +384,10 @@ exports.setup = function (seed, scenario, options) { hits2: 0, } + // Charles Alexander of Lorraine -- shift special + if (info.number >= 44 && info.number <= 49) + game.shift = [] + function setup_formation(front, reserve, c) { let card = data.cards[c] if (card.reserve) @@ -509,6 +516,21 @@ function set_sticks(c, n) { map_set(game.sticks, c, n) } +function get_shift_sticks(c) { + if (game.shift) + return map_get(game.shift, c, 0) + return 0 +} + +function set_shift_sticks(c, n) { + if (game.shift) { + if (n) + map_set(game.shift, c, n) + else + map_delete(game.shift, c) + } +} + function remove_sticks(c, n) { let p = find_card_owner(c) let old = get_sticks(c) @@ -570,22 +592,26 @@ function eliminate_card(c) { function rout_card(c) { let p = find_card_owner(c) game.lost[p] += get_sticks(c) + game.lost[p] += get_shift_sticks(c) log(c + " routed.") eliminate_card(c) } function pursue_card(c) { log(c + " pursued.") + game.lost[p] += get_shift_sticks(c) // TODO ? eliminate_card(c) } function retire_card(c) { log(c + " retired.") + game.lost[p] += get_shift_sticks(c) // TODO ? eliminate_card(c) } function remove_card(c) { log(c + " removed.") + game.lost[p] += get_shift_sticks(c) // TODO ? eliminate_card(c) } @@ -1654,6 +1680,12 @@ function is_reaction(c, a) { } function is_mandatory_reaction(c, a) { + + if (game.scenario === S44_HOHENFRIEDBERG) { + if (c === S44_DU_MOULIN && game.selected === S44_SAXON_HORSE) + return false + } + return ( a.requirement !== "Voluntary" && a.requirement !== "Pair, Voluntary" @@ -1746,6 +1778,11 @@ function can_take_any_action() { } } + if (can_shift_any_infantry()) + return true + if (can_shift_any_cavalry()) + return true + return false } @@ -1767,12 +1804,25 @@ function count_cards_remaining_from_wing(w) { } function goto_start_turn() { + let p = player_index() + if (check_impossible_to_attack_victory()) return + // TODO: manual step to shift? + if (game.shift) { + for (let c of game.front[p]) { + let n = get_shift_sticks(c) + if (n > 0) { + set_sticks(c, get_sticks(c) + n) + set_shift_sticks(c, 0) + } + } + } + if (game.scenario === S25_WHEATFIELD) { // Rout Stony Hill at start of Union turn if it is the only Blue card left. - if (player_index() === 1) { + if (p === 1) { if (is_card_in_play(S25_STONY_HILL)) { if (count_cards_remaining_from_wing(BLUE) === 1) { game.state = "s25_stony_hill" @@ -1782,8 +1832,8 @@ function goto_start_turn() { } } - if (game.scenario === S44_HOTHENFRIEDBERG) { - if (player_index() === 1) { + if (game.scenario === S44_HOHENFRIEDBERG) { + if (p === 1) { let have_inf_or_cav = false for (let c of game.front[1]) if (is_infantry(c) || is_cavalry(c)) @@ -1865,6 +1915,9 @@ states.action = { } } + if (can_shift_any_infantry() || can_shift_any_cavalry()) + view.actions.shift = 1 + if (game.scenario === S40_CHIARI) { if (player_index() === 1) { if (s40_can_take_cassines_action(S40_CASSINES_I, S40_NIGRELLI, S40_KRIECHBAUM)) @@ -1915,6 +1968,10 @@ states.action = { goto_roll_phase() roll_dice_in_pool() }, + shift() { + push_undo() + game.state = "shift_from" + }, card(c) { push_undo() @@ -1941,6 +1998,87 @@ states.action = { } } +function can_shift_any_infantry() { + if (game.shift) { + let n = 0, m = 0 + for (let c of game.front[player_index()]) { + if (is_infantry(c)) { + if (get_sticks(c) > 1) + ++m + ++n + } + } + return n > 1 && m > 0 + } + return false +} + +function can_shift_any_cavalry() { + if (game.shift) { + let n = 0, m = 0 + for (let c of game.front[player_index()]) { + if (is_cavalry(c)) { + if (get_sticks(c) > 1) + ++m + ++n + } + } + return n > 1 && m > 0 + } + return false +} + +states.shift_from = { + prompt() { + view.prompt = "Shift sticks from one Formation to another." + let p = player_index() + if (can_shift_any_infantry()) + for (let c of game.front[p]) + if (is_infantry(c) && get_sticks(c) > 1) + gen_action_card(c) + if (can_shift_any_cavalry()) + for (let c of game.front[p]) + if (is_cavalry(c) && get_sticks(c) > 1) + gen_action_card(c) + }, + card(c) { + game.selected = c + game.target2 = -1 + game.state = "shift_to" + }, +} + +states.shift_to = { + prompt() { + view.prompt = "Shift sticks from " + card_name(game.selected) + "." + let p = player_index() + if (game.target2 < 0) { + if (is_infantry(game.selected)) + for (let c of game.front[p]) + if (c !== game.selected && is_infantry(c)) + gen_action_card(c) + if (is_cavalry(game.selected)) + for (let c of game.front[p]) + if (c !== game.selected && is_cavalry(c)) + gen_action_card(c) + } else { + gen_action_card(game.target2) + view.actions.next = 1 + } + }, + card(c) { + game.target2 = c + set_sticks(game.selected, get_sticks(game.selected) - 1) + set_shift_sticks(game.target2, get_shift_sticks(game.target2) + 1) + if (get_sticks(game.selected) === 1) + this.next() + }, + next() { + // TODO: skip action phase? + end_action_phase() + }, +} + states.s40_cassines = { prompt() { view.prompt = "Cassines: Move one unit stick to this card." @@ -2066,7 +2204,7 @@ function find_first_target_of_command(c, a) { return S37_THE_FOG } - if (game.scenario === S44_HOTHENFRIEDBERG) { + if (game.scenario === S44_HOHENFRIEDBERG) { if (c === S44_CHARLES) { if (game.reserve[1].length > 0) return game.reserve[1] @@ -2092,7 +2230,7 @@ function find_first_target_of_command(c, a) { function find_all_targets_of_command(c, a) { - if (game.scenario === S44_HOTHENFRIEDBERG) { + if (game.scenario === S44_HOHENFRIEDBERG) { if (c === S44_CHARLES) { return game.reserve[1].slice() } @@ -2268,7 +2406,7 @@ function update_attack1(direct) { game.hits *= 2 } - if (game.scenario === S44_HOTHENFRIEDBERG) { + if (game.scenario === S44_HOHENFRIEDBERG) { if (game.target === S44_CHARLES) { if (game.selected === S44_LEOPOLDS_L || game.selected === S44_LEOPOLDS_C || game.selected === S44_LEOPOLDS_R) game.self = 0 @@ -2406,6 +2544,13 @@ function resume_attack() { if (game.target2 >= 0) remove_sticks(game.target2, game.hits2) + if (game.scenario === S44_HOHENFRIEDBERG) { + // remove after first attack + if (game.selected === S44_BAYREUTH_DRAGOONS) { + remove_card(S44_BAYREUTH_DRAGOONS) + } + } + end_action_phase() } @@ -2465,7 +2610,7 @@ states.command = { } } - if (game.scenario === S44_HOTHENFRIEDBERG) { + if (game.scenario === S44_HOHENFRIEDBERG) { // one at a time if (game.selected === S44_CHARLES || game.selected === S44_FREDERICK_II) { pay_for_action(game.selected) @@ -2872,6 +3017,8 @@ function get_attack_hits(c, a) { return 1 + count_dice_on_card(c) case "2 hits, PLUS 1 hit per die. 1 self per action.": return 2 + count_dice_on_card(c) + case "Two hits per die.": + return 2 * count_dice_on_card(c) case "2 hits.": return 2 case "5 hits.": @@ -2890,6 +3037,7 @@ function get_attack_self(c, a) { case "1 hit per die. Ignore first target until it comes out of Reserve.": case "1 hit per die (plus dice from E. Phalanx).": case "1 hit per pair.": + case "Two hits per die.": case "2 hits.": case "5 hits.": return 0 @@ -3296,6 +3444,13 @@ function array_insert(array, index, item) { array[index] = item } +function array_remove_pair(array, index) { + let n = array.length + for (let i = index + 2; i < n; ++i) + array[i - 2] = array[i] + array.length = n - 2 +} + function array_insert_pair(array, index, key, value) { for (let i = array.length; i > index; i -= 2) { array[i] = array[i-2] @@ -3411,3 +3566,20 @@ function map_set(map, key, value) { } array_insert_pair(map, a<<1, key, value) } + +function map_delete(map, item) { + let a = 0 + let b = (map.length >> 1) - 1 + while (a <= b) { + let m = (a + b) >> 1 + let x = map[m<<1] + if (item < x) + b = m - 1 + else if (item > x) + a = m + 1 + else { + array_remove_pair(map, m<<1) + return + } + } +} diff --git a/tools/cards.csv b/tools/cards.csv index cad7293..4fb3fa5 100644 --- a/tools/cards.csv +++ b/tools/cards.csv @@ -637,7 +637,7 @@ HOHENFRIEDBERG,,,,,,,,,,,,,,,,,,,, 44,264A,dkblue,The Striegau River,,,III,Doubles,,,Command,Three Cubes,Frederick II out of reserve,,,,,,,,The bridge at the Striegau River created a bottleneck for Frederick's columns. 44,265A,blue,Frederick II,,,1,4-6,,Commanded,Command,,See Below,,,,,,"command_sequence=267A,268A,269A,270A,271A,272A","Each Command action brings into play the next card in this sequence: Leopold's Right, Leopold's Center, Leopold's Left, Nassau, Zieten, Bayreuth Dragoons.

If at the start of your turn you have no Infantry or Cavalry cards in play, you lose.

Note that the cards are numbered in their order of arrival, and are thus given in a right-to-left rather than the usual left-to-right order.", 44,266A,blue,Du Moulin,,cav,6,6,,,Attack,,Saxon Horse,1 hit per die. 1 self per action.,Screen,Pair,"Saxon Horse (Voluntary), Weissenfels",,,, -44,272A,dkblue,Bayreuth Dragoons,,cav,1,1-3,,Commanded,Attack,,Lorraine's Left or Lorraine's Right,Two hits per die.,,,,,,"After making its first and only attack, this card is removed from play (this does not count as a Rout).","The justly famous charge of the Bayreuth Dragoons decisively broke the enemy line, securing for Frederick a major victory." +44,272A,dkblue,Bayreuth Dragoons,,cav,1,1-3,,Commanded,Attack,,Lorraine's Left or Lorraine's Right,Two hits per die.,,,,,attack_choose_target,"After making its first and only attack, this card is removed from play (this does not count as a Rout).","The justly famous charge of the Bayreuth Dragoons decisively broke the enemy line, securing for Frederick a major victory." 44,271A,blue,Zieten,,cav,4,4-6,R,Commanded,Attack,,Austrian Horse,1 hit per die. 1 self per action.,Screen,Pair,Austrian Horse,,,, 44,270A,blue,Nassau,,cav,6,3/4,L,Commanded,Attack,,Austrian Horse,1 hit per die. 1 self per action.,Counterattack,Pair,Austrian Horse,1 hit.,,, 44,269A,dkblue,Leopold's Left,,inf,6,5/6,R,Commanded,Attack,,"Lorraine's Right, Lorraine's Left",1 hit per die. 1 self per action.,Counterattack,Pair,"Lorraine's Right, Lorraine's Left",1 hit.,,"If neither target is in play, may attack Charles and suffers no self hits.", -- cgit v1.2.3