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(`
`) if ((last_wing === "blue" || last_wing === "dkblue") && (c.wing === "red" || c.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(`

${s.expansion}

`) last_exp = s.expansion } try { scenarios.push({ number: parseInt(s.number), expansion: s.expansion, name: s.name, date: s.date, players: [ { name: s.player1, cards: parse_cards(s.cards1), morale: parseInt(s.morale1), tactical: parseInt(s.tactical1) || undefined }, { name: s.player2, cards: parse_cards(s.cards2), morale: parseInt(s.morale2), tactical: parseInt(s.tactical2) || undefined }, ], rule: s.rule || undefined, rule_text: s.rule_text || undefined, lore_text: s.lore_text || undefined, wikipedia: s.wikipedia, }) var html = `
${s.name}
${s.date}
${s.player1}
Cards ${s.cards1.replace(",",", ").replaceAll("-"," – ")}
Morale: ${s.morale1}
${s.tactical1 ? "Tactical Victory: " + s.tactical1 : ""}
${s.player2}
Cards ${s.cards2.replace(",",", ").replaceAll("-"," – ")}
Morale: ${s.morale2}
${s.name !== "Fleurus" && s.tactical2 ? "Tactical Victory: " + s.tactical2 : ""}
${s.rule_text}
${s.lore_text}
${s.number}
` scenario_html[s.number].unshift(`
`) scenario_html[s.number].unshift(html) scenario_html[s.number].unshift( ` ${s.number} - ${s.name}

${s.number} - ${s.name}

`) fs.writeFileSync(`info/s_${s.number}.html`, scenario_html[s.number].join("\n")) result.push(``, html, '') // result.push(``) } catch (err) { console.log(err) } } fs.writeFileSync("info/scenarios.html", result.join("\n")) result = [] result.push("const data = {") result.push("scenarios: [") for (let row of scenarios) result.push(JSON.stringify(row) + ",") result.push("],\ncards: [") for (let row of cards) result.push(JSON.stringify(row) + ",") result.push("],\n}") result.push("if (typeof module!=='undefined') module.exports = data") fs.writeFileSync("data.js", result.join("\n"))