summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2023-12-15 02:58:17 +0100
committerTor Andersson <tor@ccxvii.net>2024-01-08 16:36:48 +0100
commitb83115ce40d88c789fbbd5a891057e14d4cf553b (patch)
treefbccd0059aa41bc05ff7e97b2a5aaa3c2980aca1
parent4640a9affe7633628ce95de0bb5640e30dd71039 (diff)
downloadtable-battles-b83115ce40d88c789fbbd5a891057e14d4cf553b.tar.gz
stick shift special
-rw-r--r--data.js3
-rw-r--r--play.html20
-rw-r--r--play.js12
-rw-r--r--rules.js192
-rw-r--r--tools/cards.csv2
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. <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.",