├── stormy ├── __init__.py ├── download.py ├── main.py ├── page.py ├── utils.py ├── tokens.py ├── hospitality.py ├── gifts.py ├── themes.py └── voyage.py ├── docs ├── war.png ├── armor.png ├── player.pdf ├── ranged.png ├── heavy.svg ├── light.svg ├── medium.svg ├── rsoetnoasot.typ ├── might.svg ├── charm.svg ├── wits.svg ├── helen_rule.typ ├── player.typ └── rules.typ ├── assets └── fonts │ ├── italic.ttf │ └── regular.ttf ├── example_imgs ├── gifts.png ├── COMMONSPEAR.png └── NIGHTVOYAGE.png ├── output ├── ref │ ├── regular.ttf │ └── dipl.typ └── misc │ ├── ship_red.jpeg │ ├── ship_blue.jpeg │ ├── ship_green.jpeg │ ├── ship_yellow.jpeg │ └── player.typ ├── .gitattributes ├── .gitignore ├── pyproject.toml ├── README.md ├── raw_spreadsheet_data ├── new themes.csv ├── voyage.csv ├── gifts.csv ├── themes.csv └── hospitality.csv └── poetry.lock /stormy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/war.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SylvanFranklin/stormy/HEAD/docs/war.png -------------------------------------------------------------------------------- /docs/armor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SylvanFranklin/stormy/HEAD/docs/armor.png -------------------------------------------------------------------------------- /docs/player.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SylvanFranklin/stormy/HEAD/docs/player.pdf -------------------------------------------------------------------------------- /docs/ranged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SylvanFranklin/stormy/HEAD/docs/ranged.png -------------------------------------------------------------------------------- /assets/fonts/italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SylvanFranklin/stormy/HEAD/assets/fonts/italic.ttf -------------------------------------------------------------------------------- /example_imgs/gifts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SylvanFranklin/stormy/HEAD/example_imgs/gifts.png -------------------------------------------------------------------------------- /output/ref/regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SylvanFranklin/stormy/HEAD/output/ref/regular.ttf -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # SCM syntax highlighting 2 | pixi.lock linguist-language=YAML linguist-generated=true 3 | -------------------------------------------------------------------------------- /assets/fonts/regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SylvanFranklin/stormy/HEAD/assets/fonts/regular.ttf -------------------------------------------------------------------------------- /output/misc/ship_red.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SylvanFranklin/stormy/HEAD/output/misc/ship_red.jpeg -------------------------------------------------------------------------------- /example_imgs/COMMONSPEAR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SylvanFranklin/stormy/HEAD/example_imgs/COMMONSPEAR.png -------------------------------------------------------------------------------- /example_imgs/NIGHTVOYAGE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SylvanFranklin/stormy/HEAD/example_imgs/NIGHTVOYAGE.png -------------------------------------------------------------------------------- /output/misc/ship_blue.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SylvanFranklin/stormy/HEAD/output/misc/ship_blue.jpeg -------------------------------------------------------------------------------- /output/misc/ship_green.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SylvanFranklin/stormy/HEAD/output/misc/ship_green.jpeg -------------------------------------------------------------------------------- /output/misc/ship_yellow.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SylvanFranklin/stormy/HEAD/output/misc/ship_yellow.jpeg -------------------------------------------------------------------------------- /output/misc/player.typ: -------------------------------------------------------------------------------- 1 | #set page(flipped: true, background: image("ship_red.jpeg")) 2 | 3 | #table(columns: 9) 4 | #circle(width: 1in, height: 1in) 5 | 6 | 7 | #place(center + bottom)[ 8 | #for i in range(10) { 9 | box(width: 0.75in, height: 0.75in, stroke: 0.22em, fill: white)[#place(center + horizon)[#text(16pt)[#i]]] 10 | } 11 | ] 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/heavy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/medium.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/rsoetnoasot.typ: -------------------------------------------------------------------------------- 1 | #let icon(path) = { 2 | box(baseline: 20%, width: 1.3em, height: 1.3em)[#image(path)] 3 | } 4 | #let recipe(arr) = { 5 | for elm in arr { 6 | elm 7 | } 8 | } 9 | 10 | #let light = icon("light.svg") 11 | #let heavy = icon("heavy.svg") 12 | #let medium = icon("medium.svg") 13 | #let wits = icon("wits.svg") 14 | #let charm = icon("charm.svg") 15 | #let might = icon("might.svg") 16 | 17 | #let all = (light, medium, heavy, wits, charm, might) 18 | 19 | #let orb = i => circle(fill: white, radius: 0.75in, stroke: 4pt)[#align(center + horizon)[#text(60pt)[#i]]] 20 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # ignore all pdf, png, jpg, log, tif, tiff, and DS_Store files in the assets and assets directories, and subdirectories 3 | assets/**/*.pdf 4 | assets/**/*.png 5 | assets/**/*.jpg 6 | assets/**/*.jpeg 7 | assets/**/*.log 8 | assets/**/*.tif 9 | assets/**/*.tiff 10 | assets/**/*.DS_Store 11 | output/**/*.pdf 12 | output/**/*.png 13 | output/**/*.jpg 14 | output/**/*.log 15 | output/**/*.tif 16 | output/**/*.tiff 17 | output/**/*.DS_Store 18 | raw_spreadsheet_data/.DS_Store 19 | .DS_Store 20 | __pycache__ 21 | old assets 22 | 23 | # pixi environments 24 | .pixi 25 | *.egg-info 26 | # magic environments 27 | .magic 28 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "stormy" 3 | version = "0.1.0" 4 | description = "stormy seas board game" 5 | authors = [{ name = "SylvanFranklin", email = "sylvanfranklin@icloud.com" }] 6 | readme = "README.md" 7 | requires-python = ">=3.13" 8 | dependencies = ["pillow (>=11.1.0,<12.0.0)", "requests (>=2.32.3,<3.0.0)", "typst (>=0.13.2,<0.14.0)"] 9 | 10 | [tool.poetry] 11 | packages = [{ include = "stormy" }] 12 | 13 | [project.scripts] 14 | dl = 'stormy.main:download_sheets' 15 | compile = 'stormy.main:compile_files' 16 | 17 | [tool.pyright] 18 | venvPath = "." 19 | venv = ".venv" 20 | 21 | [build-system] 22 | requires = ["poetry-core>=2.0.0,<3.0.0"] 23 | build-backend = "poetry.core.masonry.api" 24 | 25 | -------------------------------------------------------------------------------- /docs/might.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/charm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/wits.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /stormy/download.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import os 3 | import sys 4 | from stormy.utils import colors 5 | 6 | 7 | def download_csv_file( 8 | name: str, sheet_id: str = "1wFRQ-EIMEUqx4yjBVeRkrX_5UgcV9rENszB5iZ4jkXM" 9 | ): 10 | print(f"Downloading sheet: {name}") 11 | 12 | url = f"https://docs.google.com/spreadsheets/d/{ 13 | sheet_id}/gviz/tq?tqx=out:csv&sheet={name}" 14 | 15 | headers = { 16 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" 17 | } 18 | 19 | try: 20 | response = requests.get(url, headers=headers, timeout=10) 21 | response.raise_for_status() 22 | # Check for redirection to Google login page 23 | if response.url.startswith("https://accounts.google.com/"): 24 | print( 25 | colors.YELLOW 26 | + "Access Denied: The Google Sheet is not publicly accessible." 27 | + colors.RESET 28 | ) 29 | sys.exit(1) 30 | 31 | output_dir = "raw_spreadsheet_data" 32 | os.makedirs(output_dir, exist_ok=True) 33 | 34 | file_path = os.path.join(output_dir, f"{name}.csv") 35 | with open(file_path, "wb") as f: 36 | f.write(response.content) 37 | print(f"{colors.GREEN} Downloaded successfully: { 38 | colors.RESET} {file_path}") 39 | 40 | except requests.exceptions.RequestException as e: 41 | print(f"{colors.RED} Downloaded failed: {colors.RESET} {e}") 42 | sys.exit(1) 43 | -------------------------------------------------------------------------------- /docs/helen_rule.typ: -------------------------------------------------------------------------------- 1 | 2 | Sylvan Franklin 3 | 4 | #let today = datetime.today() 5 | #today.display() 6 | 7 | For each unwilling helen check the nearest city, and consult the chart based on if they have a Helen 8 | 9 | #table(columns: 3)[No Helen][Willing Helen][Unwilling Helen][Place the helen here][Run further][This Helen takes the place of the existing Helen, that helen runs further] 10 | 11 | Running Further: Use the swept to location on leading players card. All Helens running further will jump to the closest palace in the next region in the direction of the swept to location. If a running further helen is already in the swept to region she will attempt to jump to the next closest palace in that region. Use the above table to determine what happens when a helen reaches a new palace, it may result in multiple jumps. 12 | 13 | When an unwilling finds a home it becomes willing. 14 | 15 | #import "@preview/fletcher:0.5.4" as fletcher: diagram, node, edge 16 | #import fletcher.shapes: hexagon 17 | 18 | #diagram( 19 | node-stroke: .1em, 20 | spacing: 4em, 21 | edge((-1, 0), "r", "-|>", [Unwilling Helen], label-pos: 0, label-side: left), 22 | node((0, 0), [Closest Palace \ in region], radius: 4em), 23 | edge((0, 0), (2, 0), `has willing`, "-|>", label-angle: auto, bend: 30deg), 24 | edge((0, 0), (1.3, .7), `has unwilling`, "-|>", label-angle: auto), 25 | node((1.3, .7), [Take place of this Helen], radius: 3em), 26 | node((2, 0), [Move over one region \ in the direction of swept to]), 27 | edge((1.3, .7), (2, 0), `new Helen`, "--|>", bend: -10deg, label-angle: auto), 28 | edge((0, 0), (0, 0), `no Helen`, "--|>", bend: 130deg), 29 | edge((2, 0), (0, 0), `Start Over`, "--|>"), 30 | ) 31 | -------------------------------------------------------------------------------- /stormy/main.py: -------------------------------------------------------------------------------- 1 | import stormy.gifts as gifts 2 | import stormy.hospitality as hospitality 3 | import stormy.themes as themes 4 | import stormy.tokens as tokens 5 | import stormy.voyage as voyage 6 | from stormy.page import layout_pages 7 | 8 | 9 | def download_sheets(): 10 | import stormy.download as download 11 | print('ran') 12 | for i in ["voyage", "hospitality", "gifts", "themes", "new themes"]: 13 | download.download_csv_file(i) 14 | 15 | 16 | def open(): 17 | import argparse 18 | parser = argparse.ArgumentParser(description='Compile assets') 19 | parser.add_argument('which', type=str, 20 | help='the name of the target assets') 21 | 22 | 23 | def compile_files(): 24 | import argparse 25 | parser = argparse.ArgumentParser(description='Compile assets') 26 | parser.add_argument('which', type=str, 27 | help='the name of the target assets') 28 | args = parser.parse_args() 29 | 30 | if not args.which or "all" in args: 31 | gifts.compile_all() 32 | themes.compile_all() 33 | hospitality.compile_all() 34 | voyage.compile_all() 35 | tokens.compile_all() 36 | return 37 | 38 | if "gifts" in args.which: 39 | gifts.compile_all() 40 | if "themes" in args.which: 41 | themes.compile_all() 42 | if "hospitality" in args.which: 43 | hospitality.compile_all() 44 | if "voyage" in args.which: 45 | voyage.compile_all() 46 | if "tokens" in args.which: 47 | tokens.compile_all() 48 | if "dl" in args.which: 49 | download_sheets() 50 | 51 | 52 | def generate_pages(args=None): 53 | if not args or "all" in args: 54 | layout_pages("voyage") 55 | layout_pages("gifts") 56 | layout_pages("themes") 57 | layout_pages("newthemes") 58 | layout_pages("hospitality") 59 | return 60 | 61 | if "voyage" in args: 62 | layout_pages("voyage") 63 | if "gifts" in args: 64 | layout_pages("gifts") 65 | if "themes" in args: 66 | layout_pages("themes") 67 | if "hospitality" in args: 68 | layout_pages("hospitality") 69 | if "newthemes" in args: 70 | layout_pages("new themes") 71 | -------------------------------------------------------------------------------- /docs/player.typ: -------------------------------------------------------------------------------- 1 | #import "utils.typ": *; 2 | 3 | #set page(width: 8.5in, height: 5.5in, margin: 0.5em) 4 | #let wits-color = color.rgb(43, 87, 96) 5 | #let charm-color = color.rgb(126, 32, 33) 6 | #let might-color = color.rgb(83, 95, 73) 7 | #let thickness = 10pt 8 | 9 | #let player = (name, title, accent, combat_ability_name, combat_ability_icons, general-ability) => [ 10 | #set text(20pt) 11 | #show heading: set text(accent) 12 | #show strong: set text(accent) 13 | = #name 14 | #v(-0.2em) 15 | #title 16 | #v(-0.6em) 17 | #combat_ability_icons 18 | 19 | #place(top + right)[ 20 | #block()[ 21 | #place(center + horizon)[#box(width: 75%, height: 2em, fill: accent)] 22 | #grid(columns: 4, rows: 1, gutter: 3mm, ..for i in range(1, 5) { 23 | ( 24 | block[ 25 | #let length = 60% - (i - 1) * 15.3% 26 | #place(center + top, dy: 1em)[#box(width: thickness, height: length, fill: accent)] 27 | #orb(text(accent.transparentize(80%))[#i])], 28 | ) 29 | }) 30 | ] 31 | ] 32 | 33 | #for i in range(3) [ 34 | #place(horizon + left, dy: -3.1em + 3em * i)[ 35 | #place(horizon + left)[ 36 | #line(length: 72% - i * 19.64%, stroke: thickness + accent) 37 | ] 38 | #box(width: 2.5em, height: 2.5em, stroke: 4pt, fill: white)[ 39 | #place(center + horizon)[ ] 40 | ] 41 | ] 42 | ] 43 | 44 | #place(horizon + center, dy: 0.95em, dx: 8em)[ 45 | #box(width: 3.8in, height: 1.9in, stroke: 4pt, fill: white)[#align(center + top)[ 46 | #block(inset: 8pt)[ 47 | Combat: *#combat_ability_name* (#combat_ability_icons) 48 | #v(-1em) 49 | #par(justify: true)[ 50 | #general-ability 51 | ] 52 | ] 53 | ]] 54 | ] 55 | 56 | #place(bottom + center)[ 57 | #set text(20pt, accent) 58 | #align(left)[#h(3.1em)#strong[Hit Points]] 59 | #set text(30pt, accent.transparentize(80%)) 60 | #v(-0.8em) 61 | #grid(gutter: 1fr, columns: 6, rows: 1, ..for x in range(6) { 62 | (box(width: 1.25in, height: 1.25in, stroke: 4pt, fill: white)[#align(center + horizon)[#x]],) 63 | }) 64 | ] 65 | ] 66 | 67 | #player([Talthybius], [Chief Herald], navy, [Disarm], recipe((charm, wits)), []) 68 | #pagebreak() 69 | #player([Paris], [Trojan Hottie], maroon, [Cheap Shot], recipe((charm, charm)), []) 70 | #pagebreak() 71 | #player([Menelaus], [Placeholder], olive, [Bloodlust], recipe((charm, might)), []) 72 | #pagebreak() 73 | #player([Odysseus], [Crafty Leader], black, [Fient], recipe((might, wits)), []) 74 | #pagebreak() 75 | #player([Sam], [Some Guy], green, [Dissapear], recipe((might, might, might)), []) 76 | -------------------------------------------------------------------------------- /stormy/page.py: -------------------------------------------------------------------------------- 1 | from stormy.utils import end, list_art_files 2 | 3 | 4 | def layout_pages(card_set): 5 | import csv 6 | from pathlib import Path 7 | from PIL import Image 8 | from stormy.utils import clean_raw_name, colors 9 | 10 | art_path = Path("output/hospitality/") 11 | save_path = Path("output/pages/hospitality/") 12 | save_path.mkdir(parents=True, exist_ok=True) 13 | if save_path.exists(): 14 | for file in save_path.glob("*"): 15 | file.unlink() 16 | 17 | card_configs = { 18 | "gifts": {"period": 25, "cards_per_row": 5, "thumbnail_size": (450, 450)}, 19 | "default": {"period": 9, "cards_per_row": 3, "thumbnail_size": None}, 20 | } 21 | config = card_configs.get(card_set, card_configs["default"]) 22 | period = config["period"] 23 | cards_per_row = config["cards_per_row"] 24 | thumbnail_size = config["thumbnail_size"] 25 | dpi = 300 26 | paper = Image.new("RGBA", (9 * dpi, 11 * dpi), (255, 255, 255)) 27 | margin = 20 28 | x, y, i = 0, 0, 0 29 | cards = [] 30 | csv_path = Path("raw_spreadsheet_data/hospitality.csv") 31 | if card_set != "voyage": 32 | with csv_path.open() as file: 33 | reader = csv.reader(file) 34 | next(reader) 35 | for line in reader: 36 | if end(line): 37 | break 38 | name = clean_raw_name(line[0]) 39 | occurrence = int(line[1]) if line[1] else 1 40 | 41 | for _ in range(occurrence): 42 | cards.append(name) 43 | else: 44 | for art_file in list_art_files(art_path): 45 | cards.append(art_file) 46 | 47 | # Sort seasons in proper order 48 | cards.sort(key=lambda x: (x[-6:], x[:-6])) 49 | 50 | print( 51 | f"{colors.GREEN}Creating {card_set} pages | {colors.YELLOW} total cards: { 52 | len(cards) 53 | }{colors.RESET}" 54 | ) 55 | 56 | for card_name in cards: 57 | try: 58 | card = Image.open(art_path / f"{card_name}.tiff").convert("RGBA") 59 | except FileNotFoundError: 60 | card = Image.open("assets/bg/theme.png").convert("RGBA") 61 | print(f"Missing {art_path}/{card_name}.tiff") 62 | 63 | # Apply thumbnail if specified in config 64 | if thumbnail_size: 65 | card.thumbnail(thumbnail_size) 66 | 67 | paper.paste(card, (x, y)) 68 | x += card.width + margin 69 | i += 1 70 | 71 | if i % cards_per_row == 0: 72 | x = 0 73 | y += card.height + margin 74 | 75 | if i % period == 0: 76 | x = 0 77 | y = 0 78 | paper.save( 79 | save_path / f"page{card_set}{i // period}.pdf", 80 | "PDF", 81 | resolution=300.0, 82 | ) 83 | paper = Image.new("RGBA", (9 * dpi, 11 * dpi), (255, 255, 255)) 84 | 85 | # Save the remaining cards 86 | if i % period != 0: 87 | paper.save( 88 | save_path / f"page{card_set}{i // period + 1}.pdf", 89 | "PDF", 90 | resolution=300.0, 91 | ) 92 | -------------------------------------------------------------------------------- /stormy/utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from PIL import Image, ImageDraw, ImageFont 3 | import os 4 | 5 | 6 | def saved_message(card_name): 7 | print(colors.GREEN + "Exported: " + colors.RESET + f"{card_name}.tiff") 8 | 9 | 10 | def compile_error(e): 11 | print(colors.RED + f"Compilation failed: {e}" + colors.RESET) 12 | 13 | 14 | def end(line): 15 | return ( 16 | not line 17 | or all(cell.strip() == "" for cell in line) 18 | or (line and line[0].strip() == "EOF") 19 | ) 20 | 21 | 22 | def rmbg(image): 23 | for x in range(image.width): 24 | for y in range(image.height): 25 | r, g, b, _ = image.getpixel((x, y)) 26 | if r > 200 and g > 200 and b > 200: 27 | image.putpixel((x, y), (255, 255, 255, 0)) 28 | return image 29 | 30 | 31 | class colors: 32 | RED = "\033[31m" 33 | RESET = "\033[m" 34 | GREEN = "\033[32m" 35 | YELLOW = "\033[33m" 36 | BLUE = "\033[34m" 37 | 38 | 39 | def clean_raw_name(val): 40 | return ( 41 | val.upper() 42 | .replace(" ", "") 43 | .replace(",", "") 44 | .replace("_", "") 45 | .replace("'", "") 46 | .replace("-", "") 47 | ).split(".")[0] 48 | 49 | 50 | def textsize(text, font): 51 | im = Image.new(mode="P", size=(0, 0)) 52 | draw = ImageDraw.Draw(im) 53 | _, _, width, height = draw.textbbox((0, 0), text=text, font=font) 54 | return width, height 55 | 56 | 57 | ROOT_DIR = Path("/Users/sylvanfranklin/documents/projects/stormy") 58 | ASSETS_DIR = ROOT_DIR / "assets" 59 | OUTPUT_DIR = ROOT_DIR / "output" 60 | FONT_DIR = ROOT_DIR / "assets" / "fonts" 61 | 62 | 63 | body_font = ImageFont.truetype(FONT_DIR / "regular.ttf", 40) 64 | title_font = ImageFont.truetype(FONT_DIR / "regular.ttf", 66) 65 | 66 | # trying to make this italic compelety breaks everything for some reason 67 | italic_flavor_font = ImageFont.truetype(FONT_DIR / "regular.ttf", 24) 68 | normal_flavor_font = ImageFont.truetype(FONT_DIR / "regular.ttf", 24) 69 | 70 | 71 | def missing_art_error(name): 72 | return ( 73 | colors.RED 74 | + "MISSING ART FOR " 75 | + colors.RESET 76 | + name 77 | + colors.RED 78 | + " USING DEFAULT" 79 | + colors.RESET 80 | ) 81 | 82 | 83 | def clear_directory(directory_path): 84 | import os 85 | import shutil 86 | 87 | # Check if the directory exists 88 | if not os.path.exists(directory_path): 89 | print(f"The directory {directory_path} does not exist.") 90 | return 91 | 92 | for entry in os.listdir(directory_path): 93 | entry_path = os.path.join(directory_path, entry) 94 | if ( 95 | os.path.isfile(entry_path) 96 | or os.path.islink(entry_path) 97 | and not entry_path.endswith("typ") 98 | ): 99 | os.unlink(entry_path) 100 | elif os.path.isdir(entry_path): 101 | shutil.rmtree(entry_path) 102 | 103 | print(f"All entries in {directory_path} have been removed.") 104 | 105 | 106 | def list_art_files(path): 107 | valid_extensions = ("png", "jpeg", "jpg", "tiff", "tif") 108 | final = set() 109 | 110 | for f in os.listdir(path): 111 | full_path = os.path.join(path, f) 112 | 113 | if os.path.isfile(full_path) and f.lower().endswith(valid_extensions): 114 | final.add(f) 115 | 116 | return final 117 | -------------------------------------------------------------------------------- /docs/rules.typ: -------------------------------------------------------------------------------- 1 | #import "utils.typ": *; 2 | 3 | = Winning 4 | 5 | Need ten fame 6 | #table(columns: 2)[ 7 | thing][fame][ 8 | Sack 9 | ][ 10 | 1 11 | ][ 12 | Ally 13 | ][ 14 | 1 15 | ][ 16 | Full ship of gifts 17 | ][ 18 | 3 19 | ][ 20 | Real Helen 21 | ][ 22 | 5 23 | ][ 24 | Non Helen Hottie 25 | ][ 26 | 3 27 | ][ 28 | Visit all region 29 | ][ 30 | 3 31 | ] 32 | 33 | = Dice base system 34 | Every Hero will start their turn by rolling five dice with the following faces: 35 | 36 | #recipe(all) or (light, medium, heavy, wits, charm, might) 37 | 38 | In addition to these dice each hero has automatic die face results that they may spend each turn, for example: 39 | 40 | *Odysseus* 41 | #recipe((wits, wits, might)) 42 | 43 | *Paris* 44 | #recipe((charm, charm)) 45 | These icons can be spent on cards, combat, alliance, and other abilities on the hero cards. Over the course of the game the amount of cards 46 | 47 | = Combat 48 | Combat will have all of the same combos as diplomacy, the key difference is that instead of spending gifts towards the activation of combos, we instead have weapons that contribute to combos 49 | 50 | = Diplomacy 51 | To become an ally of a palace you need points equal to or greater than the rank of the palace. Or in other words you need to fill a number of buckets equal to one plus the palace rank with one of the following combos. 52 | 53 | - Threat #recipe((might, might)) = 1 (However can't use #charm or #wits) 54 | - Emotion #recipe((charm, charm)) = 1 55 | - Reason #recipe((wits, wits)) = 1 56 | 57 | Gifts 58 | - #recipe((heavy,) * 5) = 1 59 | - #recipe((medium,) * 4) = 1 60 | - #recipe((light,) * 3) = 1 61 | 62 | These gift values will be printed on the physical gifts instead of fame. For isntance a gift might have a value. 63 | 64 | = Win conditions 65 | Getting a one of these win conditions, and then sailing off of the edge of the board. 66 | 67 | - Having the real Helen 68 | - Having some large number of gifts points on your shif (ex. 4 gold) 69 | - Five? allied regions 70 | - Five? sacked palaces 71 | 72 | = Combat 73 | + Base 74 | #block()[ 75 | #recipe((heavy, ) *3) = 1 76 | #recipe((medium, ) *3) = 2 77 | #recipe((light, ) *3) = 3 78 | ] 79 | + Weapons 80 | #block()[ 81 | #recipe((heavy, ) *3) = 1 82 | #recipe((medium, ) *3) = 2 83 | #recipe((light, ) *3) = 3 84 | 85 | ] 86 | 87 | 88 | #show "hp": it => text(red)[#strong()[#it]] 89 | 90 | #recipe((might, might)) = sunder 91 | #par()[ 92 | Remove an armor from this combat, then inflict 1hp, or 2hp no opponent has no armor 93 | ] 94 | #recipe((might, charm)) = blood lust (Menelaos) 95 | #par()[ 96 | All subsequent attacks this combat are at +1hp 97 | ] 98 | #recipe((charm, charm)) = cheap shot (Paris) 99 | #par()[ 100 | Opponent skips next roll 101 | ] 102 | #recipe((wits, charm)) = disarm (Talthybios) 103 | #par()[ 104 | Remove a weapon from this combat or 1hp 105 | ] 106 | #recipe((wits, wits)) = deflect 107 | #par()[ 108 | Deflect all recieved hp this round to opponent 109 | ] 110 | #recipe((wits, might)) = feint (Odysseus) 111 | #par()[ 112 | Reroll these dice. Inflict 1hp. 113 | ] 114 | 115 | *Special Character Combat Abilities* 116 | 117 | Menelaos: #recipe((might, charm)) 118 | 119 | Whenever you roll bloodlust reroll the entire hand 120 | 121 | Paris: #recipe((charm, charm)) 122 | 123 | Recover 1hp 124 | 125 | Talthybios: #recipe((wits, charm)) 126 | 127 | Ignore all damage when disarming 128 | 129 | Odysseus: #recipe((wits, might)) 130 | 131 | -------------------------------------------------------------------------------- /stormy/tokens.py: -------------------------------------------------------------------------------- 1 | import PIL 2 | import os 3 | import pathlib 4 | import stormy.utils as utils 5 | 6 | SAVE_PATH = "output/tokens" 7 | 8 | 9 | def compile_all(clean: bool = True, open_output: bool = True): 10 | if clean: 11 | utils.clear_directory(SAVE_PATH) 12 | 13 | # Make sure the output directory exists 14 | pathlib.Path(SAVE_PATH).mkdir(parents=True, exist_ok=True) 15 | 16 | # Compile one of each type 17 | helens() 18 | storms() 19 | circle() 20 | square() 21 | 22 | if open_output: 23 | os.system(f"open {SAVE_PATH}") 24 | 25 | 26 | def helens(): 27 | # 1" by 2" tiles ie 300 by 600 pixels 28 | domino = PIL.Image.open("assets/game_crafter/domino.png") 29 | helen_path = "assets/helens" 30 | output_dir = f"{SAVE_PATH}/helens" 31 | pathlib.Path(output_dir).mkdir(exist_ok=True) 32 | bg = PIL.Image.new("CMYK", (300, 600), 1) 33 | 34 | scale = 0.8 35 | # Get the first helen asset as an example 36 | helen_files = list(pathlib.Path(helen_path).glob("*.tiff")) 37 | if helen_files: 38 | for helen in helen_files: 39 | try: 40 | local_domino = domino.copy() 41 | local = bg.copy() 42 | 43 | fg = PIL.Image.open(helen) 44 | fg = fg.crop((200, 0, fg.width - 200, fg.height)) 45 | fg.thumbnail((bg.width * scale, bg.height * scale)) 46 | 47 | local.paste(fg, ((bg.width - fg.width) // 2, 48 | (bg.height - fg.height) // 2)) 49 | 50 | name = f"{output_dir}/{helen.stem}.png" 51 | local.resize((300, 600)) 52 | local_domino.paste(local, ((domino.width - local.width) // 53 | 2, (domino.height - local.height) 54 | // 2)) 55 | local_domino.convert("RGBA") 56 | local_domino.save(name) 57 | except (): 58 | print("failed to open helen" + helen) 59 | 60 | return "Helens" 61 | 62 | 63 | def storms(): 64 | # deal with sizes later right around 450 by 450 pixels 65 | storm_path = "assets/storms" 66 | output_dir = f"{SAVE_PATH}/storms" 67 | pathlib.Path(output_dir).mkdir(exist_ok=True) 68 | 69 | # Get the first storm asset as an example 70 | storm_files = list(pathlib.Path(storm_path).glob("*.png")) 71 | if storm_files: 72 | img = PIL.Image.open(storm_files[0]) 73 | # Resize to 450x450 pixels 74 | img = img.resize((450, 450)) 75 | # Save to output 76 | output_file = f"{output_dir}/{storm_files[0].stem}.png" 77 | img.save(output_file) 78 | return output_file 79 | return "Storms" 80 | 81 | 82 | def circle(): 83 | from PIL import Image 84 | 85 | # pirates, same as gifts 86 | # 1.25" by 1.25" tiles ie 375 by 375 pixels 87 | pirates = Image.open("assets/themes/PIRATESGENERIC.PNG") 88 | output_dir = f"{SAVE_PATH}/circles" 89 | pathlib.Path(output_dir).mkdir(exist_ok=True) 90 | bg = Image.open("assets/components/bg.png") 91 | scale = 0.8 92 | pirates.thumbnail((bg.width * scale, pirates.height * scale)) 93 | pirates = utils.rmbg(pirates) 94 | bg.paste( 95 | pirates, ((bg.width - pirates.width) // 2, 96 | (bg.height - pirates.height) // 2), 97 | pirates 98 | ) 99 | bg.thumbnail((375, 375)) 100 | 101 | for i in range(10): 102 | name = f"{output_dir}/pirates{i}.png" 103 | bg.save(name) 104 | 105 | 106 | def square(): 107 | # Fame, Ship, Crew, New Foundation, Hostile, Allied, Sacked, Hidden 108 | square_path = "assets/tokens" 109 | output_dir = f"{SAVE_PATH}/squares" 110 | pathlib.Path(output_dir).mkdir(exist_ok=True) 111 | 112 | # Get the first square asset as an example 113 | square_files = list(pathlib.Path(square_path).glob("*.png")) 114 | if square_files: 115 | img = PIL.Image.open(square_files[0]) 116 | img = img.resize((375, 375)) 117 | output_file = f"{output_dir}/{square_files[0].stem}.png" 118 | img.save(output_file) 119 | return output_file 120 | return "Square" 121 | -------------------------------------------------------------------------------- /stormy/hospitality.py: -------------------------------------------------------------------------------- 1 | from stormy.utils import clear_directory, colors, saved_message, compile_error 2 | 3 | 4 | def compile_all(clean: bool = True, open_output: bool = True): 5 | import csv 6 | import os 7 | import textwrap 8 | 9 | from PIL import Image, ImageDraw 10 | 11 | from stormy.utils import ( 12 | body_font, 13 | clean_raw_name, 14 | end, 15 | title_font, 16 | rmbg 17 | ) 18 | 19 | save_path = "output/hospitality" 20 | if not os.path.exists(save_path): 21 | os.makedirs(save_path) 22 | if clean: 23 | clear_directory(save_path) 24 | if open_output: 25 | os.system(f"open {save_path}") 26 | 27 | bg = Image.open("assets/bg/hospitality.tif").convert("RGBA") 28 | gifts_icon = Image.open("assets/components/rank.tiff").convert("RGBA") 29 | mission = Image.open("assets/themes/MASTERSHIPWORK.png").convert("RGBA") 30 | mission = rmbg(mission) 31 | gifts_icon = rmbg(gifts_icon) 32 | gifts_icon.thumbnail((100, 100)) 33 | mission.thumbnail((400, 400)) 34 | with open("raw_spreadsheet_data/hospitality.csv") as file: 35 | print(colors.YELLOW + "Reading hospitality file" + colors.RESET + "...") 36 | reader = csv.reader(file, skipinitialspace=True) 37 | next(reader) 38 | for line in reader: 39 | try: 40 | if end(line): 41 | print("END OF FILE") 42 | break 43 | 44 | local_bg = bg.copy() 45 | card_name = line[0].upper().replace(" ", "") 46 | title = line[0].upper() 47 | body_text = line[2] 48 | gifts_raw = line[3].lower() 49 | kind = line[4].lower() 50 | title_color = "black" 51 | 52 | if "hostile" in kind: 53 | title_color = "#B74141" 54 | elif "expert" in kind: 55 | title_color = "#E89C23" 56 | elif "mission" in title_color: 57 | title_color = "#3E5365" 58 | 59 | draw = ImageDraw.Draw(local_bg) 60 | draw.text( 61 | (bg.width // 2, 120), 62 | title, 63 | title_color, 64 | font=title_font, 65 | anchor="mm", 66 | align="center", 67 | ) 68 | 69 | margin = 80 70 | offset = 0 71 | if kind.lower() == "mission": 72 | offset += 300 73 | local_bg.paste( 74 | mission, ((bg.width - mission.width) // 2, 140), mission) 75 | 76 | if "rank" in gifts_raw: 77 | 78 | text = gifts_raw.replace("rank", "") 79 | 80 | hoffset = 10 81 | 82 | draw.text((margin + hoffset, 250 + offset), "Draw gifts = ", "black", 83 | font=body_font, 84 | # anchor="mm", 85 | align="left", 86 | spacing=5, 87 | ) 88 | 89 | hoffset += 280 90 | local_bg.paste( 91 | gifts_icon, (hoffset, 220 + offset), gifts_icon) 92 | 93 | hoffset += 60 94 | draw.text((hoffset, 250 + offset), text, "black", 95 | font=body_font, 96 | # anchor="mm", 97 | align="left", 98 | spacing=5, 99 | ) 100 | else: 101 | draw.text((margin, 250 + offset), f"Draw gifts = {gifts_raw}", "black", 102 | font=body_font, 103 | # anchor="mm", 104 | align="left", 105 | spacing=5, 106 | ) 107 | pass 108 | 109 | wrapped_text = textwrap.fill(body_text, width=35) 110 | draw.multiline_text( 111 | (margin, 330 + offset), 112 | wrapped_text, 113 | "black", 114 | font=body_font, 115 | # anchor="mm", 116 | align="left", 117 | spacing=4, 118 | ) 119 | 120 | local_bg.thumbnail((825, 1125), Image.Resampling.LANCZOS) 121 | local_bg.save(f"{save_path}/{clean_raw_name(card_name)}.tiff") 122 | saved_message(card_name) 123 | 124 | # catch everything and print the error 125 | except Exception as e: 126 | compile_error(e) 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🇬🇷 Stormy Seas of Cyprus 🇬🇷 2 | Board game for 2–4 players themed in and around the Odyssey. Players take on the role of a Greek hero, navigating the seas of Cyprus, fighting for treasure and divine favor, while looking for the real Helen of Troy. The board features a unique wind table based movement system that changes throughout the four seasons. 3 | 4 | 5 | ![](./example_imgs/gifts.png "title") 6 | ![](./example_imgs/NIGHTVOYAGE.png "title") 7 | 8 | 9 | ### Contained 10 | This Repo contains code for compiling all game assets, along with a script for pulling the information from a master [sheet](https://docs.google.com/spreadsheets/d/1wFRQ-EIMEUqx4yjBVeRkrX_5UgcV9rENszB5iZ4jkXM). 11 | 12 | ⚠️ **Art assets are not included in this repo due to file size restraints** ⚠️ 13 | 14 | ***Credits*** 15 | - Artwork for board and cards: Glynnis Fawkes. 16 | - Layout and design: Sylvan and John Franklin. 17 | - Game design: Sylvan and John Franklin. 18 | 19 | 20 | EXPORTED AMPHORAONE.png 21 | EXPORTED AMPHORATHREE.png 22 | EXPORTED AMPHORATWO.png 23 | EXPORTED AXE.png 24 | EXPORTED BABOONS.png 25 | EXPORTED BEES.png 26 | EXPORTED BRACELET.png 27 | EXPORTED CARNELIANBEADS.png 28 | EXPORTED CAT.png 29 | EXPORTED CEDAR.png 30 | EXPORTED CLOTH.png 31 | EXPORTED COMMONBOW.png 32 | EXPORTED COMMONSPEAR.png 33 | EXPORTED COMMONSWORD.png 34 | EXPORTED COPPERINGOT.png 35 | EXPORTED EARRINGS.png 36 | EXPORTED EBONY.png 37 | EXPORTED FAIENCEDISHWITHMUSICGIRL.png 38 | EXPORTED FRUITS.png 39 | EXPORTED GLASSINGOTS.png 40 | EXPORTED GOAT.png 41 | EXPORTED GOLD.png 42 | EXPORTED GOLDCHALICE.png 43 | EXPORTED GOLDRHYTON.png 44 | EXPORTED GREAVES.png 45 | EXPORTED HELMET.png 46 | EXPORTED IRONKNIFEWITHBRONZERIVETS.png 47 | EXPORTED IVORYDUCK.png 48 | EXPORTED IVORYRAW.png 49 | EXPORTED JEWELRY.png 50 | EXPORTED LAPISLAZULICYLINDERSEAL.png 51 | EXPORTED LEKYTHION.png 52 | EXPORTED LYRE.png 53 | EXPORTED MUSICGIRLS.png 54 | EXPORTED NEPENTHE.png 55 | EXPORTED OARS.png 56 | EXPORTED OSTRICHEGG.png 57 | EXPORTED RATS.png 58 | EXPORTED RESIN.png 59 | EXPORTED ROPE.png 60 | EXPORTED SCARABOFNEFERTITI.png 61 | EXPORTED SHIELD.png 62 | EXPORTED SILVER-HILTEDSWORD.png 63 | EXPORTED SLAVES.png 64 | EXPORTED SPICES.png 65 | EXPORTED STAND.png 66 | EXPORTED STATUE.png 67 | EXPORTED STONESCEPTER.png 68 | EXPORTED SYMOSIUMBOWL.png 69 | EXPORTED TABLET.png 70 | EXPORTED THORAX.png 71 | EXPORTED TININGOT.png 72 | EXPORTED TORTOISESHELL.png 73 | EXPORTED TRIPODONE.png 74 | EXPORTED TRIPODTWO.png 75 | EXPORTED TRUMPET.png 76 | EXPORTED UNCOMMONSPEAR.png 77 | EXPORTED VASESWITHICONS.png 78 | EXPORTED VESSELBLUE.png 79 | EXPORTED VESSELBLUETWO.png 80 | EXPORTED VESSELRED.png 81 | EXPORTED VESSELSILVER.png 82 | EXPORTED VESSELTALL.png 83 | No image found for CARNELIANBEADS 84 | No image found for FAIENCEDISHWITHMUSICGIRL 85 | No image found for GREAVES 86 | No image found for IRONKNIFEWITHBRONZERIVETS 87 | No image found for LAPISLAZULICYLINDERSEAL 88 | No image found for OSTRICHEGG 89 | No image found for RATS 90 | No image found for SCARABOFNEFERTITI 91 | No image found for SHIELD 92 | No image found for SILVER-HILTEDSWORD 93 | No image found for SLAVES 94 | No image found for SYMOSIUMBOWL 95 | No image found for TININGOT 96 | No image found for TORTOISESHELL 97 | No image found for TRUMPET 98 | No image found for UNCOMMONSPEAR 99 | No image found for VASESWITHICONS 100 | 101 | (Using default Image) Exported: BEGGING OFF.png 102 | (Using default Image) Exported: CHANGE OF HEART.png 103 | (Using default Image) Exported: CHEAP SHOT.png 104 | (Using default Image) Exported: DEFLECT.png 105 | (Using default Image) Exported: DISARM.png 106 | (Using default Image) Exported: DISOKOUROI.png 107 | (Using default Image) Exported: DOLDRUMS.png 108 | (Using default Image) Exported: FIRST STRIKE.png 109 | (Using default Image) Exported: FORESIGHT.png 110 | (Using default Image) Exported: HERMES.png 111 | (Using default Image) Exported: INSIDE INFORMATION.png 112 | (Using default Image) Exported: LEVIATHAN.png 113 | (Using default Image) Exported: RUMORS.png 114 | (Using default Image) Exported: SEA SACRIFICE.png 115 | (Using default Image) Exported: SEA-GOD'S FAVOR.png 116 | (Using default Image) Exported: SECOND CHANCE.png 117 | (Using default Image) Exported: STORM GOD STRIKES.png 118 | (Using default Image) Exported: SUNDER.png 119 | Exported: AMPHIKALYPSIS.png 120 | Exported: BIRD OMEN.png 121 | Exported: BLOOD LUST.png 122 | Exported: CYPRIOT PIRATES.png 123 | Exported: DISGUISE.png 124 | Exported: DIVINE MIST.png 125 | Exported: DIVINE MIST.png 126 | Exported: ESCAPE.png 127 | Exported: FABLED WEALTH.png 128 | Exported: FAIR WIND AND SMOOTH SEA.png 129 | Exported: HELP FROM EIDOTHEA.png 130 | Exported: HIDDEN REEF.png 131 | Exported: INSPIRATION.png 132 | Exported: INTERCEPT.png 133 | Exported: LIBYAN PIRATES.png 134 | Exported: LUKKA PIRATES.png 135 | Exported: MAN OVERBOARD.png 136 | Exported: MASTER SHIPWORK.png 137 | Exported: NIGHT ATTACK.png 138 | Exported: NIGHT VOYAGE.png 139 | Exported: OPPOSING WIND.png 140 | Exported: PHOENICIAN PIRATES.png 141 | Exported: POLITICAL HYSTERIA.png 142 | Exported: SHIFTING WIND.png 143 | Exported: SYRIAN PIRATES.png 144 | Exported: UNMOORED.png 145 | Exported: UNSEASONABLE WIND.png 146 | Exported: WILL OF ZEUS.png 147 | -------------------------------------------------------------------------------- /output/ref/dipl.typ: -------------------------------------------------------------------------------- 1 | #let ANY = "KEY" 2 | #let ALMONDS = "ALMONDS" 3 | #let AMPHORAONE = "AMPHORAONE" 4 | #let AMPHORATHREE = "AMPHORATHREE" 5 | #let AMPHORATWO = "AMPHORATWO" 6 | #let AXE = "AXE" 7 | #let BABOONS = "BABOONS" 8 | #let BEES = "BEES" 9 | #let BRACELET = "BRACELET" 10 | #let CANAANITEAMPHORA = "CANAANITEAMPHORA" 11 | #let CANAANITEAMPHORAALT = "CANAANITEAMPHORAALT" 12 | #let CANAANITEGOD = "CANAANITEGOD" 13 | #let CARNELIANBEADS = "CARNELIANBEADS" 14 | #let CAT = "CAT" 15 | #let CEDAR = "CEDAR" 16 | #let CHIEFHERALD = "CHIEFHERALD" 17 | #let CLOTH = "CLOTH" 18 | #let COMMONBOW = "COMMONBOW" 19 | #let COMMONSPEAR = "COMMONSPEAR" 20 | #let COMMONSWORD = "COMMONSWORD" 21 | #let COMMONTHORAX = "COMMONTHORAX" 22 | #let COPPERINGOT = "COPPERINGOT" 23 | #let DATES = "DATES" 24 | #let EARRINGS = "EARRINGS" 25 | #let EBONY = "EBONY" 26 | #let EXPERTHELMSMAN = "EXPERTHELMSMAN" 27 | #let FRUITS = "FRUITS" 28 | #let FUGITIVEDIVINER = "FUGITIVEDIVINER" 29 | #let GLASSINGOTS = "GLASSINGOTS" 30 | #let GOAT = "GOAT" 31 | #let GOLD = "GOLD" 32 | #let GOLDCHALICE = "GOLDCHALICE" 33 | #let GOLDRHYTON = "GOLDRHYTON" 34 | #let GREAVES = "GREAVES" 35 | #let HEALERHEADINGHOME = "HEALERHEADINGHOME" 36 | #let HELMET = "HELMET" 37 | #let IRONKNIFE = "IRONKNIFE" 38 | #let IVORYDUCK = "IVORYDUCK" 39 | #let IVORYRAW = "IVORYRAW" 40 | #let JEWELRY = "JEWELRY" 41 | #let LAPISLAZULICYLINDERSEAL = "LAPISLAZULICYLINDERSEAL" 42 | #let LEKYTHION = "LEKYTHION" 43 | #let LYRE = "LYRE" 44 | #let MASTERARCHER = "MASTERARCHER" 45 | #let MASTERNAVIGATOR = "MASTERNAVIGATOR" 46 | #let MUSICGIRLS = "MUSICGIRLS" 47 | #let MYCENEANJAR = "MYCENEANJAR" 48 | #let NECKLACE = "NECKLACE" 49 | #let NEPENTHE = "NEPENTHE" 50 | #let OARS = "OARS" 51 | #let OLIVEOIL = "OLIVEOIL" 52 | #let OSTRICHEGG = "OSTRICHEGG" 53 | #let POMEGRANITE = "POMEGRANITE" 54 | #let RATS = "RATS" 55 | #let RESIN = "RESIN" 56 | #let ROPE = "ROPE" 57 | #let SCALE = "SCALE" 58 | #let SCARABOFNEFERTITI = "SCARABOFNEFERTITI" 59 | #let SHIELDANDSPEAR = "SHIELDANDSPEAR" 60 | #let SILVERHILTEDSWORD = "SILVERHILTEDSWORD" 61 | #let SINGER = "SINGER" 62 | #let SLAVES = "SLAVES" 63 | #let SPICES = "SPICES" 64 | #let STAND = "STAND" 65 | #let STATUE = "STATUE" 66 | #let STONESCEPTER = "STONESCEPTER" 67 | #let SWORDS = "SWORDS" 68 | #let SYMOSIUMBOWL = "SYMOSIUMBOWL" 69 | #let TABLET = "TABLET" 70 | #let THORAX = "THORAX" 71 | #let TININGOT = "TININGOT" 72 | #let TORTOISESHELL = "TORTOISESHELL" 73 | #let TRIPODONE = "TRIPODONE" 74 | #let TRIPODTWO = "TRIPODTWO" 75 | #let UNCOMMONSPEAR = "UNCOMMONSPEAR" 76 | #let VESSELBLUE = "VESSELBLUE" 77 | #let VESSELBLUETWO = "VESSELBLUETWO" 78 | #let VESSELRED = "VESSELRED" 79 | #let VESSELSILVER = "VESSELSILVER" 80 | #let VESSELTALL = "VESSELTALL" 81 | #let WATER = "WATER" 82 | #let WHEAT = "WHEAT" 83 | 84 | #let pots = ( 85 | AMPHORAONE, 86 | AMPHORATHREE, 87 | AMPHORATWO, 88 | ) 89 | 90 | 91 | #let gifts = ( 92 | ANY, 93 | ALMONDS, 94 | AMPHORAONE, 95 | AMPHORATHREE, 96 | AMPHORATWO, 97 | AXE, 98 | BABOONS, 99 | BEES, 100 | BRACELET, 101 | CANAANITEAMPHORA, 102 | CANAANITEAMPHORAALT, 103 | CANAANITEGOD, 104 | CARNELIANBEADS, 105 | CAT, 106 | CEDAR, 107 | CHIEFHERALD, 108 | CLOTH, 109 | COMMONBOW, 110 | COMMONSPEAR, 111 | COMMONSWORD, 112 | COMMONTHORAX, 113 | COPPERINGOT, 114 | DATES, 115 | EARRINGS, 116 | EBONY, 117 | EXPERTHELMSMAN, 118 | FRUITS, 119 | FUGITIVEDIVINER, 120 | GLASSINGOTS, 121 | GOAT, 122 | GOLD, 123 | GOLDCHALICE, 124 | GOLDRHYTON, 125 | GREAVES, 126 | HEALERHEADINGHOME, 127 | HELMET, 128 | IRONKNIFE, 129 | IVORYDUCK, 130 | IVORYRAW, 131 | JEWELRY, 132 | LAPISLAZULICYLINDERSEAL, 133 | LEKYTHION, 134 | LYRE, 135 | MASTERARCHER, 136 | MASTERNAVIGATOR, 137 | MUSICGIRLS, 138 | MYCENEANJAR, 139 | NECKLACE, 140 | NEPENTHE, 141 | OARS, 142 | OLIVEOIL, 143 | OSTRICHEGG, 144 | POMEGRANITE, 145 | RATS, 146 | RESIN, 147 | ROPE, 148 | SCALE, 149 | SCARABOFNEFERTITI, 150 | SHIELDANDSPEAR, 151 | SILVERHILTEDSWORD, 152 | SINGER, 153 | SLAVES, 154 | SPICES, 155 | STAND, 156 | STATUE, 157 | STONESCEPTER, 158 | SWORDS, 159 | SYMOSIUMBOWL, 160 | TABLET, 161 | THORAX, 162 | TININGOT, 163 | TORTOISESHELL, 164 | TRIPODONE, 165 | TRIPODTWO, 166 | UNCOMMONSPEAR, 167 | VESSELBLUE, 168 | VESSELBLUETWO, 169 | VESSELRED, 170 | VESSELSILVER, 171 | VESSELTALL, 172 | WATER, 173 | WHEAT, 174 | ) 175 | 176 | #let key(parts) = { 177 | show grid.cell: set align(horizon + center) 178 | let cols = parts.map(part => image("gift-icons/" + part + ".png")) 179 | grid( 180 | stroke: 0pt, gutter: 2em, columns: parts.len() * 2, 181 | ..cols 182 | .map(it => { 183 | grid.cell[#box(width: 4em)[= #it]] 184 | }) 185 | .intersperse([=]) 186 | ) 187 | } 188 | 189 | #let icon(it) = { 190 | set text(16pt) 191 | box(width: 9mm)[#it] 192 | } 193 | 194 | #let recipe(parts) = { 195 | show grid.cell: set align(horizon + center) 196 | let cols = parts.map(part => image("gift-icons/" + part + ".png")) 197 | grid( 198 | gutter: 4pt, 199 | columns: parts.len() + 1, 200 | ..cols.map(icon) 201 | ) 202 | } 203 | 204 | #key(gifts) 205 | 206 | 207 | #table(columns: 3)[Name][Score][Recipe][ 208 | Copper Monopoly][4][#recipe((COPPERINGOT, TININGOT))][ 209 | Famine][4][#recipe((WHEAT,))][ 210 | Copper Monopoly][4][#recipe((COPPERINGOT, TININGOT))] 211 | 212 | // = Stormy Seas 213 | // Diplomacy reference tables. All values here are subject to change, they provide some starting points for diplomacy "packages" which are small groups of gifts that when combined give you many more points than the individual values 214 | 215 | -------------------------------------------------------------------------------- /raw_spreadsheet_data/new themes.csv: -------------------------------------------------------------------------------- 1 | "NAME","COUNT","TEXT","TYPE","FLAVOR","","","","","","","","","","","","","","","","","","","","","","" 2 | "ZEUS","1","Double your damage in this round of combat. OR cancel any Theme card.","DIVINE","And the Heroes at Troy were being killed, and the Will of Zeus was brought to pass ( _Cypria_ Fragment 1).","","","","","","","","","","","","","","","","","","","","","","" 3 | "APHRODITE","1","Flip Willingness of all Helens. OR Secretly swap two Helens.","DIVINE","Thither I will not go—it would be a reproach— / Sharing that man’s bed (Homer _Iliad_ 3.410–11).","","","","","","","","","","","","","","","","","","","","","","" 4 | "KINYRAS","1","Automatically Ally any rank two Palace. OR fully heal Hero and Crew.","DIVINE","","","","","","","","","","","","","","","","","","","","","","","" 5 | "HERMES","1","Swap any two Helens not on ships (without examining).","DIVINE","Hermes swept me high into the skycoves / And concealing me in a cloud, brought me to the house of Proteus (Euripides _Helen_ 45–6).","","","","","","","","","","","","","","","","","","","","","","" 6 | "POSEIDON","1","Cancel any Theme card that affects a Voyage.","DIVINE","Hear me, Poseidon Earth-Shaker, and do not refuse / To accomplish these deeds for us who pray. (Homer _Odyssey_ 3.55–6).","","","","","","","","","","","","","","","","","","","","","","" 7 | "EIDOTHEA","1","Becalmed Hero automatically succeeds in wrestling and escaping Proteus ","DIVINE","Daughter of mighty Proteus, old man of the sea / Eidothea, for her heart most of all I stirred (Homer _Odyssey_ 4.365–6).","","","","","","","","","","","","","","","","","","","","","","" 8 | "HERA","1","Immediately cancel one Storm OR Cause Storm in one Sea Region without Storm","DIVINE","Hera sends a great Storm against them, under the force of which they make for Sidοn (ps.-Apollodorus Epitome 3.4).","","","","","","","","","","","","","","","","","","","","","","" 9 | "PROTEUS","1","Look at and rearrange the top four cards of the Voyage Deck.","DIVINE","","","","","","","","","","","","","","","","","","","","","","","" 10 | "RUMORS","5","Add or remove one Region Visited Marker.","FORTUNE","A rumor you may hear / From Zeus, which very often brings reports to men (Homer _Odyssey_ 1.282–3).","","","","","","","","","","","","","","","","","","","","","","" 11 | "DISGUISE","2","Treat a Visited Region as unvisited, or a Hostile Palace as Neutral, for this Turn.","FORTUNE","Disguised like that he entered the Trojans’ city and deceived they were, / Every one (Homer _Odyssey_ 4.249–50).","","","","","","","","","","","","","","","","","","","","","","" 12 | "BIRD OMEN","2","Look at and rearrange the top four cards of the Hospitality Deck.","FORTUNE","Yet good were the omen-birds for him setting out, / On the right hand (Homer, _Odyssey_ 24.311–12).","","","","","","","","","","","","","","","","","","","","","","" 13 | "NIGHT ATTACK","3","You may sack palace after hospitality, treat the palace as Rank -1","FORTUNE","","","","","","","","","","","","","","","","","","","","","","","" 14 | "RESTFUL NIGHT","2","Recover all Crew and Hit Points during Upkeep.","FORTUNE","","","","","","","","","","","","","","","","","","","","","","","" 15 | "PIRATES","9","Place two Pirates tokens. OR Move a pirate token up to three circles.","VOYAGE","Or do you rove at random / Like pirates over the sea who wander / Risking their souls, bringing evil to foreign people (Homer _Odyssey_ 3.72–4).","","","","","","","","","","","","","","","","","","","","","","" 16 | "SHIFTING WIND","4","Rotate any Voyage card.","VOYAGE","And next for us behind our dark-prowed ship / A favorable wind she sent, sail-filling, a fair companion (Homer _Odyssey_ 11.6–7).","","","","","","","","","","","","","","","","","","","","","","" 17 | "LEVIATHAN","1","Force a player in a non-shallow Sea Circle to discard Three gifts and lose three Crew.","VOYAGE","Or a great god may send me some monster / From the sea, the sort which famous Amphitrite nourishes in quantity (Homer _Odyssey_ 5.421–2).","","","","","","","","","","","","","","","","","","","","","","" 18 | "MAN OVERBOARD","1","Force a player to discard an Expert, or to lose three Crew.","VOYAGE","There Phoebus Apollo the helmsman / Slew, attacking him with gentle bolts, / As he held the rudder in his hands while the ship sped on (Homer _Odyssey_ 3.279–81).","","","","","removed this card when working on trial combat dice activation system w Sylvan April 6 2025","","","","","","","","","","","","","","","","","" 19 | "DIOSKOUROI","1","Gain six movement points.","VOYAGE","","","","","","","","","","","","","","","","","","","","","","","" 20 | "STORMY SEAS","4","Move any Storm up to three Circles in any direction.","VOYAGE","","","","","","","","","","","","","","","","","","","","","","","" 21 | "FAIR WIND AND SMOOTH SEAS","2","All Sea Circles cost 2 during your Turn.","VOYAGE","On the third day from Sparta, Alexandros reached Troy, carrying Helen, with a fair wind and smooth sea (Herodotus 2.117). ","","","","","","","","","","","","","","","","","","","","","","" 22 | "ABUNDANCE","4","Recieve two extra Gifts during Hospitality. ","ECONOMIC","","","","","","","","","","","","","","","","","","","","","","","" 23 | "CONTRIBUTIONS","3","Draw one Gift for each Ally you have. Each other player in a region with one of your Allies must also pay your one Gift. ","ECONOMIC","This mare Ekhepolos, son of Ankhises, gave to Agamemnon— / A gift, so as not to follow him to windy Ilion / But to stay at home enjoying himself (Homer _Iliad_ 23.296–8).","","","","","","","","","","","","","","","","","","","","","","" 24 | "MASTER SHIPWORK","1","Attach this Card to your Ship after passing a complete turn at a Palace for Recovery and Refit, or when receiving a new Ship. Adds 2 Movement Points to all Voyage Cards.","VOYAGE","","Harmonides, who knew how to fashion all intricate things / By hand... / Who even built balanced Ships for Alexandros (Homer _Iliad_ 5.60–2).","","","","","","","","","","","","","","","","","","","","","" 25 | "EOF","","","","","","","","","","","","","","","","","","","","","","","","","","" 26 | "TOTAL","52","","","","","","","","","","","","","","","","","","","","","","","","","" 27 | "","22","","","","","","","","","","","","","","","","","","","","","","","","","" -------------------------------------------------------------------------------- /raw_spreadsheet_data/voyage.csv: -------------------------------------------------------------------------------- 1 | "yy","Count","Movement Point","Storm location","Storm Ship","Storm Crew","Combined Threshold","Swept To Location","","","","","","","","","","","","","","","","","","","","" 2 | "Winter","1","18","Issikon Pelagos","3d6","3d6","4","Hatti","","","","","","","","","","","","","","","","","","","","" 3 | "Winter","1","19","Aigyption Pelagos","3d6","4d6","2","Syria","","","","","","","","","","","","","","","","","","","","" 4 | "Winter","1","20","Pelagos Tyron","4d6","4d6","3","Phoenicia","","","","","","","","","","","","","","","","","","","","" 5 | "Winter","1","21","Issikon Pelagos","4d6","3d6","3","Cyprus North","","","","","","","","","","","","","","","","","","","","" 6 | "Winter","1","21","Lykion Pelagos","4d6","5d6","3","Cyprus South","","","","","","","","","","","","","","","","","","","","" 7 | "Winter","1","22","Libykon Pelagos","5d6","5d6","3","Philistia","","","","","","","","","","","","","","","","","","","","" 8 | "Winter","1","22","Aigyption Pelagos","5d6","4d6","3","Egypt","","","","","","","","","","","","","","","","","","","","" 9 | "Winter","1","23","Pelagos Tyron","4d6","4d6","3","Libya","","","","","","","","","","","","","","","","","","","","" 10 | "Winter","1","23","Issikon Pelagos","4d6","4d6","2","Proteus' Island","","","","","","","","","","","","","","","","","","","","" 11 | "Winter","1","24","Lykion Pelagos","4d6","5d6","3","Lukka","","","","","","","","","","","","","","","","","","","","" 12 | "Winter","1","25","Libykon Pelagos","3d6","3d6","3","Hatti","","","","","","","","","","","","","","","","","","","","" 13 | "Winter","1","26","Aigyption Pelagos","3d6","4d6","2","Syria","","","","","","","","","","","","","","","","","","","","" 14 | "Spring","1","18","Pelagos Tyron","4d6","4d6","3","Phoenicia","","","","","","","","","","","","","","","","","","","","" 15 | "Spring","1","19","Issikon Pelagos","4d6","3d6","3","Cyprus North","","","","","","","","","","","","","","","","","","","","" 16 | "Spring","1","20","Lykion Pelagos","4d6","5d6","3","Cyprus South","","","","","","","","","","","","","","","","","","","","" 17 | "Spring","1","21","Libykon Pelagos","5d6","5d6","3","Philistia","","","","","","","","","","","","","","","","","","","","" 18 | "Spring","1","21","Aigyption Pelagos","5d6","4d6","3","Egypt","","","","","","","","","","","","","","","","","","","","" 19 | "Spring","1","22","Pelagos Tyron","4d6","4d6","3","Libya","","","","","","","","","","","","","","","","","","","","" 20 | "Spring","1","22","Issikon Pelagos","4d6","4d6","2","Proteus' Island","","","","","","","","","","","","","","","","","","","","" 21 | "Spring","1","23","Lykion Pelagos","4d6","5d6","3","Lukka","","","","","","","","","","","","","","","","","","","","" 22 | "Spring","1","23","Libykon Pelagos","3d6","3d6","3","Hatti","","","","","","","","","","","","","","","","","","","","" 23 | "Spring","1","24","Aigyption Pelagos","3d6","4d6","2","Syria","","","","","","","","","","","","","","","","","","","","" 24 | "Spring","1","25","Pelagos Tyron","4d6","4d6","3","Phoenicia","","","","","","","","","","","","","","","","","","","","" 25 | "Spring","1","26","Issikon Pelagos","4d6","3d6","3","Cyprus North","","","","","","","","","","","","","","","","","","","","" 26 | "Summer","1","18","Lykion Pelagos","4d6","5d6","3","Cyprus South","","","","","","","","","","","","","","","","","","","","" 27 | "Summer","1","19","Libykon Pelagos","5d6","5d6","3","Philistia","","","","","","","","","","","","","","","","","","","","" 28 | "Summer","1","20","Aigyption Pelagos","5d6","4d6","3","Egypt","","","","","","","","","","","","","","","","","","","","" 29 | "Summer","1","21","Pelagos Tyron","4d6","4d6","3","Libya","","","","","","","","","","","","","","","","","","","","" 30 | "Summer","1","21","Issikon Pelagos","4d6","4d6","2","Proteus' Island","","","","","","","","","","","","","","","","","","","","" 31 | "Summer","1","22","Lykion Pelagos","4d6","5d6","3","Lukka","","","","","","","","","","","","","","","","","","","","" 32 | "Summer","1","22","Libykon Pelagos","3d6","3d6","3","Hatti","","","","","","","","","","","","","","","","","","","","" 33 | "Summer","1","23","Aigyption Pelagos","3d6","4d6","2","Syria","","","","","","","","","","","","","","","","","","","","" 34 | "Summer","1","23","Pelagos Tyron","4d6","4d6","3","Phoenicia","","","","","","","","","","","","","","","","","","","","" 35 | "Summer","1","24","Issikon Pelagos","4d6","3d6","3","Cyprus North","","","","","","","","","","","","","","","","","","","","" 36 | "Summer","1","25","Lykion Pelagos","4d6","5d6","3","Cyprus South","","","","","","","","","","","","","","","","","","","","" 37 | "Summer","1","26","Libykon Pelagos","5d6","5d6","3","Philistia","","","","","","","","","","","","","","","","","","","","" 38 | "Autumn","1","18","Aigyption Pelagos","5d6","4d6","3","Egypt","","","","","","","","","","","","","","","","","","","","" 39 | "Autumn","1","19","Pelagos Tyron","4d6","4d6","3","Libya","","","","","","","","","","","","","","","","","","","","" 40 | "Autumn","1","20","Issikon Pelagos","4d6","4d6","2","Proteus' Island","","","","","","","","","","","","","","","","","","","","" 41 | "Autumn","1","21","Lykion Pelagos","4d6","5d6","3","Lukka","","","","","","","","","","","","","","","","","","","","" 42 | "Autumn","1","21","Libykon Pelagos","3d6","3d6","3","Hatti","","","","","","","","","","","","","","","","","","","","" 43 | "Autumn","1","22","Aigyption Pelagos","3d6","4d6","2","Syria","","","","","","","","","","","","","","","","","","","","" 44 | "Autumn","1","22","Pelagos Tyron","4d6","4d6","3","Phoenicia","","","","","","","","","","","","","","","","","","","","" 45 | "Autumn","1","23","Issikon Pelagos","4d6","3d6","3","Cyprus North","","","","","","","","","","","","","","","","","","","","" 46 | "Autumn","1","23","Lykion Pelagos","4d6","5d6","3","Cyprus South","","","","","","","","","","","","","","","","","","","","" 47 | "Autumn","1","24","Libykon Pelagos","5d6","5d6","3","Philistia","","","","","","","","","","","","","","","","","","","","" 48 | "Autumn","1","25","Aigyption Pelagos","5d6","4d6","3","Egypt","","","","","","","","","","","","","","","","","","","","" 49 | "Autumn","1","26","Pelagos Tyron","4d6","4d6","3","Proteus' Island","","","","","","","","","","","","","","","","","","","","" 50 | "EOF","","","","","","","","","","","","","","","","","","","","","","","","","","","" -------------------------------------------------------------------------------- /stormy/gifts.py: -------------------------------------------------------------------------------- 1 | from stormy.utils import ( 2 | list_art_files, 3 | clean_raw_name, 4 | clear_directory, 5 | colors, 6 | missing_art_error, 7 | rmbg, 8 | OUTPUT_DIR, 9 | ASSETS_DIR 10 | ) 11 | 12 | import csv 13 | from PIL import Image, ImageDraw, ImageFont 14 | import os 15 | 16 | textless = True 17 | 18 | 19 | def load_assets(): 20 | try: 21 | print("Loading assets...") 22 | path = ASSETS_DIR / "components" 23 | 24 | assets = { 25 | "font": ImageFont.truetype("assets/regular.ttf", 120), 26 | "background": Image.open(f"{path}/bg.png").convert("RGBA"), 27 | "fameorb": Image.open(f"{path}/fame.png").convert("RGBA"), 28 | "weapon": Image.open(f"{path}/weapon.png").convert("RGBA"), 29 | "armor": Image.open(f"{path}/armor.png").convert("RGBA"), 30 | "expert": Image.open(f"{path}/expert.png").convert("RGBA"), 31 | "notrade": Image.open(f"{path}/notrade.png").convert("RGBA"), 32 | "pot": Image.open(f"{path}/heavy.png").convert("RGBA"), 33 | "raw": Image.open(f"{path}/heavy.png").convert("RGBA"), 34 | "heavy": Image.open(f"{path}/heavy.png").convert("RGBA"), 35 | "medium": Image.open(f"{path}/medium.png").convert("RGBA"), 36 | "light": Image.open(f"{path}/light.png").convert("RGBA"), 37 | } 38 | 39 | print(colors.GREEN + "Assets loaded successfully." + colors.RESET) 40 | return assets 41 | except FileNotFoundError as e: 42 | print(colors.RED + f"ERROR: {e}" + colors.RESET) 43 | return None 44 | 45 | 46 | def determine_ring(kind, weight, assets, ability): 47 | 48 | if "w" in kind: 49 | return assets["weapon"].copy() 50 | elif "a" in kind: 51 | return assets["armor"].copy() 52 | elif "x" in kind: 53 | return assets["expert"].copy() 54 | elif "p" in weight: 55 | return assets["pot"].copy() 56 | elif "r" in weight: 57 | return assets["raw"].copy() 58 | elif weight == "h": 59 | return assets["heavy"].copy() 60 | elif weight == "m": 61 | return assets["medium"].copy() 62 | else: 63 | return assets["light"].copy() 64 | 65 | 66 | def process_special_text(draw, font, name, special_text, ring): 67 | """Handle special text placement on the ring.""" 68 | special_text_offset = { 69 | "GOAT": (100, -250), 70 | "AXE": (100, 200), 71 | "NEPENTHE": (0, -120), 72 | } 73 | 74 | center_title = False 75 | if "^" in special_text: 76 | center_title = True 77 | special_text = special_text.replace("^", "") 78 | 79 | if "\\n" in special_text: 80 | special_text = special_text.replace("\\n", "\n") 81 | 82 | margins = 400 83 | offset_pair = special_text_offset.get(name.upper(), (0, 0)) 84 | if center_title: 85 | draw.multiline_text( 86 | (ring.width // 2, 300 + offset_pair[1]), 87 | special_text, 88 | (245, 98, 81), 89 | font=font, 90 | anchor="mm", 91 | ) 92 | else: 93 | left, right = (special_text.split("|") + [""])[:2] 94 | draw.multiline_text( 95 | ( 96 | (ring.width // 2) + margins + offset_pair[0], 97 | (ring.height // 2) + offset_pair[1], 98 | ), 99 | right, 100 | (245, 98, 81), 101 | font=font, 102 | anchor="mm", 103 | ) 104 | draw.multiline_text( 105 | ( 106 | (ring.width // 2) - margins + offset_pair[0], 107 | (ring.height // 2) + offset_pair[1], 108 | ), 109 | left, 110 | (245, 98, 81), 111 | font=font, 112 | anchor="mm", 113 | ) 114 | 115 | 116 | def extract_line_data(line, debug=False): 117 | name = clean_raw_name(line[0].upper().replace(" ", "")) 118 | cargo_type, fame, special_text, kind, additional_rule, tradable, symbology = ( 119 | line[2].lower(), # Cargo Type 120 | line[3], # Fame 121 | line[4][1:], # Special Text 122 | line[5], # Kind 123 | line[6], # Additional Rule 124 | len(line[7]) == 0, # Tradable 125 | line[18], 126 | ) 127 | 128 | if debug: 129 | print(f"Name: {name}") 130 | print(f"Special Text: {special_text}") 131 | print(f"Kind: {kind}") 132 | print(f"Weight: {cargo_type}") 133 | print(f"Fame: {fame}") 134 | print(f"Additional Rule: {additional_rule}") 135 | print(f"Tradable: {tradable}") 136 | 137 | return ( 138 | name, 139 | cargo_type, 140 | fame, 141 | special_text, 142 | kind, 143 | additional_rule, 144 | tradable, 145 | symbology, 146 | ) 147 | 148 | 149 | def process_gift_entry(line, assets, save_path, unused_art): 150 | """Process a single line from the CSV file.""" 151 | try: 152 | ( 153 | name, 154 | cargo_type, 155 | fame, 156 | special_text, 157 | kind, 158 | additional_rule, 159 | tradable, 160 | symbology, 161 | ) = extract_line_data(line, False) 162 | 163 | try: 164 | best_match = name 165 | stem = clean_raw_name(name) 166 | for art in unused_art: 167 | if stem == clean_raw_name(art): 168 | best_match = art 169 | break 170 | 171 | unused_art.discard(best_match) 172 | fg = Image.open(ASSETS_DIR / "gifts" / 173 | f"{best_match}").convert("RGBA") 174 | except FileNotFoundError: 175 | print(missing_art_error(name)) 176 | fg = assets["none"].copy().convert("RGBA") 177 | 178 | fg.thumbnail((875, 875)) 179 | ring = determine_ring(kind, cargo_type, assets, len(special_text) != 0) 180 | bg = assets["background"].copy() 181 | fg = rmbg(fg) 182 | center = ((bg.width - fg.width) // 2, (bg.height - fg.height) // 2) 183 | 184 | if not tradable: 185 | ring.paste( 186 | assets["notrade"], 187 | ( 188 | (ring.width - assets["notrade"].width) // 2, 189 | (ring.height - assets["notrade"].height) // 2, 190 | ), 191 | assets["notrade"], 192 | ) 193 | 194 | bg.paste(fg, (center[0], center[1]), fg) 195 | double = True 196 | if "n" in kind and cargo_type != "p": 197 | double = False 198 | 199 | draw = ImageDraw.Draw(ring) 200 | if not textless: 201 | draw.text( 202 | (ring.width // 2, (ring.height - 150) - (50 if double else 0)), 203 | fame + "*" if additional_rule else fame, 204 | (0, 0, 0), 205 | font=assets["font"], 206 | anchor="mm", 207 | ) 208 | 209 | if special_text and not textless: 210 | process_special_text( 211 | draw, assets["font"], name, special_text, ring) 212 | 213 | # print(f"Name: {name}") 214 | # print(f"Special Text: {special_text}") 215 | # print(f"Kind: {kind}") 216 | # print(f"Weight: {cargo_type}") 217 | # print(f"Fame: {fame}") 218 | # print(f"Additional Rule: {additional_rule}") 219 | # print(f"Tradable: {tradable}") 220 | 221 | bg.resize((450, 450)) 222 | bg.paste(ring, (0, 0), ring) 223 | fg.close() 224 | ring.close() 225 | final = bg.convert("RGBA") 226 | final.resize((450, 450)) 227 | final.save(os.path.join(save_path, f"{name}.png"), dpi=(300, 300)) 228 | print(colors.GREEN + f"EXPORTED: {name}.png" + colors.RESET) 229 | 230 | except Exception as e: 231 | print(colors.RED + f"ERROR processing {name}: {e}" + colors.RESET) 232 | 233 | 234 | def compile_all(clean: bool = True, open_output: bool = True): 235 | save_path = OUTPUT_DIR / "gifts" 236 | 237 | if clean: 238 | clear_directory(save_path) 239 | 240 | if open_output: 241 | os.system(f"open {save_path}") 242 | 243 | assets = load_assets() 244 | if not assets: 245 | return 246 | 247 | unused_art = list_art_files(ASSETS_DIR / "gifts/") 248 | with open("raw_spreadsheet_data/gifts.csv") as file: 249 | reader = csv.reader(file) 250 | next(reader) 251 | 252 | for line in reader: 253 | if "EOF" in line or len(line) == 0: 254 | print("Done. Remaining Art:") 255 | for name in unused_art: 256 | print(name) 257 | return 258 | process_gift_entry(line, assets, save_path, unused_art) 259 | -------------------------------------------------------------------------------- /stormy/themes.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | 3 | import csv 4 | 5 | from PIL import Image, ImageDraw, ImageFont 6 | 7 | from stormy.utils import ( 8 | body_font, 9 | clean_raw_name, 10 | clear_directory, 11 | colors, 12 | list_art_files, 13 | missing_art_error, 14 | title_font, 15 | rmbg, 16 | ) 17 | 18 | 19 | def compile_all(clean: bool = True, open_output: bool = True): 20 | import os 21 | 22 | save_path = "output/themes" 23 | if clean: 24 | clear_directory(save_path) 25 | 26 | if open_output: 27 | os.system(f"open {save_path}") 28 | 29 | with open("raw_spreadsheet_data/new themes.csv") as file: 30 | print(colors.BLUE + "Reading themes file" + colors.RESET + "...") 31 | reader = csv.reader(file, skipinitialspace=True) 32 | image_size = (390, 600 - 15) 33 | 34 | try: 35 | print("Preliminary image loading...") 36 | bg = Image.open("assets/bg/theme.png").convert("RGBA") 37 | except FileNotFoundError: 38 | print(colors.RED + "ERROR: Font or image not found." + colors.RESET) 39 | return 40 | 41 | next(reader) 42 | for line in reader: 43 | local_bg = bg.copy() 44 | try: 45 | title = clean_raw_name(line[0]) 46 | if title == "EOF": 47 | print("reached, end of file") 48 | break 49 | 50 | text, kind = ( 51 | line[2].join(["\n", "\n"]), 52 | line[3] if line[3] else "0", 53 | ) 54 | 55 | if kind.lower() == "economic": 56 | color = "#E89C23" 57 | elif kind.lower() == "voyage": 58 | color = "#3E5365" 59 | elif kind.lower() == "divine": 60 | color = (0, 0, 0) 61 | else: 62 | color = "#B74141" 63 | 64 | try: 65 | if title.find("PIRATE") != -1: 66 | fg = Image.open("assets/themes/PIRATESGENERIC.png") 67 | else: 68 | # Try PNG first, fall back to TIFF if PNG not found 69 | png_path = f"assets/themes/{title}.png" 70 | tiff_path = f"assets/themes/{title}.tiff" 71 | 72 | if os.path.exists(png_path): 73 | fg = Image.open(png_path).convert("RGBA") 74 | elif os.path.exists(tiff_path): 75 | fg = Image.open(tiff_path).convert("RGBA") 76 | else: 77 | # If neither exists, this will raise FileNotFoundError 78 | fg = Image.open(png_path).convert("RGBA") 79 | 80 | except Exception as _: 81 | print(missing_art_error(title)) 82 | fg = Image.new( 83 | "RGBA", 84 | (image_size[0] // 2, image_size[1] // 2), 85 | (255, 255, 255, 0), 86 | ) 87 | 88 | margin = 120 89 | draw = ImageDraw.Draw(local_bg) 90 | title = line[0].upper() 91 | 92 | dumb = False 93 | if "fair wind" in title.lower(): 94 | dumb = True 95 | title = "FAIR WIND AND \n SMOOTH SEAS" 96 | 97 | draw.multiline_text( 98 | (local_bg.width // 2, 160 if dumb else 120), 99 | title, 100 | color, 101 | font=title_font, 102 | align="center", 103 | spacing=0, 104 | anchor="mm", 105 | ) 106 | 107 | fg.thumbnail(image_size, Image.Resampling.LANCZOS) 108 | fg = rmbg(fg) 109 | fg_position = ((local_bg.width - fg.width) // 2, 110 | (70 + margin + (30 if dumb else 0))) 111 | local_bg.paste(fg, fg_position, fg) 112 | 113 | # divider_pos = ( 114 | # (local_bg.width - divider.width) // 2, 115 | # bg.height // 2 + 80, 116 | # ) 117 | # 118 | # local_bg.paste(divider, divider_pos, divider) 119 | 120 | extra = 0 121 | if "shipwork" in title.lower() or "contributions" in title.lower(): 122 | extra += 80 123 | 124 | body = textwrap.fill(text[1:], width=35) 125 | draw.multiline_text( 126 | ( 127 | (local_bg.width) // 2, 128 | fg.height + fg_position[1] + 80 + extra, 129 | ), 130 | body, 131 | "black", 132 | font=body_font, 133 | anchor="mm", 134 | align="left", 135 | spacing=5, 136 | ) 137 | 138 | # flavor text 139 | # flavor = textwrap(flavor, margins, local_bg.width, font=italic_flavor_font) 140 | 141 | # in_parens = False 142 | # current_h -= 70 143 | # # margins = 80 144 | # font = italic_flavor_font 145 | # open_citation = False 146 | # for line in flavor: 147 | # # what we want to do now, is go word by word, and insert insert the padding between each, so that they are flush with the sides of the card 148 | # line_w, h = textsize(line, body_font) 149 | # current_w = 0 150 | # for word in custom_split(line): 151 | # if word == "(": 152 | # in_parens = True 153 | # 154 | # if in_parens: 155 | # if word == "_": 156 | # open_citation = not open_citation 157 | # 158 | # if open_citation: 159 | # font = italic_flavor_font 160 | # else: 161 | # font = normal_flavor_font 162 | # 163 | # if word != "_": 164 | # draw.text( 165 | # ( 166 | # margins + current_w, 167 | # (local_bg.height // 2) + current_h + 100, 168 | # ), 169 | # f"{word} ", 170 | # (40, 40, 40), 171 | # font=font, 172 | # ) 173 | # 174 | # current_w += textsize(f"{word} ", font)[0] 175 | # 176 | # # only set the width when we arent' about to be done 177 | # current_w = 0 178 | # current_h += h + pad 179 | # 180 | # if not for_print: 181 | # back = Image.open("assets/rect.png") 182 | # # paste at center 183 | # back.paste( 184 | # local_bg, 185 | # ((back.width - local_bg.width) // 2, (back.height - local_bg.height) // 2), 186 | # local_bg, 187 | # ) 188 | # back.save(f"themes_output/{title}.png") 189 | 190 | # add the cost circle to the upper right corner 191 | # insert = 20 192 | # circle_chords = (local_bg.width - 98 - insert, insert) 193 | # local_bg.paste( 194 | # cost_circle, 195 | # circle_chords, 196 | # cost_circle, 197 | # ) 198 | # 199 | # if oracle_cost != 0: 200 | # oracle_cost_chords = ( 201 | # local_bg.width - 98 - insert - 80, 202 | # insert + 10, 203 | # ) 204 | # local_bg.paste( 205 | # oracle_cost_circle, 206 | # oracle_cost_chords, 207 | # oracle_cost_circle, 208 | # ) 209 | # 210 | # # the text should always be in the center of the circle 211 | # cost_size = textsize(cost, cost_font) 212 | # draw.text( 213 | # ( 214 | # circle_chords[0] + (98 - cost_size[0]) // 2, 215 | # circle_chords[1] - 10, 216 | # ), 217 | # cost, 218 | # (0, 0, 0), 219 | # font=cost_font, 220 | # ) 221 | # 222 | Template = Image.new("RGBA", (825, 1125), (180, 64, 65)) 223 | Template.paste( 224 | local_bg, 225 | ( 226 | (Template.width - local_bg.width) // 2, 227 | (Template.height - local_bg.height) // 2, 228 | ), 229 | local_bg, 230 | ) 231 | 232 | Template.save(f"{save_path}/{clean_raw_name(title)}.tiff") 233 | print(colors.GREEN + "Exported: " + 234 | colors.RESET + f"{title}.tiff") 235 | # catch everything and print the error 236 | except Exception as e: 237 | print(colors.RED + 238 | f"Export failed, {e}" + colors.RESET + title) 239 | -------------------------------------------------------------------------------- /raw_spreadsheet_data/gifts.csv: -------------------------------------------------------------------------------- 1 | "NAME","COUNT","CARGO TYPE (Heavy, Medium, Light, Raw, Pot)","checkerboard","REL FAME","SPECIAL TEXT","WEAPON, ARMOR, RANGED, EXPERT","SPECIAL RULE","Non Tradable","SIZE H","SIZE M","SIZE L","FAME * QUANTITY","","FAME","PACKAGE TYPE 1","PACKAGE TYPE 2","NAME","Cargo Type New","Symbology (Fist-Heart-Wits-Bulk-Midweight-Elite)" 2 | "ALMONDS","1","H","1000","2","","n","","","","","","","","4","Food","","ALMONDS","Midweight","MM" 3 | "AMPHORA ONE","1","P","1000","2","","n","","","4","","","4","","3","Food","","AMPHORA ONE","Midweight","MB" 4 | "AMPHORA THREE","1","P","0100","2","","n","","","4","","","12","","2","Food","","AMPHORA THREE","Bulk","BB" 5 | "AMPHORA TWO","1","P","0010","2","","n","","","4","","","8","","1","Food","","AMPHORA TWO","Bulk","BB" 6 | "AXE","1","L","1110","3","_+1d6\nATK","w","","","","2","","10","","5","Military","Ceremonial","AXE","Elite","FE" 7 | "BABOONS","1","M","0101","5","","n","","","","2","","18","","9","Elite","","BABOONS","Midweight","HM" 8 | "BEES","1","L","0111","4","","n","","","","","1","8","","4","Elite","","BEES","Elite","HE" 9 | "BRACELET","1","L","1011","3","","n","","","","","2","16","","5","Elite","","BRACELET","Elite","HE" 10 | "CANAANITE AMPHORA","3","P","1000","1","","n","","","","","","","","1","Food","","CANAANITE AMPHORA","bulk","BBB" 11 | "CANAANITE AMPHORA ALT","3","P","0100","1","","n","","","","","","","","1","Food","","CANAANITE AMPHORA ALT","bulk","BBB" 12 | "CANAANITE GOD","1","M","1010","3","","n","","","","","","","","9","Religious","Elite","CANAANITE GOD","Elite","EE" 13 | "CARNELIAN BEADS","1","L","0001","2","","n","","","","","2","12","","4","Elite","","CARNELIAN BEADS","Elite","HE" 14 | "CAT","1","M","0000","3","","n","At each Palace, cat returns to Gift Pool on 1–3. Worth 20 Fame at the end of game.","x","","1","","0","","50","","","CAT","Elite","H" 15 | "CEDAR","2","M","1010","1","_^+2 Ship","n","Play during Recovery / Refit only at a Palace ","","3","","","18","","6","Ship","Building","CEDAR","Bulk","BB" 16 | "CHIEF HERALD","1","M","1111","","","x","","","","","","","","20","Diplomacy","Expert","CHIEF HERALD","Midweight","FWH" 17 | "CLOTH","2","M","1000","2","_+1\nSHIP","n","May be played at any time, but will not prevent a Swept To outcome.","","","6","","15","","3","Ship","Dowry","CLOTH","Midweight","MMBB" 18 | "COMMON BOW","1","L","1110","3","_|2d6\nRNG","r","One preliminary 2d6 attack during Trial Combat or Ship Melee (including Pirates).","","","","3","9","","2","Military","Ceremonial","COMMON BOW","elite","FE" 19 | "COMMON SPEAR","1","L","0111","3","_+2d6\nATK|1d6\nRNG","rw","If equipped as Ranged weapon, one preliminary 1d6 attack during Trial Combat or Ship Melee (including Pirates).","","","","2","4","","2","Military","","COMMON SPEAR","elite","FE" 20 | "COMMON SWORD","1","L","1011","3","_+1d6\nATK","w","","","","","3","9","","3","Military","","COMMON SWORD","elite","FE" 21 | "COMMON THORAX","1","L","1101","3","_+2d6\nDEF","a","","","","","","","","2","Military","Ceremonial","COMMON THORAX","elite","HE" 22 | "COPPER INGOT","8","H","0101","2","_^2d6 DIPL","n","Cannot be used for Diplomacy on Cyprus","","4","","","24","","4","Military","","COPPER INGOT","Bulk","BME" 23 | "DATES","1","R","1010","1","","n","","","","","","","","6","Food","","DATES","Midweight","MM" 24 | "EARRINGS","1","L","0110","4","","n","","","","","2","10","","5","Elite","","EARRINGS","Elite","HE" 25 | "EBONY","2","H","0011","2","","n","","","2","","","10","","5","Building","Elite","EBONY","Midweight","MM" 26 | "EXPERT HELMSMAN","1","M","1111","","","x","","","","","","","","20","Military","Expert","EXPERT HELMSMAN","Midweight","FWH" 27 | "FRUITS","2","P","0110","1","_^+1 CREW","n","Play at any time, but may not be used to prevent Swept To result.","","","","2","6","","0","Food","","FRUITS","Midweight","MM" 28 | "FUGITIVE DIVINER","1","M","1111","","","x","","","","","","","","20","Religious","Expert","FUGITIVE DIVINER","Midweight","WWM" 29 | "GLASS INGOTS","2","H","0011","2","","n","","","4","","","16","","5","Elite","","GLASS INGOTS","Bulk","BBE" 30 | "GOAT","4","H","0100","2","_+3d6\nORC","n","Can be played in addition to other Gift in oracle attempt, and adds +3.","","6","","","6","","1","Religious","","GOAT","Bulk","BBW" 31 | "GOLD","4","M","1011","4","","n","","","","3","","28","","14","Elite","Dowry","GOLD","Elite","EEE" 32 | "GOLD CHALICE","1","L","1101","4","","n","","","","","3","21","","8","Elite","Dowry","GOLD CHALICE","Elite","EE" 33 | "GOLD RHYTON","1","M","1100","4","","n","","","","1","","8","","8","Elite","Ceremonial","GOLD RHYTON","Elite","EE" 34 | "GREAVES","1","L","0101","3","_+1d6\nDEF","a","","","","","","","","3","Military","","GREAVES","Elite","HE" 35 | "Healer Heading Home","1","M","1111","","","x","","","","","","","","20","Expert","","Healer Heading Home","Midweight","HWF" 36 | "HELMET","1","L","0111","4","_+2d6\nDEF","a","","","","2","","10","","3","Military","","HELMET","Elite","FE" 37 | "IRON KNIFE","1","L","1000","2","_+1d6\nATK","w","","","","","2","6","","3","Military","","IRON KNIFE","Elite","FE" 38 | "IVORY DUCK","1","L","1001","3","","n","","","","","2","8","","5","Elite","","IVORY DUCK","Elite","HE" 39 | "IVORY RAW","1","M","0101","4","","n","","","","3","","18","","6","Elite","Building","IVORY RAW","Midweight","MM" 40 | "JEWELRY","1","L","0100","","","n","","","","","2","14","","6","Elite","","JEWELRY","Elite","HE" 41 | "LAPIS LAZULI CYLINDER SEAL","1","L","0011","4","_+1d6|DIPL","n","_+1d6 to one Diplomacy attempt. Return to Gift Pool","","","","1","7","","6","Elite","Diplomacy","LAPIS LAZULI CYLINDER SEAL","Elite","FWE" 42 | "LEKYTHION","1","M","0100","3","","n","","","","4","","10","","5","Elite","","LEKYTHION","Midweight","M" 43 | "LYRE","1","M","0101","3","","n","Allows one Helen/Pahntom Willingness check each Upkeep (6.2). Roll 1d6; on a 5/6 Unwilling Helen/Phantom becomes Willing. Paris may not use Lyre for second Willingness check; but Menelaos may.","","","","","","","6","Elite","Religious","LYRE","Elite","HWE" 44 | "MASTER ARCHER","1","M","1111","","","x","","","","","","","","20","Expert","Military","MASTER ARCHER","Midweight","HWF" 45 | "MASTER NAVIGATOR","1","M","1111","","","x","","","","","","","","20","Expert","Military","MASTER NAVIGATOR","Midweight","HWF" 46 | "MUSIC GIRLS","2","H","1001","4","","n","","","2","","","20","","10","Elite","Dowry","MUSIC GIRLS","Bulk","HHBBB" 47 | "MYCENEAN JAR","2","P","0010","1","","n","","","3","","","9","","4","Food","","MYCENEAN JAR","Bulk","BB" 48 | "NECKLACE","1","L","1000","2","","n","","","","","","","","2","Elite","","NECKLACE","Elite","HE" 49 | "NEPENTHE","5","L","0111","5","_^HEAL 2","n","Play during Recovery / Refit only at a Palace ","","","","2","16","","8","Elite","","NEPENTHE","Elite","HHEE" 50 | "OARS","2","H","0010","0","_+1\nShip","n","Play during Recovery / Refit only at a Palace ","","2","","","4","","1","Military","","OARS","Bulk","FB" 51 | "Olive Oil","2","R","0110","2","","n","","","","","","","","7","Food","","Olive Oil","Bulk","B" 52 | "OSTRICH EGG","1","M","1000","4","","n","","","","3","","3","","3","Elite","","OSTRICH EGG","Elite","E" 53 | "Pomegranate","1","R","0010","2","","n","","","","","","","","4","Food","","Pomegranate","Midweight","M" 54 | "RATS","1","M","0000","0","","n","When Rats is drawn, it occupies one Medium Cargo space and cannot be spent. Can only be removed by possessing Cat for at least one turn.","x","","","1","-5","","-10","","","RATS","Midweight","" 55 | "RESIN","2","M","0001","2","","n","Can be played in addition to other Gift for Oracle attempt.","","","4","","15","","5","Ship","Religious","RESIN","Midweight","WM" 56 | "ROPE","2","M","0010","0","_^+1 Ship","n","May be played at any time, but will not prevent a Swept To outcome.","","","3","","3","","1","Ship","","ROPE","bulk","FB" 57 | "SCALE","1","L","0110","4","","n","May add abiltiy later","","","","","","","2","Meta","","SCALE","Midweight","WM" 58 | "SCARAB OF NEFERTITI","1","L","0010","5","","n","","","","","1","5","","5","Elite","","SCARAB OF NEFERTITI","Elite","HE" 59 | "SHIELD AND SPEAR","1","L","1111","4","_+1d6\nDEF|+1d6\nATK","wa","","","","2","","30","","5","Military","","SHIELD AND SPEAR","Elite","FE" 60 | "SILVER-HILTED SWORD","1","L","0111","4","_+2d6\nATK","w","","","","","2","12","","6","Military","Ceremonial","SILVER-HILTED SWORD","Elite","FE" 61 | "SINGER","1","M","1010","","","x","","","","","","","","20","Expert","Ceremonial","SINGER","Midweight","HHWWM" 62 | "SLAVES","10","H","0110","4","","n","","","","","","","","10","Elite","","SLAVES","bulk","BBB" 63 | "SPICES","2","L","1001","3","","n","","","","","3","21","","7","Elite","","SPICES","Midweight","HM" 64 | "STAND","1","M","0010","2","","n","","","","4","","12","","7","Elite","","STAND","Elite","WE" 65 | "STATUE","1","L","0100","3","","n","","","","","1","12","","10","Religious","","STATUE","Midweight","HMM" 66 | "STONE SCEPTER","1","M","1000","2","","n","","","","2","","10","","5","Elite","Ceremonial","STONE SCEPTER","Elite","FE" 67 | "SWORDS","1","L","1010","4","_+2d6\nATK","w","","","","","","","","5","Military","","SWORDS","Elite","FE" 68 | "SYMPOSIUM BOWL","1","L","0001","2","","n","","","","","2","10","","6","Elite","","SYMPOSIUM BOWL","Elite","HE" 69 | "TABLET","1","L","0101","1","","n","During any Hospitality encounter involving Gifts, draw +3 Gifts and return Tablet to Gift Pool.","","","","2","8","","0","Meta","","TABLET","Elite","WE" 70 | "THORAX","1","L","1101","4","_+1d6\nDEF","a","","","","2","","16","","5","Military","Ceremonial","THORAX","Elite","FE" 71 | "TIN INGOT","5","M","1010","2","","n","","","","8","","18","","3","Military","Trade","TIN INGOT","bulk","FFBB" 72 | "TORTOISE SHELL","1","M","0011","3","","n","","","","2","","6","","3","Ceremonial","","TORTOISE SHELL","Midweight","HWM" 73 | "TRIPOD ONE","1","M","0100","2","","n","","","","6","","10","","5","Elite","Religious","TRIPOD ONE","Midweight","HM" 74 | "TRIPOD TWO","1","M","0001","2","","n","","","","4","","8","","8","Elite","Religious","TRIPOD TWO","Midweight","HMM" 75 | "UNCOMMON SPEAR","1","L","0111","4","_+2d6\nATK","w","","","","","","","","4","Military","","UNCOMMON SPEAR","Elite","FE" 76 | "VESSEL BLUE","1","H","0010","2","","n","","","3","","","12","","4","Food","","VESSEL BLUE","Midweight","M" 77 | "VESSEL BLUE TWO","1","M","0100","2","","n","","","","4","","10","","5","Food","","VESSEL BLUE TWO","Midweight","M" 78 | "VESSEL RED","1","H","0010","1","","n","","","3","","","4","","2","Food","","VESSEL RED","Midweight","M" 79 | "VESSEL SILVER","1","M","1000","3","","n","","","","3","","6","","6","Elite","","VESSEL SILVER","elite","E" 80 | "VESSEL TALL","1","H","","2","","n","","","3","","","6","","3","Food","","VESSEL TALL","bulk","B" 81 | "EOF","","","","","","","","","","","","","","","","","EOF","","" 82 | "","","","","","","","","TOTALS","51","73","45","","","","","","","","" 83 | "","106","","","","","","","","","","","689","","","","","","","" -------------------------------------------------------------------------------- /stormy/voyage.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import random 3 | from functools import lru_cache 4 | from pathlib import Path 5 | from stormy.utils import end 6 | 7 | from PIL import Image, ImageDraw, ImageFont 8 | 9 | # Constants 10 | SAVE_PATH = Path("output/voyage") 11 | ASSETS_DIR = Path("assets") 12 | SEASON_COLORS = { 13 | "Winter": "#2D649D", 14 | "Spring": "#8DA074", 15 | "Summer": "#DD922A", 16 | "Autumn": "#7B2F20", 17 | } 18 | WIND_TABLES = { 19 | "Winter": [8, 6, 6, 5, 3, 3, 6, 2, 2], 20 | "Spring": [9, 5, 6, 4, 4, 3, 3, 2, 4], 21 | "Summer": [16, 4, 2, 5, 3, 2, 5, 2, 2], 22 | "Autumn": [11, 6, 6, 3, 4, 2, 3, 2, 3], 23 | } 24 | LOCATIONS = [ 25 | "Issikon Pelagos", 26 | "Aigyption Pelagos", 27 | "Pelagos Tyron", 28 | "Lykion Pelagos", 29 | "Libykon Pelagos", 30 | ] 31 | 32 | # Asset paths 33 | ASSETS = { 34 | "bg": ASSETS_DIR / "bg/waves.jpg", 35 | "table": ASSETS_DIR / "components/wind.png", 36 | "font": ASSETS_DIR / "regular.ttf", 37 | "arrow_light": ASSETS_DIR / "components/arrow_light.png", 38 | "arrow_light_diagonal": ASSETS_DIR / "components/diagonal_light.png", 39 | "arrow_medium": ASSETS_DIR / "components/arrow_medium.png", 40 | "arrow_medium_diagonal": ASSETS_DIR / "components/diagonal_medium.png", 41 | "arrow_heavy": ASSETS_DIR / "components/arrow_heavy.png", 42 | "arrow_heavy_diagonal": ASSETS_DIR / "components/diagonal_heavy.png", 43 | } 44 | 45 | # Overall scaling factor for both table and arrow 46 | SCALE_FACTOR = 1.15 47 | 48 | 49 | def get_random_locations(): 50 | """Return 1-3 random non-repeating locations as a string.""" 51 | num_locations = random.randint(1, 3) 52 | return "\n".join(random.sample(LOCATIONS, num_locations)) 53 | 54 | 55 | def generate_wind_table(season): 56 | """Generate a slightly randomized wind table based on the season.""" 57 | base = WIND_TABLES.get( 58 | season, [0] * 9 59 | ).copy() # Use copy to avoid modifying original 60 | 61 | for i in range(9): 62 | if base[i] > 3: 63 | for _ in range(3): 64 | if random.randint(1, 4) == 1: 65 | base[i] += random.choice([-1, 1]) 66 | return base 67 | 68 | 69 | @lru_cache(maxsize=32) 70 | def load_asset(asset_name): 71 | """Load and cache an image asset.""" 72 | try: 73 | return Image.open(ASSETS[asset_name]).convert("RGBA") 74 | except Exception as e: 75 | print(f"Error loading {asset_name}: {e}") 76 | return None 77 | 78 | 79 | @lru_cache(maxsize=2) 80 | def get_font(size): 81 | """Load and cache a font at specified size.""" 82 | try: 83 | return ImageFont.truetype(str(ASSETS["font"]), size) 84 | except Exception as e: 85 | print(f"Error loading font: {e}") 86 | return None 87 | 88 | 89 | def load_assets(): 90 | """Load all necessary assets for card generation.""" 91 | assets = { 92 | "bg": load_asset("bg"), 93 | "table": load_asset("table"), 94 | "arrow_light": load_asset("arrow_light"), 95 | "arrow_light_diagonal": load_asset("arrow_light_diagonal"), 96 | "arrow_medium": load_asset("arrow_medium"), 97 | "arrow_medium_diagonal": load_asset("arrow_medium_diagonal"), 98 | "arrow_heavy": load_asset("arrow_heavy"), 99 | "arrow_heavy_diagonal": load_asset("arrow_heavy_diagonal"), 100 | "body_font": get_font(34), 101 | "title_font": get_font(90), 102 | } 103 | 104 | # Check if all assets loaded successfully 105 | if None in assets.values(): 106 | print("Failed to load one or more assets") 107 | return None 108 | 109 | return assets 110 | 111 | 112 | def get_random_arrow_set(): 113 | """Randomly select arrow weight based on probability.""" 114 | roll = random.randint(1, 16) 115 | if roll <= 3: # ~19% chance for heavy 116 | return "arrow_heavy", "arrow_heavy_diagonal" 117 | elif roll <= 7: # ~25% chance for medium 118 | return "arrow_medium", "arrow_medium_diagonal" 119 | else: # ~56% chance for light 120 | return "arrow_light", "arrow_light_diagonal" 121 | 122 | 123 | def scale_image(image, scale_factor): 124 | """Scale an image by the given factor.""" 125 | if scale_factor == 1.0: 126 | return image.copy() 127 | 128 | new_width = int(image.width * scale_factor) 129 | new_height = int(image.height * scale_factor) 130 | return image.resize((new_width, new_height), Image.Resampling.LANCZOS) 131 | 132 | 133 | def process_voyage_data(): 134 | """Process CSV data and generate voyage cards.""" 135 | # Create output directory if it doesn't exist 136 | SAVE_PATH.mkdir(parents=True, exist_ok=True) 137 | assets = load_assets() 138 | if not assets: 139 | return 140 | half_width = assets["bg"].width // 2 141 | spacing = 48 142 | base_circle_chords = [(50 + (j % 3) * 175, 50 + (j // 3) * 175) for j in range(9)] 143 | 144 | try: 145 | with open("raw_spreadsheet_data/voyage.csv") as file: 146 | print("\033[33mReading voyage file\033[0m...") 147 | reader = csv.reader(file, skipinitialspace=True) 148 | next(reader) # Skip header 149 | 150 | for i, line in enumerate(reader, start=1): 151 | if end(line): 152 | print("END OF FILE") 153 | break 154 | 155 | try: 156 | # Unpack data 157 | season, _, mp, *_ = line[:8] 158 | 159 | # Select arrow style 160 | arrow_key, diagonal_key = get_random_arrow_set() 161 | 162 | # Get original arrows 163 | storm_arrow_original = assets[arrow_key] 164 | diagonal_arrow_original = assets[diagonal_key] 165 | 166 | # Scale the arrows 167 | storm_arrow = scale_image(storm_arrow_original, SCALE_FACTOR) 168 | diagonal_arrow = scale_image(diagonal_arrow_original, SCALE_FACTOR) 169 | 170 | # Select local arrow (25% chance for diagonal) 171 | local_arrow = ( 172 | diagonal_arrow.copy() 173 | if random.random() < 0.25 174 | else storm_arrow.copy() 175 | ) 176 | 177 | # Rotate arrow randomly 178 | local_arrow = local_arrow.rotate( 179 | random.choice([0, 90, 180, 270]), 180 | expand=True, 181 | resample=Image.BICUBIC, 182 | ) 183 | 184 | # Scale the table 185 | table = scale_image(assets["table"], SCALE_FACTOR) 186 | 187 | # Generate wind table 188 | wind_vals = generate_wind_table(season) 189 | 190 | # Draw wind values on the table with scaled positions 191 | draw_table = ImageDraw.Draw(table) 192 | for j, wind_val in enumerate(wind_vals): 193 | if wind_val: 194 | # Scale the chord positions 195 | pos_x = int(base_circle_chords[j][0] * SCALE_FACTOR) 196 | pos_y = int(base_circle_chords[j][1] * SCALE_FACTOR) 197 | draw_table.text( 198 | (pos_x, pos_y), 199 | str(wind_val), 200 | "black", 201 | font=assets["body_font"], 202 | anchor="mm", 203 | ) 204 | 205 | # Calculate position to center the table on the arrow 206 | table_pos = ( 207 | (local_arrow.width - table.width) // 2, 208 | (local_arrow.height - table.height) // 2, 209 | ) 210 | 211 | # Create the combined arrow+table image 212 | local_arrow.paste(table, table_pos, table) 213 | 214 | # Create the background canvas 215 | canvas = assets["bg"].copy() 216 | 217 | # Position the arrow on the canvas 218 | local_arrow_pos = ( 219 | (canvas.width - local_arrow.width) // 2, 220 | (((canvas.height - local_arrow.height) * 19) // 20), 221 | ) 222 | canvas.paste(local_arrow, local_arrow_pos, local_arrow) 223 | 224 | # Add text to the canvas 225 | draw = ImageDraw.Draw(canvas) 226 | current_h = canvas.width // 10 + spacing 227 | 228 | # Season title 229 | draw.text( 230 | (half_width, current_h), 231 | season.upper(), 232 | SEASON_COLORS[season], 233 | font=assets["title_font"], 234 | anchor="ms", 235 | ) 236 | current_h += spacing * 2 237 | 238 | # Movement points 239 | draw.text( 240 | (half_width, current_h), 241 | f"MOVEMENT POINTS: {mp}", 242 | "black", 243 | font=assets["body_font"], 244 | anchor="ms", 245 | ) 246 | current_h += spacing 247 | 248 | # Storm locations 249 | draw.text( 250 | (half_width, current_h), 251 | f"STORM: {get_random_locations()}!", 252 | "black", 253 | font=assets["body_font"], 254 | anchor="ms", 255 | ) 256 | 257 | # Save the card 258 | filename = SAVE_PATH / f"{season.upper()}{i}.tiff" 259 | canvas.thumbnail((825, 1125), Image.LANCZOS) 260 | canvas.save(filename) 261 | print(f"\033[32mExported:\033[0m {filename}") 262 | 263 | except Exception as e: 264 | print(f"Error processing card {i}: {e}") 265 | 266 | except Exception as e: 267 | print(f"Error reading CSV file: {e}") 268 | 269 | 270 | def compile_all(clean=True, open_output=True): 271 | """Compile all voyage cards.""" 272 | import os 273 | import shutil 274 | 275 | # Clean output directory if requested 276 | if clean and SAVE_PATH.exists(): 277 | shutil.rmtree(SAVE_PATH) 278 | print(f"Cleaned directory: {SAVE_PATH}") 279 | 280 | SAVE_PATH.mkdir(parents=True, exist_ok=True) 281 | 282 | # Process voyage data 283 | process_voyage_data() 284 | 285 | # Open output directory if requested 286 | if open_output and SAVE_PATH.exists(): 287 | os.system(f"open {SAVE_PATH}") 288 | 289 | 290 | if __name__ == "__main__": 291 | compile_all() 292 | -------------------------------------------------------------------------------- /raw_spreadsheet_data/themes.csv: -------------------------------------------------------------------------------- 1 | "Name","Count","body text","theme","oracle cost","510 characters max","Rules clarifications","","","","","","","","","","","","","","","","","","","","" 2 | "Begging Off","1","Cancel another player's successful Diplomacy attempt by returning to the Gift Pool Gifts of the same or greater Fame as was used to secure the alliance.","1","","This mare Ekhepolos, son of Ankhises, gave to Agamemnon— / A gift, so as not to follow him to windy Ilion / But to stay at home enjoying himself (Homer _Iliad_ 23.296–8).","","","","","","","","","","","","","","","","","","","","","" 3 | "Bird Omen","1","Gain two Oracle Points.","1","","Yet good were the omen-birds for him setting out, / On the right hand (Homer, _Odyssey_ 24.311–12).","","","","","","","","","","","","","","","","","","","","","" 4 | "Taxation","1","Take one gift from each player in each region you have an allied palace. ","","","","","","","","","","","","","","","","","","","","","","","","" 5 | "Change of Heart","1","Flip the Willingness of ALL Helens/Phantoms.","2","1","Thither I will not go—it would be a reproach— / Sharing that man’s bed (Homer _Iliad_ 3.410–11).","","","","","","","","","","","","","","","","","","","","","" 6 | "Cypriot Pirates","1","Forces immediate Ship Melee with other Hero mid-voyage. Pirates' strength 7d6 reduced 1d6 for each Sea Region away from pirates' home Sea Region. Pirates are cancelled by any Storm in this Sea Region.","2","","Or do you rove at random / Like pirates over the sea who wander / Risking their souls, bringing evil to foreign people (Homer _Odyssey_ 3.72–4).","","","","","","","","","","","","","","","","","","","","","" 7 | "Disguise","1","Hero can visit Palace, regardless of diplomatic status, for Hospitality or Diplomacy, even in a Land Region that already has his Region Visited marker. ","1","","Disguised like that he entered the Trojans’ city and deceived they were, / Every one (Homer _Odyssey_ 4.249–50).","","","","","","","","","","","","","","","","","","","","","" 8 | "Dioskouroi","1","Add 7 Movement Points to current Voyage Card OR Cancel _Ambush_, _Hostage_, _Lying in Wait_, or _Waylaid_; move Ship to adjacent Sea Circle.","2","1","Horse-tamer Kastor and blameless Polydeukes, /... Saviours... / of swift-faring ships, whenever rush the winds of / Winter across the ungentle sea (_Homeric Hymn to the Dioskouroi_3–8).","","","","","","","","","","","","","","","","","","","","","" 9 | "Divine Mist","1","Decline or interrupt Trial Combat without Fame penalty OR One Palace becomes Hidden and cannot be voluntarily visited until a Player spends two Oracle Points to dispel.","1","1","And him Apollo snatched away / Very easily, as can a god, and hid him then in a great mist (Homer _Iliad_ 20.443–4).","","","","","","","","","","","","","","","","","","","","","" 10 | "Divine Patroness","2","Immediately cancel Storm OR Cause Storm in one Sea Region without Storm (3d6 to Crew & Ship, Threshhold 2; use Swept To location on Player's current Voyage Card) OR Change the Willingness of one Helen/Phantom OR +7/-7 Movement Points to any Voyage Card.","2","1","Hera sends a great Storm against them, under the force of which they make for Sidοn (ps.-Apollodorus Epitome 3.4).","","","","","","","","","","","","","","","","","","","","","" 11 | "Doldrums","1","All Circles in one Sea Region cost 6 Movement Points for the Player's turn (cancel current Storm in this location). ","2","","Then right away the wind left off and a calm / Befell, windless, and a god put the waves to sleep (Homer, _Odyssey_ 12.168–9).","","","","","","","","","","","","","","","","","","","","","" 12 | "Escape","3","Captured Hero or Unwilling Helen / Phantom Escapes OR Shipwrecked Hero Rescued OR Becalmed Hero automatically succeeds in wrestling and escaping Proteus OR Cancel your Hospitality Card; move Ship to adjacent Sea Circle.","2","1","But even from there, by courage and will and wit, / We escaped. These things too, I think, we shall someday look back on (Homer _Odyssey_ 12.211–12).","","","","","","","","","","","","","","","","","","","","","" 13 | "Fair Wind and Smooth Sea","2","All Sea Circles cost 2 Movement Points for this Player's turn; Hero may ignore all storms. ","1","1","On the third day from Sparta, Alexandros reached Troy, carrying Helen, with a fair wind and smooth sea (Herodotus 2.117). ","","","","","","","","","","","","","","","","","","","","","" 14 | "Foresight","1","Examine and re-arrange the top four Cards of the Theme Deck, Hospitality Deck, or Voyage Deck.","0","2","Among them rose / Kalkhas son of Thestor, much the best of diviners, / Who knew that which is, and what is to be, and what was before (Homer _Iliad_ 1.68–70).","","","","","","","","","","","","","","","","","","","","","" 15 | "Help from Eidothea","1","Becalmed Hero automatically succeeds in wrestling and escaping Proteus OR Shipwrecked Hero is Rescued OR Cancel _Doldrums_ or _Man Overboard_ OR +7 Movement Points on one Voyage Card.","1","1","Daughter of mighty Proteus, old man of the sea / Eidothea, for her heart most of all I stirred (Homer _Odyssey_ 4.365–6).","","","","","","","","","","","","","","","","","","","","","" 16 | "Hermes","2","Examine Helen/Phantoms at any two Palaces; if you swap them, roll Willingness for both OR Examine one Helen/Phantom; you may relocate it to a Ship or other Palace without one, rolling for Willingness OR Shipwrecked Hero is Rescued.","2","1","Hermes swept me high into the skycoves / And concealing me in a cloud, brought me to the house of Proteus (Euripides _Helen_ 45–6).","","","","","","","","","","","","","","","","","","","","","" 17 | "Hidden Reef","1","Cause 5d6 Ship Damage against any Hero's Ship in a Coastal Sea Circle. If Ship reduced to 0, Hero is Shipwrecked in the closest non-Palace Coastal Circle. ","2","","There is no clear way out from the grey sea anywhere, / For on the far side are sharp crags, and all around the surf / Roars rushing, and rock runs up smooth, / And deep close-in is the sea (Homer _Odyssey_ 5.410–14).","","","","","","","","","","","","","","","","","","","","","" 18 | "Inside Information","1","Examine all Theme Cards in one Player’s hand.","0","1","O friends, would not some man trust his own / Heart, daring to go among the great-hearted / Trojans, that he might perhaps... / Learn some plan in the Trojans’ midst, / Whatever they are devising among themselves (Homer _Iliad_ 10.204–8).","","","","","","","","","","","","","","","","","","","","","" 19 | "Inspiration","1","Immediately discard up to two Theme Cards, then draw that many plus one new Theme Cards.","0","1","But what I have in mind I will even say—what for my very / Self would I contrive, if such great need upon me came (Homer _Odyssey_ 5.188–9).","","","","","","","","","","","","","","","","","","","","","" 20 | "Intercept","4","During another player's Voyage, Hero or Allied Palace may attempt to Intercept the moving Hero. Intercept range = 2 Circles for Hero or Circles x Palace Rank for Allied Palace. Successful Intercepts proceed to Trial Combat or Ship Melee as appropriate. Trial Combat may be declined without Fame penalty; proceed to Ship Melee.","1","1","Seize this man, whoever he is, since he has wronged his Host; lead him here to me, so I may know what he has to say (Herodotus 2.114).","","","","","","","","","","","","","","","","","","","","","" 21 | "Word to the Wise","1","Helen's infidelity is a caution to all men. Add 2d6 to one Diplomacy roll OR Terminate Ship Melee with Pirates or Palace Interception after having inflicted at least one point of damage. ","2","","Agamemnon, sending a herald to each of the kings... urged each to make sure of his own wife, saying that the contempt show for Greece was one and the same for all (ps.-Apollodoros _Epitome_ 3.6). ","","","","","","","","","","","","","","","","","","","","","" 22 | "Leviathan","1","8d6 Sea Monster attacks any other player's Ship if in non-Palace, non-Coastal Sea Circle. Resolve as Ship Melee, but no retreat is possible. Hero gains 24 fame for killing Sea Monster. Cannot be played during active Ship Melee or Trial Combat.","3","","Or a great god may send me some monster / From the sea, the sort which famous Amphitrite nourishes in quantity (Homer _Odyssey_ 5.421–2).","","","","","","","","","","","","","","","","","","","","","" 23 | "Libyan Pirates","1","Forces immediate Ship Melee with other Hero mid-voyage. Pirates' strength 8d6 reduced 1d6 for each Sea Region away from home Sea Region. Pirates are cancelled by any Storm in this Sea Region.","2","","Or do you rove at random / Like pirates over the sea who wander / Risking their souls, bringing evil to foreign people (Homer _Odyssey_ 3.72–4).","","","","","","","","","","","","","","","","","","","","","" 24 | "Lukka Pirates","1","Forces immediate Ship Melee with other Hero mid-voyage. Pirates' strength 8d6 reduced 1d6 for each Sea Region away from home Sea Region. Pirates are cancelled by any Storm in this Sea Region.","2","","Or do you rove at random / Like pirates over the sea who wander / Risking their souls, bringing evil to foreign people (Homer _Odyssey_ 3.72–4).","","","","","","","","","","","","","","","","","","","","","" 25 | "Man Overboard","1","Play during another Hero's Voyage phase. Hero must discard an Expert of your choice OR Hero loses one Crew.","2","","There Phoebus Apollo the helmsman / Slew, attacking him with gentle bolts, / As he held the rudder in his hands while the ship sped on (Homer _Odyssey_ 3.279–81).","","","","","","","","","","","","","","","","","","","","","" 26 | "Master Shipwork","1","Attach this Card to your Ship after passing a complete turn at a Palace for Reovery and Refit, or when receiving a new Ship. Adds 2 Movement Points to all Voyage Cards.","3","","Harmonides, who knew how to fashion all intricate things / By hand... / Who even built balanced Ships for Alexandros (Homer _Iliad_ 5.60–2).","","","","","","","","","","","","","","","","","","","","","" 27 | "Night Attack","1","Conduct a Sack Palace attempt with +1d6 after drawing Gifts in Hospitality. If successful, collect normal gains but lose Fame = Palace Rank x 20.","2","","And after being carried to Sidon, Alexandros sacks the city (Proclus, _Chrestomathy_ 80).","","","","","","","","","","","","","","","","","","","","","" 28 | "Night Voyage","1","Play during another Hero's Voyage. Your Hero and Ship move instantly to any other Palace in your same Sea Region (undergoing any current Storm). Cannot be played to and from Thebes.","1","1","Aphrodite brings Helen and Alexandros together and, after their love-making, they load most (of Menelaos’) possessions and sail off by night (Proclus, _Chrestomathy_ 80).","","","","","","","","","","","","","","","","","","","","","" 29 | "Opposing Wind","1","Flip any player's Voyage Card 180 degrees and apply those Movement Point values for the current Player's Voyage.","1","1","But the force of wind drove them from there, / Greatly unwilling (Homer _Odyssey_ 13.266–7).","","","","","","","","","","","","","","","","","","","","","" 30 | "Phoenician Pirates","1","Forces immediate Ship Melee with other Hero mid-voyage. Pirates' strength 6d6 reduced 1d6 for each Sea Region away from home Sea Region. Pirates are cancelled by any Storm in this Sea Region.","2","","Or do you rove at random / Like pirates over the sea who wander / Risking their souls, bringing evil to foreign people (Homer _Odyssey_ 3.72–4).","","","","","","","","","","","","","","","","","","","","","" 31 | "Rumors","1","Add or remove any Region Visited token for any player and gain five Fame.","0","1","A rumor you may hear / From Zeus, which very often brings reports to men (Homer _Odyssey_ 1.282–3).","","","","","","","","","","","","","","","","","","","","","" 32 | "Sea Sacrifice","1","Sacrifice one Gift while at Sea; roll d6 x Fame spent and gain one Oracle or Theme Point for each success. ","1","","We take out to sea all the items necessary for the spirits of the dead (Euripides _Helen_ 1247).","","","","","","","","","","","","","","","","","","","","","" 33 | "Sea-God's Favor","1","For each Theme Point spent: +2 Movement Points to any Voyage Card OR -2d6 from Storm Strengths or Intercept attempt OR Cancel _Man Overboard_.","1","1","Hear me, Poseidon Earth-Shaker, and do not refuse / To accomplish these deeds for us who pray. (Homer _Odyssey_ 3.55–6).","","","","","","","","","","","","","","","","","","","","","" 34 | "Sea God Strikes","1","Giant Wave: Inflict 8d6 damage during other player's Voyage. For each success, target distributes to Crew or Ship (-2 Fame per Crew lost). If either Crew or Ship is reduced to 0, Hero is Shipwrecked in the nearest non-Palace Coastal Circle in current Sea Region. Cannot be played on a Hero already undergoing Storm.","2","0","","","","","","","","","","","","","","","","","","","","","","" 35 | "Shifting Wind","1","Rotate any Player's Voyage Card up to 90 degrees in either direction and apply new Movement Point values.","1","1","And next for us behind our dark-prowed ship / A favorable wind she sent, sail-filling, a fair companion (Homer _Odyssey_ 11.6–7).","","","","","","","","","","","","","","","","","","","","","" 36 | "Storm God Strikes","1","Thunderbolt: Inflict 10d6 damage during other player's Voyage. For each success, target distributes to Crew or Ship (-2 Fame per Crew lost). If either Crew or Ship is reduced to 0, Hero is Shipwrecked in the nearest non-Palace Coastal Circle in current Sea Region. Cannot be played on a Hero already undergoing Storm.","2","1","Then at once Zeus thundered and threw his lightning at the ship; / And it was shaken all-through... / and filled with smoke; and all the men fell from the ship (Homer _Odyssey_ 14.305–7).","","","","","","","","","","","","","","","","","","","","","" 37 | "Syrian Pirates","1","Forces immediate Ship Melee with other Hero mid-voyage. Pirates' strength 6d6 reduced 1d6 for each Sea Region away from home Sea Region. Pirates are cancelled by any Storm in this Sea Region.","2","","Or do you rove at random / Like pirates over the sea who wander / Risking their souls, bringing evil to foreign people (Homer _Odyssey_ 3.72–4).","","","","","","","","","","","","","","","","","","","","","" 38 | "Unmoored","1","Cause a Hero and his Ship to move from a Palace to an adjacent non-coastal Sea Circle. Only play after a Hero has undergone his Encounter phase (2.0). Has no effect at Thebes.","1","","And straightway a Storm-wind snatched and bore them out to sea / Weeping, away from their native land (Homer _Odyssey_ 10.48–9)","","","","","","","","","","","","","","","","","","","","","" 39 | "Unseasonable Wind","1","Search Voyage Deck for the first Voyage Card of any later Season; swap with the top Card of Voyage Deck.","0","1","And for all the month the South wind blew unceasingly, nor did any other / Breeze arise save East and South (Homer _Odyssey_ 12.325–6).","","","","","","","","","","","","","","","","","","","","","" 40 | "Will of Zeus","1","Cancel any Theme or Hospitality Card OR + 4d6 to any one roll OR Captured Hero Escapes OR Shipwrekced Hero is Rescued OR Becalmed Hero automatically succeeds in wrestling and escaping Proteus.","2","1","And the Heroes at Troy were being killed, and the Will of Zeus was brought to pass ( _Cypria_ Fragment 1).","","","","","","","","","","","","","","","","","","","","","" 41 | "EOF","","","","","","","","","","","","","","","","","","","","","","","","","","" 42 | "","47","TOTALS:","48","21","","","","","","","","","","","","","","","","","","","","","","" 43 | "","","AVG COST PER CARD","1.021276596","","","","","","","","","","","","","","","","","","","","","","","" 44 | "SIMPLFIED DECK","","https://docs.google.com/spreadsheets/d/1EIoLXYuNfnalJk9DBcUKksvqxz_Yo7PvdoYHjs9DNWA/edit?usp=sharing","","","","","","","","","","","","","","","","","","","","","","","","" 45 | "OUTTAKES","","","","","","","","","","","","","","","","","","","","","","","","","","" 46 | "Aganos","1","A child is born to Paris and Helen. Play only in final Season, and only if Paris possesses a Helen. Aganos always remains attached to Paris. If Paris ends game with the real Helen, he gains 50 Fame if she is Willing, but loses 50 if Unwilling. ","6","","Pleisthenes (sc. was the son of Helen and Menelaus), with whom Aganos, the son born by her to Paris, also arrived to Cyprus (Scholion to Euripides _Andromache_ 898).","","","","","","","","","","","","","","","","","","","","","" 47 | "Threats","1","Add 3d6 to any Diplomacy attempt, but if unsuccessful Land Region becomes Hostile.","2","0","","","","","","","","","","","","","","","","","","","","","","" 48 | "Fabled Wealth","1","Examine all stacks of Gifts at the Palaces of any one Land Region.","1","1","Polybos... who lived at Thebes / In Egypt, where the most possessions reside in houses; / He gave Menelaos two silver bathtubs, / And two tripods, and ten talents of gold (Homer _Odyssey_ 4.126–9).","","","","","","","","","","","","","","","","","","","","","" -------------------------------------------------------------------------------- /raw_spreadsheet_data/hospitality.csv: -------------------------------------------------------------------------------- 1 | "Name","Count","Count","Num Gifts","Kind (Expert, Hostile, Mission)","Flavor text","Mission Reward","","","","","","","","","","","","","","","","","","","","" 2 | "Ambush","1","Upon entering the palace the host attacks you unexpectedly. You may either fight back or flee to adjacent Sea Circle.","0","Hostile","There are harbors there for hiding ships, / Entrances on either side. The Achaeans awaited him there, lying in ambush (Homer _Odyssey_ 4.846–7).","","","","","","","","","","","","","","","","","","","","","" 3 | "Supply Shortage","1","Your Host has been weakened by recent raids from hinterland tribes and his storerooms are low. You may immediately attempt to ally this palace at Rank -1, but may not use gifts just recieved. ","Rank - 1","Mission","Take me alive, and I will ransom myself. For in my home is / Bronze and gold and much-worked iron (Homer _Iliad_ 10.378–9).","","","","","","","","","","","","","","","","","","","","","" 4 | "Chief Herald","1","The King and Queen are swayed by your cause; this Palace immediately becomes your Ally. They bid their Chief Herald join your compay. Take Chief Herald from the Expert Pool, if available. ","Rank","Expert","The herald Peisenor, knowing wise counsels, / Placed a scepter in his hand (Homer _Odyssey_ 2.337–8).","","","","","","","","","","","","","","","","","","","","","" 5 | "Contest","1","Following a feast, the overweening sons of local nobles challenge your athletic prowess. If you accept roll three dice. Lose one hit point for each failure. If you get at least two successes, draw the first gold Gift you can find in the Gift Pool.","Rank","","Come, friends, let us ask the stranger if he knows / And has learned any contest (Homer _Odyssey_ 8.133–4).","","","","","","","","","","","","","","","","","","","","","" 6 | "Elite Merchant","1","You are welcomed at the Palace. When you return to the harbor, you find a ship trafficking in elite goods. Draw 10 Gifts from the Gift Pool, then you may swap any light gifts with other light gifts that you have. ","Rank","Expert","And there came Phoenicians, ship-famed men /... hauling countless fine trinkets in their dark ship (Homer _Odyssey_ 15.415–16).","","","","","","","","","","","","","","","","","","","","","" 7 | "Expert Helmsman","1","The King is moved by your plight and lends you his Expert Helmsman to assist in your voyage. Search the Expert pool for the Expert helmsman if available, otherwise draw one additional gift from Gift Pool.","Rank","Expert","Phrontis, son of Onestor, who surpassed the tribes of men in / ship-steering, whenever winds blew strong (Homer _Odyssey_ 3.282–3).","","","","","","","","","","","","","","","","","","","","","" 8 | "Favorable Omen","1","You feast well, exchanging news and stories with the Queen and her court. As you weigh anchor, you see a flight of birds on your right hand: draw an additional Theme Card next turn. ","Rank +1","","Then for him, so speaking, flew on the right hand a bird, / A hawk, Apollo’s swift messenger. (Homer _Odyssey_ 15.525–6).","","","","","","","","","","","","","","","","","","","","","" 9 | "Fugitive Diviner","1","Returning to your ship after a night of feasting, you meet a diviner fleeing his homeland for killing a man in a blood feud. Seach the Expert Pool for the Fugitive Diviner.","Rank","Expert","Near him came a man / From far away, fleeing from Argos having killed a man— / A seer (Homer _Odyssey_ 15.223–5). ","","","","","","","","","","","","","","","","","","","","","" 10 | "Gift to the Gods","1","The Queen's generosity is such that you are impelled to make a dedication at the palace shrine. Return one Gift to the Gift Pool.","Rank +1","","And a robe, whichever is most pleasing and great / In the palace, and most loved by you yourself, / Place it upon the knees of lovely-haired Athena (Homer _Iliad_ 6.271–3).","","","","","","","","","","","","","","","","","","","","","" 11 | "Great Harvest","1","The Palace enjoyed an excellent harvest in its hinterland territories, and the royal couple are generous with their parting Gifts.","Rank +1","","The West Wind, blowing, causes some to grow, others to ripen— / Pear upon pear comes mature, and apple on apple, / And again grapes on grapes, fig on fig (Homer _Odyssey_ 7.119–21).","","","","","","","","","","","","","","","","","","","","","" 12 | "Harbor Market","1","You are hosted in fine traditional style. Returning to the harbor, you find a merchant ship conducting a lively exchange. Draw six Gifts, You may exchange one or more of these for your own Gifts of equal or greater value (individually or as a lot).","Rank +1","","They got much profit in their hollow ship by trading. / But when indeed their hollow ship was laden for return... (Homer _Odyssey_ 15. 456–7).","","","","","","","","","","","","","","","","","","","","","" 13 | "Expert Healer","1","The palace is celebrating the king's recovery from long illness, thanks to the ministrations of a Healer sent by a brother monarch. When you leave, the king asks you to take the Healer on the first part of his journey home. Search the Expert Pool for the Expert Healer if availabe; otherwise draw one additional Gift.","Rank","Expert","For a healer-man is worth many others, / For cutting out arrows and applying gentle drugs (Homer _Iliad_ 11.514–15).","","","","","","","","","","","","","","","","","","","","","" 14 | "Hostage","1","Your Host has heard rumors of your efforts to help his enemies. Roll one die: if you succeed your Host gives you one Gift. If you fail the Palace attacks you. ","0","Hostile","From those possessions Father would give you boundless ransom, / If once he learns I am alive (Homer _Iliad_ 10.380–1).","","","","","","","","","","","","","","","","","","","","","" 15 | "Kinnaru's Gift","1","The royal couple share your love of music, and instruct their Chief Singer to see to your guest-gifts. Draw Gifts = Palace Rank, and search Gift Pool to take Lyre or Music Girls (if available). ","0","Divine","Him they found delighting his heart with a bright lyre— / Beautiful, elaborate, and on it was a silver crossbar (Homer _Iliad_ 9.186–7).","","","","","","","","","","","","","","","","","","","","","" 16 | "Kothar's Gift","1","This King controls a renowned bronze workshop in the Ingot God's sanctuary. He sees your need and takes you to his armoury: search Gift Pool for one Special Weapon or Armor (if available). ","Rank","Divine","Next in turn he donned the corselet round his chest / Which once Kinyras gave him as a friendship-gift (Homer _Iliad_ 11.19–20).","","","","","","","","","","","","","","","","","","","","","" 17 | "Lying in Wait","1","A gang of impoverished aristocratic youths besets your ship just around the last headland before the harbor. Treat as pirates with 6 hitpoints. If you win proceed to Palace and are compensated with a fine banquet (draw Gifts = Palace Rank + 1).","Rank +1","Hostile","There is a rocky island mid-sea /... /... And ship-sheltering harbors it has, / Entrances on either side. / There in ambush the Achaeans awaited him (Homer _Odyssey_ 4.844–7).","","","","","","","","","","","","","","","","","","","","","" 18 | "Master Archer","1","The King learns of your need and, after providing a solid repast, restores your Crew to full strength and bids his Master Archer join your expedition. Master Archer allows one preliminary 3 die attack in any Ship Melee or Sack Palace attempt, and hits on Wits or Might. ","Rank","Expert","At once he fit a bitter arrow to the string / And prayed to Apollo, wolf-born, famous for the bow (Homer _Iliad_ 4.118–20).","","","","","","","","","","","","","","","","","","","","","" 19 | "Master Navigator","1","The Queen is moved by your plight, and dispatches her Master Navigator to expedite your journey. Master Navigator can rotate a Voyage Card 45 degrees each turn.","Rank +1","Expert","And straightaway, when we had made all gear ready throughout the ship, / We sat, and wind and steersman made straight her course (Homer _Odyssey_ 12.151–2).","","","","","","","","","","","","","","","","","","","","","" 20 | "Micrologos","1","Just your luck: your Host is a miserly kill-joy who makes you sleep alone on the portico.","1","","Therefore do not rush and send him packing, nor gifts / Curtail for him in need like this; for you have many / Possessions laid up in your palace by the gods’ favor (Homer _Odyssey_ 11.339–40).","","","","","","","","","","","","","","","","","","","","","" 21 | "Nepenthe","1","This Palace, though welcoming and generous, has a strangely lethargic atmosphere. You may also search Gift Pool for the Nepenthe Gift. The Queen adds some of this same drug to your wine: recover all Hit Points and restore Crew to full strength, but skip your next turn.","Rank","","Then she put into the wine, of which they drank, a drug, / Pain-free, angerless, forgetfulness of every ill (Homer _Odyssey_ 4.220–1)","","","","","","","","","","","","","","","","","","","","","" 22 | "New Ship","1","Your royal Host offers you a new ship. You may restore Ship and Crew to 5 (discard Expert Shipwork if you have it, or play it now if in your hand). ","Rank","","Alkinoos stowed well the gifts beneath the benches, / Going himself throughout the ship (Homer _Odyssey_ 13.20–1).","","","","","","","","","","","","","","","","","","","","","" 23 | "Old Windbag","1","You are handsomely entertained, but yours is a garrulous older Host with many tales to tell. Forfeit next Voyage phase and remain at Palace with no further Encounter or Helen phase. Draw two extra theme cards. ","Rank x2","","Do not lead me past my ship, but leave me here, / That your old man not detain me unwilling in his house, / With his eager welcome (Homer _Odyssey_ 15.199–200).","","","","","","","","","","","","","","","","","","","","","" 24 | "Pirate Ally","1","After a night of feasting, you meet and strike a bargain with a salty captain. You may search Theme Deck and take one Pirates Card; discard one if necessary.","Rank","","(Zeus) impelled me with much-roving pirates / To go to Egypt—a long road, that I might perish (Homer _Odyssey_ 17.425–6).","","","","","","","","","","","","","","","","","","","","","" 25 | "Queen of Heaven","1","You arrive as a cosmopolitan crowd has gathered to celebrate the Great Goddess. Search Theme Deck for _Aphrodite_ or _Hera_ (if available). Discard one card if necessary.","Rank","Divine","But laughter-loving Aphrodite then arrived to Cyprus, / To Paphos; and there she has her precinct and perfumed altar (Homer _Odyssey_ 8.362–3).","","","","","","","","","","","","","","","","","","","","","" 26 | "Queen's Mercy","1","The Queen, moved by your trials and tribulations, bestows valuable items in your Ship to help rebuild your fortune.","Rank x2","","If for you she thinks kind thoughts within her heart, / Then there’s hope to see your friends and reach / Your high-roofed home and win back to your fatherland (Homer _Odyssey_ 7.75–6).","","","","","","","","","","","","","","","","","","","","","" 27 | "Rumors of Helen","1","During a feast with the King and local aristocrats, you meet a captain who has just returned from coasting through Hatti and Syria. You may examine one Helen/Phantom at a Palace in each of these regions.","Rank","","My name, however, meanwhile along the banks of the / Simois gathers a false report (Euripides _Helen_ 251–2).","","","","","","","","","","","","","","","","","","","","","" 28 | "Rumors of Helen","1","During a feast with the King and local aristocrats, you meet a captain who has just returned from coasting through Phoenicia and Philistia. You may examine one Helen/Phantom at a Palace in each of these regions.","Rank","","","","","","","","","","","","","","","","","","","","","","","" 29 | "Rumors of Helen","1","During a feast with the King and local aristocrats, you meet a captain who has just returned from coasting through Libya and Egypt. You may examine one Helen/Phantom at a Palace in each of these regions.","Rank","","My name, however, meanwhile along the banks of the / Simois gathers a false report (Euripides _Helen_ 251–2).","","","","","","","","","","","","","","","","","","","","","" 30 | "Sea God's Festival","1","Good timing! The Palace is celebrating their Sea God. You may search Theme Deck for any card that effects voyage. Discard one card if neccesary. ","Rank","Divine","And they, upon the ocean’s strand were making sacrifice, / Bulls all-black, to the dark-haired Earth-Shaker (Homer _Odyssey_ 3.5-6).","","","","","","","","","","","","","","","","","","","","","" 31 | "Singer","1","Your Host has engaged a famous travelling Singer for the feast. The Singer offers to join your companions and magnify your fame. Take the singer from the gift pool if availabe. Whenever the season changes while the Singer is with you, gain one fame. ","Rank +1","Expert","Men celebrate most that song / Which, to them listening, newest comes (Homer _Odyssey_ 1.351–2).","","","","","","","","","","","","","","","","","","","","","" 32 | "Steamy Asaminthos","1","The welcome here is so so warm that your bath is drawn by the King’s own daughter! If you exercise sufficient charm, you may leave behind more than your memory. You may discard a Hera or Aphrodite theme card to automatically ally this palace.","Rank +2","","Then beautiful Polykaste, youngest daughter of / Nestor son of Neleus, bathed Telemakhos (Homer _Odyssey_ 3.464–5) •And then to Telemakhos did well-girdled Polykaste, / Youngest daughter of Nestor son of Neleus, / Bear Persepolis, mingling through golden Aphrodite (Hesiod _Catalogue of Women_ fragment 221 M-W). ","","","","","","","","","","","","","","","","","","","","","" 33 | "Storm God Festival","1","You arrive during the Storm God’s great New Year festival. The royal couple lavish you with friendship Gifts. You may search the Theme Deck and take Zeus (if available).","Rank +1","Divine","Vow to all the gods that perfect hecatombs / You will sacrifice, if ever Zeus accomplish payback deeds (Homer _Odyssey_ 17.50–1).","","","","","","","","","","","","","","","","","","","","","" 34 | "Useful Contact","1","You enjoy a solid royal feast. In the morning, another voyager speaks to the assembled nobles, and you learn of conditions on the next stage of your journey. You may discard one or two Theme Cards and draw to replace OR examine one Helen at any Palace.","Rank","","Go to the assembly-place, so you may learn of the stranger, / Who just now reached the home of wise Alkinoos, / Driven upon the sea (Homer _Odyssey_ 8.12–14).","","","","","","","","","","","","","","","","","","","","","" 35 | "Warrior Poet","1","Your Host calls upon you to tell your tale before the assembled nobles. Roll dice times Palace Rank + 2. For each sucess draw one gift. If there are three or more successes gain one fame. ","Rank","","But on you is a comeliness of words, and noble mind inside you. / Your tale you told in skillful wise as when a singer sings (Homer _Odyssey_ 11.367–8).","","","","","","","","","","","","","","","","","","","","","" 36 | "Waylaid","1","On your way up to the Palace, and out of sight from your Crew, you are jumped by a gang of impoverished aristocratic ne'er-do-wells. Resolve as mandatory Trial Combat against an opponent with Hit Points = Palace Rank + 1, and inflict 1HP on might. ","0","Hostile","But let us get the jump, seizing him in the field outside the city, / Or in the road; and let us ourselves have his livelihood and possessions, / Sharing out equally among ourselves (Homer _Odyssey_ 16.383–5).","","","","","","","","","","","","","","","","","","","","","" 37 | "Wise Elder","1","You are handsomely entertained by an old monarch with much wisdom to impart. Draw two Theme cards: you may keep one or both, discarding other cards if necessary. ","Rank +2","","First of all for them there wove his counsel the old man, / Nestor, whose counsel had also seemed the best before (Homer _Iliad_ 7.324–5).","","","","","","","","","","","","","","","","","","","","","" 38 | "Wrath of Baal","1","You are royally entertained during a Feast of All Gods. But one ritual has gone amiss, and the Storm God's anger is rising. Move the nearest storm to this palace. ","Rank +1","Divine","Then indeed a storm-wind snatched him up / And upon the fishy sea it bore him gravely groaning, / Unto the farthest reach of land (Homer _Odyssey_ 4.515–17).","","","","","","","","","","","","","","","","","","","","","" 39 | "Friends in Egypt","1","The king bids you visit his friends in Egypt. Go to a palace other than this one in Egypt and undergo a friendly hospitality encounter, then discard this card and draw two extra gifts.","Rank","Mission","","Two Gifts. ","","","","","","","","","","","","","","","","","","","","" 40 | "Friends Philistia","1","The king bids you visit his friends in Philistia. Go to a palace other than this one in Philistia and undergo a friendly hospitality encounter, then discard this card and draw a gift.","Rank","Mission","","Two Gifts. ","","","","","","","","","","","","","","","","","","","","" 41 | "Friends Lukka","1","The king bids you visit his friends in Lukka. Go to a palace other than this one in Lukka and undergo a friendly hospitality encounter, then discard this card and draw a gift.","Rank","Mission","","One Gifts. ","","","","","","","","","","","","","","","","","","","","" 42 | "Friends Lybia","1","The king bids you visit his friends in Lybia. Go to a palace other than this one in Lybia and undergo a friendly hospitality encounter, then discard this card and draw two gifts.","Rank","Mission","","Two Gifts. ","","","","","","","","","","","","","","","","","","","","" 43 | "Friends Syria","1","The king bids you visit his friends in Syria. Go to a palace other than this one in Syria and undergo a friendly hospitality encounter, then discard this card and draw two gifts","Rank","Mission","","Two Gifts. ","","","","","","","","","","","","","","","","","","","","" 44 | "Friends Cyprus","2","The king bids you visit his friends in Cyprus. Go to a palace other than this one in Cyprus and undergo a friendly hospitality encounter, then discard this card and draw two gifts.","Rank","Mission","","One Gift. ","","","","","","","","","","","","","","","","","","","","" 45 | "Friends Phoenicia","1","The king bids you visit his friends in Phoenicia. Go to a palace other than this one in Phoenicia and undergo a friendly hospitality encounter, then discard this card and draw two gifts.","Rank","Mission","","Two Gifts. ","","","","","","","","","","","","","","","","","","","","" 46 | "Copper Delivery ","2","Give two copper ingots to any palace, discard this card and gain one fame.","0","Mission","","One Gift one fame.","","","","","","","","","","","","","","","","","","","","" 47 | "Royal marrige","1","Give away a Helen to a palace without one to automatically ally the palace.","Rank","Mission","","Automatically ally the palace.","","","","","","","","","","","","","","","","","","","","" 48 | "Clear the seas","1","Win three ship melee against pirates, gain one fame.","Rank","Mission","","One Fame. ","","","","","","","","","","","","","","","","","","","","" 49 | "Rival City","1","Sack a palace not in this region, draw three extra gifts.","Rank","Mission","","","","","","","","","","","","","","","","","","","","","","" 50 | "Map Maker","2","Whenever you visit a region, put a region visited token on this card in addition. When it has three discard it. And draw three gifts and gain one fame.","Rank","Mission","","","","","","","","","","","","","","","","","","","","","","" 51 | "","49","","","","","","","","","","","","","","","","","","","","","","","","","" -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "certifi" 5 | version = "2025.1.31" 6 | description = "Python package for providing Mozilla's CA Bundle." 7 | optional = false 8 | python-versions = ">=3.6" 9 | groups = ["main"] 10 | files = [ 11 | {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, 12 | {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, 13 | ] 14 | 15 | [[package]] 16 | name = "charset-normalizer" 17 | version = "3.4.1" 18 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 19 | optional = false 20 | python-versions = ">=3.7" 21 | groups = ["main"] 22 | files = [ 23 | {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, 24 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, 25 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, 26 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, 27 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, 28 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, 29 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, 30 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, 31 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, 32 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, 33 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, 34 | {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, 35 | {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, 36 | {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, 37 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, 38 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, 39 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, 40 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, 41 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, 42 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, 43 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, 44 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, 45 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, 46 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, 47 | {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, 48 | {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, 49 | {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, 50 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, 51 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, 52 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, 53 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, 54 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, 55 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, 56 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, 57 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, 58 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, 59 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, 60 | {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, 61 | {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, 62 | {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, 63 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, 64 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, 65 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, 66 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, 67 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, 68 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, 69 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, 70 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, 71 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, 72 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, 73 | {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, 74 | {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, 75 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, 76 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, 77 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, 78 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, 79 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, 80 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, 81 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, 82 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, 83 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, 84 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, 85 | {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, 86 | {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, 87 | {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, 88 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, 89 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, 90 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, 91 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, 92 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, 93 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, 94 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, 95 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, 96 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, 97 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, 98 | {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, 99 | {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, 100 | {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, 101 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, 102 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, 103 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, 104 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, 105 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, 106 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, 107 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, 108 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, 109 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, 110 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, 111 | {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, 112 | {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, 113 | {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, 114 | {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, 115 | ] 116 | 117 | [[package]] 118 | name = "idna" 119 | version = "3.10" 120 | description = "Internationalized Domain Names in Applications (IDNA)" 121 | optional = false 122 | python-versions = ">=3.6" 123 | groups = ["main"] 124 | files = [ 125 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 126 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 127 | ] 128 | 129 | [package.extras] 130 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 131 | 132 | [[package]] 133 | name = "pillow" 134 | version = "11.1.0" 135 | description = "Python Imaging Library (Fork)" 136 | optional = false 137 | python-versions = ">=3.9" 138 | groups = ["main"] 139 | files = [ 140 | {file = "pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8"}, 141 | {file = "pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192"}, 142 | {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2"}, 143 | {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26"}, 144 | {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07"}, 145 | {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482"}, 146 | {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e"}, 147 | {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269"}, 148 | {file = "pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49"}, 149 | {file = "pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a"}, 150 | {file = "pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65"}, 151 | {file = "pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457"}, 152 | {file = "pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35"}, 153 | {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2"}, 154 | {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070"}, 155 | {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6"}, 156 | {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1"}, 157 | {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2"}, 158 | {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96"}, 159 | {file = "pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f"}, 160 | {file = "pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"}, 161 | {file = "pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71"}, 162 | {file = "pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a"}, 163 | {file = "pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b"}, 164 | {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3"}, 165 | {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a"}, 166 | {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1"}, 167 | {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f"}, 168 | {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91"}, 169 | {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c"}, 170 | {file = "pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6"}, 171 | {file = "pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf"}, 172 | {file = "pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5"}, 173 | {file = "pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc"}, 174 | {file = "pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0"}, 175 | {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1"}, 176 | {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec"}, 177 | {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5"}, 178 | {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114"}, 179 | {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352"}, 180 | {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3"}, 181 | {file = "pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9"}, 182 | {file = "pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c"}, 183 | {file = "pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65"}, 184 | {file = "pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861"}, 185 | {file = "pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081"}, 186 | {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c"}, 187 | {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547"}, 188 | {file = "pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab"}, 189 | {file = "pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9"}, 190 | {file = "pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe"}, 191 | {file = "pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756"}, 192 | {file = "pillow-11.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6"}, 193 | {file = "pillow-11.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e"}, 194 | {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc"}, 195 | {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2"}, 196 | {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade"}, 197 | {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884"}, 198 | {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196"}, 199 | {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8"}, 200 | {file = "pillow-11.1.0-cp39-cp39-win32.whl", hash = "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5"}, 201 | {file = "pillow-11.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f"}, 202 | {file = "pillow-11.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a"}, 203 | {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90"}, 204 | {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb"}, 205 | {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442"}, 206 | {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83"}, 207 | {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f"}, 208 | {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73"}, 209 | {file = "pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0"}, 210 | {file = "pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20"}, 211 | ] 212 | 213 | [package.extras] 214 | docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] 215 | fpx = ["olefile"] 216 | mic = ["olefile"] 217 | tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] 218 | typing = ["typing-extensions ; python_version < \"3.10\""] 219 | xmp = ["defusedxml"] 220 | 221 | [[package]] 222 | name = "requests" 223 | version = "2.32.3" 224 | description = "Python HTTP for Humans." 225 | optional = false 226 | python-versions = ">=3.8" 227 | groups = ["main"] 228 | files = [ 229 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 230 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 231 | ] 232 | 233 | [package.dependencies] 234 | certifi = ">=2017.4.17" 235 | charset-normalizer = ">=2,<4" 236 | idna = ">=2.5,<4" 237 | urllib3 = ">=1.21.1,<3" 238 | 239 | [package.extras] 240 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 241 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 242 | 243 | [[package]] 244 | name = "typst" 245 | version = "0.13.2" 246 | description = "Python binding to typst" 247 | optional = false 248 | python-versions = ">=3.8" 249 | groups = ["main"] 250 | files = [ 251 | {file = "typst-0.13.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:1bd26a9f51fb2cf72a071034642c2cd0aec0236ed5f379e4a0c3aa07fd40ef90"}, 252 | {file = "typst-0.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81f201740cfe2e04dde3f69d7f08358b78be942c0e7c889abc6c6fe89c3a4f2c"}, 253 | {file = "typst-0.13.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7845dcb07e1d1f569810db64d5efb5b657598a25f73b03dccad1034d0684c6f0"}, 254 | {file = "typst-0.13.2-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f360cd431e41477653c150da18a62474b20b5538d5082c120283b60bf2c896f"}, 255 | {file = "typst-0.13.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4980a85f9d2b6e0fea267cf337c2a88e905dad10996feb1617bbffa68bc26555"}, 256 | {file = "typst-0.13.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfa3f33592a3d156cf039510046f495cc41130ee5613998d641d5141e82d81e6"}, 257 | {file = "typst-0.13.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:789138b066982abffdc8d360493499188d64d7ef59aea998dda606df9d452128"}, 258 | {file = "typst-0.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:be1ae603d07c2f406778a3c3b80484b69ed68a33c67bd56ac930af5ec7243497"}, 259 | {file = "typst-0.13.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6d3c5e9372fbfc4b8176fafb5cac35d29693e437b8e15f91cc00523ddd071569"}, 260 | {file = "typst-0.13.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:cd4d880e94b91028184cd167dda4e950264a8ea628841a48842d3e8453558a95"}, 261 | {file = "typst-0.13.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6abf9352540af30bcbb1687e4d00f0d0e7115db27db1b71b901df15abaf2086a"}, 262 | {file = "typst-0.13.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d558ee6fb449714955297b026cc2f567f44c991b6b7d32d8ba5262150f267603"}, 263 | {file = "typst-0.13.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b9e542d136741df846f15ed23e110475d4c82e1f0f29ab948cd3344bcc8181e"}, 264 | {file = "typst-0.13.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3939b7cc16abc0f753015c886f8173f3a0a289ebc828d886c0f2284415d20490"}, 265 | {file = "typst-0.13.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c2a78e01b897e3075b6bc01b0bf7738ae110550394d7be1ec51de178ccd5e8c"}, 266 | {file = "typst-0.13.2-cp38-abi3-win_amd64.whl", hash = "sha256:897829e5ddfad5ace94f3f378ce7c6d580b78402af92e66eed5b28cb98186b57"}, 267 | {file = "typst-0.13.2.tar.gz", hash = "sha256:ee84c54886bf1c1890dd064819d9d2e332e1ae1fff89a3615fae429b239305a4"}, 268 | ] 269 | 270 | [[package]] 271 | name = "urllib3" 272 | version = "2.3.0" 273 | description = "HTTP library with thread-safe connection pooling, file post, and more." 274 | optional = false 275 | python-versions = ">=3.9" 276 | groups = ["main"] 277 | files = [ 278 | {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, 279 | {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, 280 | ] 281 | 282 | [package.extras] 283 | brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] 284 | h2 = ["h2 (>=4,<5)"] 285 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 286 | zstd = ["zstandard (>=0.18.0)"] 287 | 288 | [metadata] 289 | lock-version = "2.1" 290 | python-versions = ">=3.13" 291 | content-hash = "686a648bd9f0a1aaecd707d78e321b840efe0c002d1e1ecf74dfd2b9210f75c8" 292 | --------------------------------------------------------------------------------