const fs = require("fs")
const { parse } = require("csv-parse/sync")
var card_records = parse(fs.readFileSync("tools/cards.csv", "utf-8"), { columns: true, records_with_empty_values: true })
var scenario_records = parse(fs.readFileSync("tools/scenarios.csv", "utf-8"), { columns: true, skip_records_with_empty_values: true })
var result = []
const WING = { red: 0, pink: 1, blue: 2, dkblue: 3 }
const WING_ICON = [ "\u2666", "\u2665", "\u2663", "\u2660" ]
const SQUEEZE_BOXES = [ "82A", "128A", "136B" , "274B", "291A" ]
const SQUEEZE_MARGINS = [ "91B", "239B", "274B", "291A", "69B" ]
const dice_text_map = {
"1-3": "1/2/3",
"2-4": "2/3/4",
"3-5": "3/4/5",
"4-6": "4/5/6",
"(1-3)": "(1/2/3)",
"(2-4)": "(2/3/4)",
"(3-5)": "(3/4/5)",
"(4-6)": "(4/5/6)",
}
var cards = [ ]
var card_index = {}
var cards_show = [ ]
var scenarios = []
var a_seq = 0
var b_seq = 0
/*
var message_map = {}
var msg_old = []
fs.readFileSync("tools/messages.txt", "utf-8").split("\n").forEach(line => {
line = line.trimEnd()
if (line === "")
return
if (line.startsWith("\t")) {
line = line.trimStart()
for (let old of msg_old)
message_map[old] = line
msg_old = []
} else {
msg_old.push(line)
}
})
*/
function get_html_effect(effect) {
effect = effect.replace(" If reduced to", " If reduced to")
effect = effect.replace(" Take dice", " Take dice")
effect = effect.replace(" (See", " (See")
effect = effect.replace(" You CHOOSE", " You CHOOSE")
return effect
}
function get_raw_effect(effect) {
effect = effect.replace(/ /, " ")
effect = effect.replace(/ \(See .*/, "")
effect = effect.replace(/ Take dice.*/, "")
effect = effect.replace(/ You CHOOSE the target./, "")
effect = effect.replace(/ Ignores Link./i, "")
effect = effect.replace(/ Warwick retires.*/, "")
return effect
}
function remap(x) {
if (x >= 42 && x <= 50) return (x - 42) + 51
if (x >= 51) x += 9
if (x >= 91 && x <= 99) return -1
if (x >= 99) x -= 9
if (x >= 216 && x <= 224) return x - 216 + 42
if (x >= 224) x -= 5
return x
}
let last_wing = null
var last_scen = null
var allcards = [
`
Table Battle Scenarios
`
]
var result = [ ]
var scenario_html = {}
function flush_scenario_html() {
scenario_html[last_scen] = result
result = []
}
function nbsp_target(target) {
return target
.split(", ")
.map(x=>x.replaceAll(" ", "\xa0"))
.join(", ")
.replaceAll("\xa0or\xa0", " or ")
.replaceAll("\xa0OR\xa0", " OR ")
}
for (let c of card_records) {
if (!c.number) {
allcards.push(`
${c.scenario}
`)
if (result.length > 2) {
flush_scenario_html()
}
last_wing = null
continue
}
last_scen = c.scenario
let html = []
if ((c.wing === "blue" || c.wing === "dkblue") && (last_wing === "red" || last_wing === "pink"))
html.push(`
`)
last_wing = c.wing
if (!c.name)
continue
c.dice = c.dice.replace(")/(", "/")
let card = {
scenario: parseInt(c.scenario),
number: c.number,
name: c.name,
wing: WING[c.wing],
morale: 1,
}
if (c.alias)
card.alias = c.alias
let id = card_index[c.number] = cards.length
cards.push(card)
if (c.strength === "I") {
card.special = 1
}
else if (c.strength === "II") {
card.special = 2
}
else if (c.strength === "III") {
card.special = 3
}
else if (c.strength === "IV") {
card.special = 4
}
else if (c.strength === "V") {
card.special = 5
}
else if (c.strength === "?") {
card.special = -1
}
else if (c.strength.endsWith("*")) {
card.strength = parseInt(c.strength)
card.morale = 2
}
else if (c.strength.endsWith("-")) {
card.strength = parseInt(c.strength)
card.morale = 0
}
else {
card.strength = parseInt(c.strength)
}
card.dice = c.dice
card.actions = []
let squeeze = 0
if (c.action1_effect && c.action2_effect && (c.rule_text_2 || c.lore_text))
squeeze = 1
else if (SQUEEZE_BOXES.includes(c.number))
squeeze = 1
if (SQUEEZE_MARGINS.includes(c.number)) {
card.squeeze = 1
html.push(`
`)
} else {
html.push(`
`)
}
if (c.symbol)
html.push(`
${c.name}
`)
else
html.push(`
${c.name}
`)
if (c.symbol === "inf") {
card.infantry = 1
html.push(``)
}
if (c.symbol === "cav") {
card.cavalry = 1
html.push(``)
}
if (card.strength)
html.push(`
${card.strength}
`)
else
html.push(`
${c.strength}
`)
if (c.link === "LR") {
card.link = [ id - 1, id + 1 ]
html.push(``)
html.push(``)
} else if (c.link === "L") {
html.push(``)
card.link = [ id - 1 ]
} else if (c.link === "R") {
html.push(``)
card.link = [ id + 1 ]
}
if (c.dice) {
let dice_text = dice_text_map[c.dice] || c.dice
dice_text = dice_text.replaceAll("-", " − ")
dice_text = dice_text.replaceAll("/", " / ")
dice_text = dice_text.replace("(", "( ")
dice_text = dice_text.replace(")", " )")
if (card.morale === 2)
html.push(`
★
`)
html.push(`
${dice_text}
`)
}
function make_action(type, requirement, target, effect, rule_text, short) {
let a = { type }
if (requirement) a.requirement = requirement
if (target) a.target = target
if (effect) a.effect_text = get_html_effect(effect)
if (effect) a.effect = get_raw_effect(effect)
if (rule_text) a.rule_text = rule_text
if (short) a.short = 1
return a
}
if (c.action1_type) {
let short = c.action1_effect && (c.rule_text_1 || squeeze)
card.actions.push(make_action(c.action1_type, c.action1_req, c.action1_target, c.action1_effect, c.action1_rule_text, short))
html.push(`
`)
if (/Screen|Absorb|Counterattack/.test(c.action1_type))
html.push(`
${c.action1_type}
`)
else
html.push(`
${c.action1_type}
`)
html.push(`
${c.action1_req}
`)
html.push(`
${nbsp_target(c.action1_target)}
`)
if (c.action1_effect) {
c.action1_effect = get_html_effect(c.action1_effect)
if (c.rule_text_1 || squeeze)
html.push(`
${c.action1_effect}
`)
else
html.push(`
${c.action1_effect}
`)
}
html.push(`
`)
}
if (c.rule_text_1) {
card.rule_text_1 = c.rule_text_1
html.push(`
${c.rule_text_1}
`)
}
if (c.action2_type || c.action2_effect) {
let short = c.action2_effect && (c.rule_text_2 || squeeze)
card.actions.push(make_action(c.action2_type, c.action2_req, c.action2_target, c.action2_effect, c.action2_rule_text, short))
html.push(`
`)
if (/Screen|Absorb|Counterattack/.test(c.action2_type))
html.push(`
${c.action2_type}
`)
else
html.push(`
${c.action2_type}
`)
html.push(`
${c.action2_req}
`)
html.push(`
${nbsp_target(c.action2_target)}
`)
if (c.action2_effect) {
c.action2_effect = get_html_effect(c.action2_effect)
if (c.rule_text_2 || squeeze)
html.push(`
${c.action2_effect}
`)
else
html.push(`
${c.action2_effect}
`)
}
html.push(`
`)
}
if (c.rule_text_2) {
card.rule_text_2 = c.rule_text_2
html.push(`
${c.rule_text_2}
`)
}
if (c.lore_text) {
card.lore_text = c.lore_text
html.push(`
${c.lore_text}
`)
}
if (c.rule) {
card.rules = {}
for (let rule of c.rule.split("; ")) {
if (/=/.test(rule)) {
let [ key, vals ] = rule.split("=")
card.rules[key] = vals.split(",")
} else {
card.rules[rule] = 1
}
}
}
if (c.reserve) {
if (c.reserve === "RETIRE, Ward") {
card.retire = 1
card.reserve = [ "Ward" ]
}
else if (c.reserve === "RETIRE, PURSUIT") {
card.retire = 1
card.pursuit = 1
}
else if (c.reserve === "RETIRE")
card.retire = 1
else if (c.reserve === "PURSUIT")
card.pursuit = 1
else if (c.reserve === "Commanded" || c.reserve === "See Above" || c.reserve === "Special Rule")
card.reserve = c.reserve
else
card.reserve = c.reserve.split(" or ")
if (card.retire && card.reserve)
html.push(`
RETIRE; RESERVE (${card.reserve.join(", ")})
`)
else if (card.retire && card.pursuit)
html.push(`
RETIRE, PURSUIT
`)
else if (card.retire)
html.push(`
RETIRE
`)
else if (card.pursuit)
html.push(`
PURSUIT
`)
else
html.push(`
IN RESERVE (${c.reserve})
`)
}
html.push(`
${c.number}
`)
html.push(`
${WING_ICON[card.wing]}
`)
html.push(`
`)
for (let line of html) {
result.push(line)
allcards.push(line)
}
if (c.number.endsWith("A"))
allcards.push(``)
else
allcards.push(``)
}
flush_scenario_html()
function find_card(scenario, name) {
name = name.replace("*", "")
name = name.replace(" (Voluntary)", "")
for (let c of cards)
if (c.scenario === scenario && (c.name === name || c.alias === name))
return card_index[c.number]
throw new Error("CARD NOT FOUND: " + scenario + " " + name)
}
function find_enemy_cards(scenario, wing) {
let w1, w2
if (wing === 0 || wing === 1)
w1 = 2, w2 = 3
if (wing === 2 || wing === 3)
w1 = 0, w2 = 1
let list = []
for (let c of cards)
if (c.scenario === scenario && (c.wing === w1 || c.wing === w2))
list.push(card_index[c.number])
return list
}
function find_friendly_cards(scenario, wing) {
let w1, w2
if (wing === 0 || wing === 1)
w1 = 0, w2 = 1
if (wing === 2 || wing === 3)
w1 = 2, w2 = 3
let list = []
for (let c of cards)
if (c.scenario === scenario && (c.wing === w1 || c.wing === w2))
list.push(card_index[c.number])
return list
}
function find_wing_cards(scenario, wing) {
let list = []
for (let c of cards)
if (c.scenario === scenario && c.wing === wing)
list.push(card_index[c.number])
return list
}
/* process action and reserve targets */
function process_card(c, ix) {
for (let a of c.actions) {
if (a.target) {
let tname = a.target.replace(" out of reserve", "")
if (tname === "Any enemy attack" || tname === "Any enemy formation" || tname === "Any enemy attacking it")
a.target_list = find_enemy_cards(c.scenario, c.wing)
else if (tname === "Any friendly formation")
a.target_list = find_friendly_cards(c.scenario, c.wing)
else if (tname === "Any friendly Pink formation")
a.target_list = find_wing_cards(c.scenario, WING.pink)
else if (tname === "Any other Pink formation")
a.target_list = find_wing_cards(c.scenario, WING.pink).filter(x => x !== c)
else if (tname === "Any Red formation")
a.target_list = find_wing_cards(c.scenario, WING.red)
else if (tname === "Any Pink formation")
a.target_list = find_wing_cards(c.scenario, WING.pink)
else if (tname === "Any Blue formation")
a.target_list = find_wing_cards(c.scenario, WING.blue)
else if (tname === "Any Dark Blue formation")
a.target_list = find_wing_cards(c.scenario, WING.dkblue)
else if (tname.startsWith("Any attack on "))
a.target_list = tname.replace("Any attack on ", "").split(" or ").map(name => find_card(c.scenario, name))
else if (tname === "Any friendly but Little Round Top")
a.target_list = find_friendly_cards(c.scenario, c.wing).filter(x => x !== find_card(c.scenario, "Little Round Top"))
else if (tname === 'Activate "Retreat to Nivelles"')
a.target_list = null
else if (tname === "The Fog Lifts...")
a.target_list = null
else if (tname === "See Below")
a.target_list = null
else
a.target_list = tname.split(/, | OR | or | and /).map(name => find_card(c.scenario, name))
}
if (a.type === "Absorb") {
a.target_list = a.target_list.filter(x => x !== ix) // never absorb for self
}
if (a.type === "Screen")
a.choice = 1
else if (/, /.test(a.target) || !a.target_list || a.target_list.length < 2)
a.choice = 0
else
a.choice = 1
}
// Hohenfriedberg invisible charles last target in list!
if (c.number === "267A" || c.number === "268A" || c.number === "269A")
c.actions[0].target_list.push(find_card(c.scenario, "Charles"))
if (c.rules) {
for (let key in c.rules) {
let val = c.rules[key]
if (Array.isArray(val))
c.rules[key] = val.map(number => card_index[number])
}
}
if (c.pursuit) {
if (c.actions[0].type !== "Attack" && c.actions[0].type !== "Counterattack")
throw new Error("PURSUIT without Attack or Counterattack as first action")
if (c.actions[0].target_list.length !== 1)
throw new Error("PURSUIT with more than one target!")
c.pursuit = c.actions[0].target_list[0]
}
if (Array.isArray(c.reserve))
c.reserve = c.reserve.map(name => find_card(c.scenario, name))
}
let failed = false
cards.forEach((c, ix) => {
try {
process_card(c, ix)
} catch (err) {
console.log(err)
failed = true
}
})
fs.writeFileSync("info/ref.html", allcards.join("\n"))
result = [
`
Table Battle Scenarios
`
]
function parse_cards(text) {
let out = []
let list = text.split(",")
for (let range of list) {
let [a, b] = range.split("-")
a = card_index[a]
b = card_index[b]
if (a === undefined || b === undefined)
throw new Error("MISSING CARDS FOR " + text)
for (let i = a; i <= b; ++i)
out.push(i)
}
return out
}
let last_exp = null
for (let s of scenario_records) {
if (!s.name)
continue
if (last_exp !== s.expansion) {
result.push(`