aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2025-06-08 20:34:11 -0500
committerFlorian Fischer <florian.fischer@muhq.space>2025-07-03 22:01:22 -0400
commit27f2934a62030eaf1bf6cc1f7f34802d3e1b86f3 (patch)
tree66ec0ade0d9aa4b63a697d20f3eee327759ebfdd /scripts
parentb9cb10d93c802280f4bc7f9cb42bb6b596edcf10 (diff)
downloadmuhqs-game-27f2934a62030eaf1bf6cc1f7f34802d3e1b86f3.tar.gz
muhqs-game-27f2934a62030eaf1bf6cc1f7f34802d3e1b86f3.zip
add boss overview pages
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/generate_boss_html.py303
1 files changed, 303 insertions, 0 deletions
diff --git a/scripts/generate_boss_html.py b/scripts/generate_boss_html.py
new file mode 100755
index 00000000..46ebd15f
--- /dev/null
+++ b/scripts/generate_boss_html.py
@@ -0,0 +1,303 @@
+#!/usr/bin/env python3
+"""Generate html page of an AI boss opponent"""
+
+# TODO: support multiple languages
+
+import argparse
+from pathlib import Path
+from string import Template
+import re
+import yaml
+
+from data import name2file
+import generate_card_hover_links
+
+SETS = {"kraken": "kraken", "tyrant": "tyrant"}
+NAMES = {"kraken": "The Kraken", "tyrant": "The Tyrant"}
+DESCS = {
+ "kraken":
+ "Face the evil of the sea. Overcome the kraken, the fierce epicenter of the ozean, which employs creatures of the depth to stop your offense.<br>Since the kraken does not move and can not win it is the perfect boss to start.",
+ "tyrant": "" # TODO
+}
+
+TEMPLATE = """<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" lang="$lang" xml:lang="$lang">
+<head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
+ <title>${boss_name}</title>
+ <style>
+:root {
+ /* define applied colors */
+ --bg: #fdfdfd;
+ --fg: #1a1a1a;
+ --link-color: #1a1a1a;
+ --pre-bg: #fdfdfd;
+}
+@media (prefers-color-scheme: dark) {
+ /* change apllied colors to the dark gruvbox pallet
+ * https://github.com/morhetz/gruvbox
+ */
+ :root {
+ --bg: #282828; /* dark bg */
+ --fg: #ebdbb2; /* dark fg */
+ --link-color: #d79921; /* yellow */
+ --pre-bg: #1d2021; /* dark bg0_h */
+ }
+}
+html {
+ line-height: 1.5;
+ font-family: Georgia, serif;
+ font-size: 20px;
+ color: var(--fg);
+ background-color: var(--bg);
+}
+body {
+ margin: 0 auto;
+ max-width: 36em;
+ padding: 50px;
+ hyphens: auto;
+ word-wrap: break-word;
+ text-rendering: optimizeLegibility;
+ font-kerning: normal;
+}
+.main {
+ //display: flex;
+ //flex-flow: row wrap;
+ display: grid;
+ grid-template-columns: 3fr 2fr;
+ grid-template-rows: 200px auto
+
+ max-width: 100%;
+ overflow:hidden;
+}
+.intro {
+ grid-column-start: 1;
+ grid-row-start: 1;
+}
+.card {
+ margin-top: 2em;
+ grid-column-start: 2;
+ grid-row-start: 1;
+ grid-row-end: 3;
+}
+.content {
+ grid-column-start: 1;
+ grid-row-start: 2;
+}
+@media (max-width: 600px) {
+ body {
+ font-size: 0.9em;
+ padding: 1em;
+ }
+ .main {
+ grid-template-columns: 1fr
+ }
+ .card {
+ margin-top: 0;
+ grid-column-start: 1;
+ grid-row-start: 2;
+ }
+ .content {
+ grid-row-start: 3;
+ }
+}
+@media print {
+ body {
+ background-color: transparent;
+ color: black;
+ font-size: 12pt;
+ }
+ p, h2, h3 {
+ orphans: 3;
+ widows: 3;
+ }
+ h2, h3 {
+ page-break-after: avoid;
+ }
+}
+.content {
+ overflow:hidden;
+}
+p {
+ margin: 1em 0;
+}
+a {
+ color: var(--link-color);
+}
+a:visited {
+ color: var(--link-color);
+}
+img {
+ max-width: 100%;
+}
+h1, h2, h3 {
+ margin-top: 1.4em;
+}
+ol, ul {
+ padding-left: 1.7em;
+ margin-top: 1em;
+}
+li > ol, li > ul {
+ margin-top: 0;
+}
+code {
+ font-family: Menlo, Monaco, 'Lucida Console', Consolas, monospace;
+ padding: .2em .4em;
+ font-size: 85%;
+ margin: 0;
+ white-space: pre-wrap;
+}
+pre {
+ margin: 0;
+ background-color: var(--pre-bg);
+ //padding: 1em;
+ display: block;
+ overflow: auto;
+}
+pre > code {
+ white-space: pre;
+}
+pre code { display: inline-block; }
+header {
+ margin-bottom: 4em;
+ text-align: center;
+}
+#draft-notation-hint {
+ font-size: 20px;
+ margin-left: 5px;
+ padding: 1px;
+ border: 0.5px solid var(--link-color);
+ border-radius: 100%;
+}
+ </style>
+
+</head>
+<body>
+<div id="main" class="main">
+<div class="intro">
+<h2 class="title">${boss_name}</h2>
+<a href="../$lang/cards_listing.html#${set_name}">cards</a>[<a href="../latex-build/$lang/${set_name}.pdf">pdf</a>]
+<a href="../maps/${map_name}.png">map</a>
+<a href="ai-companion.html?mapInput=${map_name}">ai-companion</a>
+<p id="desc">${desc}</p>
+</div>
+<img class="card" src="../latex-build/$lang/${boss_card}.png"/>
+<div class="content">
+<h3 id="wincon">Win Condition</h3>
+<p>${win_condition}</p>
+<h3 id="start-deck">Starting Deck</h3>
+<p>${start_deck}</p>
+<h3 id="draft">Recommended Draft <a id="draft-notation-hint" href="../rules/$lang/rules.html#notation">?</a></h3>
+<p><code>${draft}</code></p>
+<h3 id="ai">Instructions</h3>
+<p><pre><code>${instructions}</code></pre></p>
+</div>
+</div> <!-- main -->
+</body>
+</html>
+"""
+
+
+def name_to_map(name: str) -> str:
+ """Return the map's file name of a boss"""
+ return name.lower().replace(" ", "-")
+
+
+def wincon_desc(map_def: dict) -> str:
+ """Format a map's win condition"""
+ wincon = map_def['win_condition']
+ if isinstance(wincon, str):
+ return wincon.capitalize()
+
+ s = ""
+ for player, wc in wincon.items():
+ s += f'<li><b>{player.capitalize()}</b>: {wc.capitalize()}</li>'
+ return f'<ul>{s}</ul>'
+
+
+def recommended_draft(map_def: dict) -> str:
+ """Format the map's recommended draft"""
+ # TODO: improve default draft
+ # TODO: format multiple recommendations
+ return map_def.get('draft', '3x[2;8]')
+
+
+def ai_instruction(name: str, rules, lang: str) -> str:
+ """Extract the boss' instructions from the rules"""
+ rules_path = Path(rules) / lang / 'ai.md'
+ with open(rules_path, 'r', encoding='utf-8') as rf:
+ rules = rf.read()
+
+ p = f'#.*{name}.*\n'
+ ai_start = re.findall(p, rules)
+ ai_start = ai_start[0]
+
+ rules = rules[rules.find(ai_start) + len(ai_start):]
+ # rule taken from mdextractor
+ instructions = re.findall(r"```(?:\w+\s+)?(.*?)```", rules, re.DOTALL)
+ return instructions[0].strip()
+
+
+def gen_startdeck_ul(map_def: dict, lang: str) -> str:
+ if 'start_deck_list' in map_def:
+ dl = map_def["start_deck_list"].splitlines()
+ else:
+ dl = ['3 misc/farmer']
+
+ s = ""
+ for card in dl:
+ cardFmt = ""
+ parts = card.split()
+ if len(parts) > 1:
+ cardFmt = f'{parts[0]} '
+ c = parts[1]
+ else:
+ c = card
+
+ def gen_link_target(c, lang):
+ return generate_card_hover_links.gen_link_to_cardlisting(
+ c, lang, path_prefix='../')
+
+ hlink = generate_card_hover_links.gen_hoverable_link(
+ c, lambda _: c, gen_link_target, '../latex-build/', lang)
+ cardFmt += hlink
+ s += f'\n<li>{cardFmt}</li>'
+ return f'<ul id="start-decklist">{s}\n</ul>'
+
+
+def main():
+ # pylint: disable=W0641
+ """Generate a boss page"""
+
+ parser = argparse.ArgumentParser(description='generate a boss html page')
+ parser.add_argument('name',
+ help='the name of the boss',
+ choices=['kraken', 'tyrant'])
+ parser.add_argument('data', help='directory containing the card data')
+ parser.add_argument('maps', help='directoey containing the map data')
+ parser.add_argument('rules', help='directoey containing the rules')
+
+ args = parser.parse_args()
+
+ lang = 'en'
+ boss_name = NAMES[args.name]
+ desc = DESCS[args.name]
+ set_name = SETS[args.name]
+ boss_card = f'{set_name}/{name2file(boss_name)}'
+
+ map_name = name_to_map(boss_name)
+ map_path = Path(args.maps) / f'{map_name}.yml'
+ with open(map_path, 'r', encoding='utf-8') as map_file:
+ map_def = yaml.safe_load(map_file)
+
+ win_condition = wincon_desc(map_def)
+ start_deck = gen_startdeck_ul(map_def, lang)
+ draft = recommended_draft(map_def)
+ instructions = ai_instruction(boss_name, args.rules, lang)
+
+ print(Template(TEMPLATE).substitute(locals()))
+
+
+if __name__ == '__main__':
+ main()