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" ] 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", "<br>If reduced to") effect = effect.replace(" Take dice", "<br>Take dice") effect = effect.replace(" (See", "<br>(See") return effect } function get_raw_effect(effect) { effect = effect.replace(/<br>/, " ") 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 = [ `<!doctype html> <html lang="en"> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <head> <title>Table Battle Scenarios</title> <link rel="stylesheet" href="/fonts/fonts.css"> <link rel="stylesheet" href="/table-battles/cards.css"> <style> body{background-color:dimgray;color:whitesmoke;padding:20px;background-image:url(../images/background.png)} .list{display:grid;grid-template-columns:min-content min-content;gap:20px;margin-bottom:40px} a{text-decoration:none;color:black} </style> </head> <body> <div class="list">` ] 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(`</div><h1>${c.scenario}</h1><div class="list">`) 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(`</div><div class="list">`) if ((last_wing === "blue" || last_wing === "dkblue") && (c.wing === "red" || c.wing === "pink")) html.push(`</div><div class="list">`) 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(`<div class="formation card squeeze">`) } else { html.push(`<div class="formation card">`) } if (c.symbol) html.push(`<div class="name with_symbol ${c.wing}">${c.name}</div>`) else html.push(`<div class="name ${c.wing}">${c.name}</div>`) if (c.symbol === "inf") { card.infantry = 1 html.push(`<div class="symbol infantry"></div>`) } if (c.symbol === "cav") { card.cavalry = 1 html.push(`<div class="symbol cavalry"></div>`) } if (card.strength) html.push(`<div class="strength">${card.strength}</div>`) else html.push(`<div class="strength">${c.strength}</div>`) if (c.link === "LR") { card.link = [ id - 1, id + 1 ] html.push(`<div class="link left"></div>`) html.push(`<div class="link right"></div>`) } else if (c.link === "L") { html.push(`<div class="link left"></div>`) card.link = [ id - 1 ] } else if (c.link === "R") { html.push(`<div class="link right"></div>`) card.link = [ id + 1 ] } if (c.dice) { if (card.morale === 2) html.push(`<div class="star">★</div>`) html.push(`<div class="dice_area">${c.dice}</div>`) } 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(`<div class="action_row">`) if (/Screen|Absorb|Counterattack/.test(c.action1_type)) html.push(`<div class="action_type reaction">${c.action1_type}</div>`) else html.push(`<div class="action_type">${c.action1_type}</div>`) html.push(`<div class="action_requirement">${c.action1_req}</div>`) html.push(`<div class="action_target">${nbsp_target(c.action1_target)}</div>`) if (c.action1_effect) { c.action1_effect = get_html_effect(c.action1_effect) if (c.rule_text_1 || squeeze) html.push(`<div class="action_effect short">${c.action1_effect}</div>`) else html.push(`<div class="action_effect">${c.action1_effect}</div>`) } html.push(`</div>`) } if (c.rule_text_1) { card.rule_text_1 = c.rule_text_1 html.push(`<div class="rule_text">${c.rule_text_1}</div>`) } 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(`<div class="action_row">`) if (/Screen|Absorb|Counterattack/.test(c.action2_type)) html.push(`<div class="action_type reaction">${c.action2_type}</div>`) else html.push(`<div class="action_type">${c.action2_type}</div>`) html.push(`<div class="action_requirement">${c.action2_req}</div>`) html.push(`<div class="action_target">${nbsp_target(c.action2_target)}</div>`) if (c.action2_effect) { c.action2_effect = get_html_effect(c.action2_effect) if (c.rule_text_2 || squeeze) html.push(`<div class="action_effect short">${c.action2_effect}</div>`) else html.push(`<div class="action_effect">${c.action2_effect}</div>`) } html.push(`</div>`) } if (c.rule_text_2) { card.rule_text_2 = c.rule_text_2 html.push(`<div class="rule_text">${c.rule_text_2}</div>`) } if (c.lore_text) { card.lore_text = c.lore_text html.push(`<div class="lore_text">${c.lore_text}</div>`) } 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(`<div class="reserve">RETIRE; RESERVE (${card.reserve.join(", ")})</div>`) else if (card.retire && card.pursuit) html.push(`<div class="reserve">RETIRE, PURSUIT</div>`) else if (card.retire) html.push(`<div class="reserve">RETIRE</div>`) else if (card.pursuit) html.push(`<div class="reserve">PURSUIT</div>`) else html.push(`<div class="reserve">IN RESERVE (${c.reserve})</div>`) } html.push(`<div class="number">${c.number}</div>`) html.push(`<div class="extra">${WING_ICON[card.wing]}</div>`) html.push(`</div>`) for (let line of html) { result.push(line) allcards.push(line) } if (c.number.endsWith("A")) allcards.push(`<img class="ref" height="338" src="/table-battles/ref2/sliced/card_A${remap(a_seq++)}.jpg">`) else allcards.push(`<img class="ref" height="338" src="/table-battles/ref2/sliced/card_B${remap(b_seq++)}.jpg">`) } 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 = [ `<!doctype html> <html lang="en"> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <head> <title>Table Battle Scenarios</title> <link rel="stylesheet" href="/fonts/fonts.css"> <link rel="stylesheet" href="/table-battles/cards.css"> <style> body{background-color:dimgray;color:whitesmoke;padding:20px;background-image:url(../images/background.png)} .list{display:flex;flex-wrap:wrap;gap:20px;margin-bottom:40px} a{text-decoration:none;color:black} </style> </head> <body> <div class="list">` ] 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(`</div><h1>${s.expansion}</h1><div class="list">`) 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 = `<div id="scenario_${s.number}" class="scenario card"> <div class="scenario_title"> <div class="battle_name">${s.name}</div> <div class="battle_date">${s.date}</div> </div> <div class="scenario_player"> <div class="scenario_player_name">${s.player1}</div> <div class="scenario_line">Cards ${s.cards1}</div> <div class="scenario_line">Morale: ${s.morale1}</div> <div class="scenario_line">${s.tactical1 ? "Tactical Victory: " + s.tactical1 : ""}</div> </div> <div class="scenario_player"> <div class="scenario_player_name">${s.player2}</div> <div class="scenario_line">Cards ${s.cards2}</div> <div class="scenario_line">Morale: ${s.morale2}</div> <div class="scenario_line">${s.name !== "Fleurus" && s.tactical2 ? "Tactical Victory: " + s.tactical2 : ""}</div> </div> <div class="rule_text">${s.rule_text}</div> <div class="lore_text">${s.lore_text}</div> <div class="number">${s.number}</div> </div>` scenario_html[s.number].unshift(`</div><div class="list">`) scenario_html[s.number].unshift(html) scenario_html[s.number].unshift( `<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>${s.number} - ${s.name}</title> <link rel="stylesheet" href="/fonts/fonts.css"> <link rel="stylesheet" href="/table-battles/cards.css"> <style> body{background-color:dimgray;color:whitesmoke;padding:20px;background-image:url(../images/background.png)} .list{display:flex;flex-wrap:wrap;gap:20px;margin-bottom:40px} </style> </head> <body> <h1>${s.number} - ${s.name}</h1> <div class="list">`) fs.writeFileSync(`info/s_${s.number}.html`, scenario_html[s.number].join("\n")) result.push(`<a href="/table-battles/info/s_${s.number}.html">`, html, '</a>') // result.push(`<img src="/table-battles/paintings/${s.name.toLowerCase().replaceAll(" ", "_")}.jpg">`) } 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"))