diff options
author | Tor Andersson <tor@ccxvii.net> | 2023-12-15 02:58:17 +0100 |
---|---|---|
committer | Tor Andersson <tor@ccxvii.net> | 2024-01-08 16:36:48 +0100 |
commit | b83115ce40d88c789fbbd5a891057e14d4cf553b (patch) | |
tree | fbccd0059aa41bc05ff7e97b2a5aaa3c2980aca1 | |
parent | 4640a9affe7633628ce95de0bb5640e30dd71039 (diff) | |
download | table-battles-b83115ce40d88c789fbbd5a891057e14d4cf553b.tar.gz |
stick shift special
-rw-r--r-- | data.js | 3 | ||||
-rw-r--r-- | play.html | 20 | ||||
-rw-r--r-- | play.js | 12 | ||||
-rw-r--r-- | rules.js | 192 | ||||
-rw-r--r-- | tools/cards.csv | 2 |
5 files changed, 218 insertions, 11 deletions
@@ -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" @@ -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; @@ -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") @@ -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. <p>If at the start of your turn you have no Infantry or Cavalry cards in play, you lose.<p>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.",
|