diff --git a/.gitignore b/.gitignore index 367d4ab..2f958ad 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ art/generated/creature-previews/ art/generated/sprites/*-raw*.png art/generated/sprites/anim/*-raw*.png art/generated/sprites/scripts/__pycache__/ +art/generated/portraits/*-raw*.png +art/generated/portraits/scripts/__pycache__/ diff --git a/art/generated/portraits/PROMPTS.md b/art/generated/portraits/PROMPTS.md new file mode 100644 index 0000000..733621e --- /dev/null +++ b/art/generated/portraits/PROMPTS.md @@ -0,0 +1,62 @@ +# Bestiary portraits (fal.ai) + +Generated on 2026-11-07/08: one portrait per creature kind (the 30 of +`../sprites/manifest.json` plus the imp, `EK::Worker`) for the bestiary in the high-res style. +Bust (head and shoulders; head and front body for beasts, the machine for the siege engines), +three-quarter view, torch light from one side, dark dungeon wall behind. + +- `-raw.png`: the model output, 1024x1024 (local only, ignored by git). +- `-128.png`: the processed 128x128 pixel-art portrait (48 colours, opaque). +- `../../../assets/portraits/portraits.png`: all 31 in one atlas, 8 columns x 4 rows of 128x128, + in the order of `../../../assets/portraits/order.txt` (`EK` names; mirrored by `ORDER` in + `src/portraits.rs`, a test checks both agree). + +## Generation + +- Model: `fal-ai/nano-banana-pro/edit` (POST https://fal.run/fal-ai/nano-banana-pro/edit) +- `aspect_ratio: 1:1`, `resolution: 1K`, `num_images: 1`, `output_format: png` +- Reference image (`image_urls`, data URI): the front and front three-quarter view of the + creature's idle sheet `../sprites/-64.png` (the imp: `assets/sprites/imp-64.png`), + scaled 6x nearest onto a flat dark grey (768x384). +- 31 images, no retries (every one showed the right character, no text). About $4.65. +- Scripts in `scripts/`: `portraits.py` (descriptions taken from + `../sprites/scripts/creatures.py` plus the imp, framing per kind, prompt template), `gen.py` + (requests, 4 at a time; reads the key from `~/.config/dungeoncraft/fal-key`), `process.py`. +- The first two (knight, spider) were sent before "(the torch itself outside the picture)" was + added; they show a torch. + +### Prompt template + +`{thing}` is `character`, `creature` (beasts: spider, bat, owl, wyrm, beetle, cavebear, +gargoyle) or `siege engine` (ram, catapult); `{framing}` is `a head-and-shoulders bust +portrait`, `a close-up of its head and the front of its body (bust portrait)` or `a close-up of +the front part of the machine with its crew-less wooden frame and iron parts`. + +```text +The attached image shows a small pixel art game sprite of the "{name}" (left: front view, right: three-quarter view). {desc} + +Paint a PORTRAIT of exactly this {thing} for the illustrated monster compendium of a dark-fantasy dungeon strategy game: {framing}, three-quarter view turned slightly to the side, looking at the viewer, filling most of the square picture. It must clearly be the same {thing} as in the sprite: the same colours, clothing, armour, markings, gear and weapon (show the weapon or tool where it fits in the frame), but drawn with much more detail and slightly more natural, characterful proportions. + +Style: hand-painted pixel art, like a detailed 128x128 pixel portrait scaled up with hard square pixels, a limited colour palette, clean dark outlines, no anti-aliasing blur, no smooth gradients, no photo realism. + +Lighting and background: a little dramatic - a warm torch light from one side (the torch itself outside the picture) and a deep shadow on the other side, a rim light on the silhouette. Dark atmospheric dungeon background: rough stone wall in darkness, a faint glow and drifting dust or mist, darkening to near black at the edges. + +Exactly one {thing}, no text, no letters, no name, no labels, no frame, no border, no signature, no UI. +``` + +## Notes per creature + +All usable as generated. Weaker in the "close-up" sense: the ram and the catapult show the +whole machine rather than a close-up; the giant is a plain bald man with a club (as the sprite). +The cave dwarf in the bestiary uses the dwarf miner's portrait (as it uses its sprite). + +## Processing + +```bash +/tmp/sprvenv/bin/python -I scripts/gen.py # all, or: gen.py ... +/tmp/sprvenv/bin/python -I scripts/process.py +``` + +`process.py`: whole picture (optional per-id crop fractions in `CROP`), box downscale to +128x128, sharpness 1.6, quantize to 48 colours (max coverage, k-means 4, no dithering), RGB; +writes `-128.png`, the atlas and `order.txt`. diff --git a/art/generated/portraits/archer-128.png b/art/generated/portraits/archer-128.png new file mode 100644 index 0000000..dda81f3 Binary files /dev/null and b/art/generated/portraits/archer-128.png differ diff --git a/art/generated/portraits/bat-128.png b/art/generated/portraits/bat-128.png new file mode 100644 index 0000000..0c97bdb Binary files /dev/null and b/art/generated/portraits/bat-128.png differ diff --git a/art/generated/portraits/battlemage-128.png b/art/generated/portraits/battlemage-128.png new file mode 100644 index 0000000..138ce40 Binary files /dev/null and b/art/generated/portraits/battlemage-128.png differ diff --git a/art/generated/portraits/beetle-128.png b/art/generated/portraits/beetle-128.png new file mode 100644 index 0000000..836285e Binary files /dev/null and b/art/generated/portraits/beetle-128.png differ diff --git a/art/generated/portraits/catapult-128.png b/art/generated/portraits/catapult-128.png new file mode 100644 index 0000000..5685222 Binary files /dev/null and b/art/generated/portraits/catapult-128.png differ diff --git a/art/generated/portraits/cavebear-128.png b/art/generated/portraits/cavebear-128.png new file mode 100644 index 0000000..bac0364 Binary files /dev/null and b/art/generated/portraits/cavebear-128.png differ diff --git a/art/generated/portraits/chainbreaker-128.png b/art/generated/portraits/chainbreaker-128.png new file mode 100644 index 0000000..ad09aa5 Binary files /dev/null and b/art/generated/portraits/chainbreaker-128.png differ diff --git a/art/generated/portraits/cogling-128.png b/art/generated/portraits/cogling-128.png new file mode 100644 index 0000000..3e8ef71 Binary files /dev/null and b/art/generated/portraits/cogling-128.png differ diff --git a/art/generated/portraits/dwarf-128.png b/art/generated/portraits/dwarf-128.png new file mode 100644 index 0000000..ea3d6bd Binary files /dev/null and b/art/generated/portraits/dwarf-128.png differ diff --git a/art/generated/portraits/fairy-128.png b/art/generated/portraits/fairy-128.png new file mode 100644 index 0000000..ddcded9 Binary files /dev/null and b/art/generated/portraits/fairy-128.png differ diff --git a/art/generated/portraits/gargoyle-128.png b/art/generated/portraits/gargoyle-128.png new file mode 100644 index 0000000..d4ad635 Binary files /dev/null and b/art/generated/portraits/gargoyle-128.png differ diff --git a/art/generated/portraits/giant-128.png b/art/generated/portraits/giant-128.png new file mode 100644 index 0000000..83f4a6c Binary files /dev/null and b/art/generated/portraits/giant-128.png differ diff --git a/art/generated/portraits/imp-128.png b/art/generated/portraits/imp-128.png new file mode 100644 index 0000000..748d456 Binary files /dev/null and b/art/generated/portraits/imp-128.png differ diff --git a/art/generated/portraits/inquisitor-128.png b/art/generated/portraits/inquisitor-128.png new file mode 100644 index 0000000..12aeb19 Binary files /dev/null and b/art/generated/portraits/inquisitor-128.png differ diff --git a/art/generated/portraits/jailer-128.png b/art/generated/portraits/jailer-128.png new file mode 100644 index 0000000..2e0cd3a Binary files /dev/null and b/art/generated/portraits/jailer-128.png differ diff --git a/art/generated/portraits/knight-128.png b/art/generated/portraits/knight-128.png new file mode 100644 index 0000000..5df8078 Binary files /dev/null and b/art/generated/portraits/knight-128.png differ diff --git a/art/generated/portraits/kobold-128.png b/art/generated/portraits/kobold-128.png new file mode 100644 index 0000000..3745afb Binary files /dev/null and b/art/generated/portraits/kobold-128.png differ diff --git a/art/generated/portraits/mage-128.png b/art/generated/portraits/mage-128.png new file mode 100644 index 0000000..4d2db11 Binary files /dev/null and b/art/generated/portraits/mage-128.png differ diff --git a/art/generated/portraits/monk-128.png b/art/generated/portraits/monk-128.png new file mode 100644 index 0000000..6269ad5 Binary files /dev/null and b/art/generated/portraits/monk-128.png differ diff --git a/art/generated/portraits/ogre-128.png b/art/generated/portraits/ogre-128.png new file mode 100644 index 0000000..1e64918 Binary files /dev/null and b/art/generated/portraits/ogre-128.png differ diff --git a/art/generated/portraits/owl-128.png b/art/generated/portraits/owl-128.png new file mode 100644 index 0000000..928b25c Binary files /dev/null and b/art/generated/portraits/owl-128.png differ diff --git a/art/generated/portraits/paladin-128.png b/art/generated/portraits/paladin-128.png new file mode 100644 index 0000000..4087288 Binary files /dev/null and b/art/generated/portraits/paladin-128.png differ diff --git a/art/generated/portraits/priest-128.png b/art/generated/portraits/priest-128.png new file mode 100644 index 0000000..67006b7 Binary files /dev/null and b/art/generated/portraits/priest-128.png differ diff --git a/art/generated/portraits/priestess-128.png b/art/generated/portraits/priestess-128.png new file mode 100644 index 0000000..818901b Binary files /dev/null and b/art/generated/portraits/priestess-128.png differ diff --git a/art/generated/portraits/ram-128.png b/art/generated/portraits/ram-128.png new file mode 100644 index 0000000..c2a9e0e Binary files /dev/null and b/art/generated/portraits/ram-128.png differ diff --git a/art/generated/portraits/ranger-128.png b/art/generated/portraits/ranger-128.png new file mode 100644 index 0000000..53b0791 Binary files /dev/null and b/art/generated/portraits/ranger-128.png differ diff --git a/art/generated/portraits/revenant-128.png b/art/generated/portraits/revenant-128.png new file mode 100644 index 0000000..3f841df Binary files /dev/null and b/art/generated/portraits/revenant-128.png differ diff --git a/art/generated/portraits/scripts/gen.py b/art/generated/portraits/scripts/gen.py new file mode 100644 index 0000000..916c80b --- /dev/null +++ b/art/generated/portraits/scripts/gen.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# Generate bestiary portraits with fal-ai/nano-banana-pro/edit; the reference image is the +# front and front three-quarter view of the creature's 64 px idle sheet, scaled up 6x. +import base64, io, json, os, sys, urllib.request, concurrent.futures as cf +from PIL import Image + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from portraits import CREATURES, prompt_for, sheet + +OUT = "/home/zink/Projects/dungeoncraft/art/generated/portraits" +KEY = open(os.path.expanduser("~/.config/dungeoncraft/fal-key")).read().strip() +EXTRA = os.environ.get("EXTRA", "") + + +def ref_uri(cid): + im = Image.open(sheet(cid)).convert("RGBA") + h = im.height + two = im.crop((0, 0, 2 * h, h)).resize((12 * h, 6 * h), Image.NEAREST) + bg = Image.new("RGBA", two.size, (40, 36, 44, 255)) + bg.alpha_composite(two) + buf = io.BytesIO() + bg.convert("RGB").save(buf, "PNG") + return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode() + + +def gen(cid, suffix=""): + body = {"prompt": prompt_for(cid, EXTRA), "image_urls": [ref_uri(cid)], "aspect_ratio": "1:1", + "resolution": "1K", "num_images": 1, "output_format": "png"} + req = urllib.request.Request("https://fal.run/fal-ai/nano-banana-pro/edit", data=json.dumps(body).encode(), + headers={"Authorization": "Key " + KEY, "Content-Type": "application/json"}) + try: + with urllib.request.urlopen(req, timeout=600) as r: + res = json.load(r) + with urllib.request.urlopen(res["images"][0]["url"], timeout=300) as r: + data = r.read() + path = f"{OUT}/{cid}-raw{suffix}.png" + open(path, "wb").write(data) + return cid, "ok " + path + except Exception as e: + msg = str(e) + if hasattr(e, "read"): + try: + msg += " " + e.read().decode()[:300] + except Exception: + pass + return cid, "ERROR " + msg + + +if __name__ == "__main__": + ids = sys.argv[1:] or [c["id"] for c in CREATURES] + suffix = os.environ.get("SUFFIX", "") + with cf.ThreadPoolExecutor(4) as ex: + for cid, st in ex.map(lambda i: gen(i, suffix), ids): + print(cid, st, flush=True) diff --git a/art/generated/portraits/scripts/portraits.py b/art/generated/portraits/scripts/portraits.py new file mode 100644 index 0000000..4319ad0 --- /dev/null +++ b/art/generated/portraits/scripts/portraits.py @@ -0,0 +1,43 @@ +# Portrait prompts for the bestiary (high-res style). Descriptions come from the sprite pipeline. +import importlib.util, os + +ROOT = "/home/zink/Projects/dungeoncraft" +_spec = importlib.util.spec_from_file_location("creatures", f"{ROOT}/art/generated/sprites/scripts/creatures.py") +_cr = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_cr) + +IMP = dict(id="imp", kind="Worker", name="Pit Imp", + desc="A small goblin-like worker demon: big head, oversized pointed ears and two small black horns, orange skin, glowing yellow eyes, a wide mischievous grin with tiny fangs, a ragged dark-red tunic with a rope belt, carrying a small iron pickaxe with a wooden handle over its right shoulder.") +CREATURES = [IMP] + [dict(id=c["id"], kind=c["kind"], name=c["name"], desc=c["desc"]) for c in _cr.CREATURES] +BY_ID = {c["id"]: c for c in CREATURES} +SIEGE = {"ram", "catapult"} +BEAST = {"spider", "bat", "owl", "wyrm", "beetle", "cavebear", "gargoyle"} + + +def sheet(cid): + if cid == "imp": + return f"{ROOT}/assets/sprites/imp-64.png" + return f"{ROOT}/art/generated/sprites/{cid}-64.png" + + +TEMPLATE = """The attached image shows a small pixel art game sprite of the "{name}" (left: front view, right: three-quarter view). {desc} + +Paint a PORTRAIT of exactly this {thing} for the illustrated monster compendium of a dark-fantasy dungeon strategy game: {framing}, three-quarter view turned slightly to the side, looking at the viewer, filling most of the square picture. It must clearly be the same {thing} as in the sprite: the same colours, clothing, armour, markings, gear and weapon (show the weapon or tool where it fits in the frame), but drawn with much more detail and slightly more natural, characterful proportions. + +Style: hand-painted pixel art, like a detailed 128x128 pixel portrait scaled up with hard square pixels, a limited colour palette, clean dark outlines, no anti-aliasing blur, no smooth gradients, no photo realism. + +Lighting and background: a little dramatic - a warm torch light from one side (the torch itself outside the picture) and a deep shadow on the other side, a rim light on the silhouette. Dark atmospheric dungeon background: rough stone wall in darkness, a faint glow and drifting dust or mist, darkening to near black at the edges. + +Exactly one {thing}, no text, no letters, no name, no labels, no frame, no border, no signature, no UI.""" + + +def prompt_for(cid, extra=""): + c = BY_ID[cid] + if cid in SIEGE: + thing, framing = "siege engine", "a close-up of the front part of the machine with its crew-less wooden frame and iron parts" + elif cid in BEAST: + thing, framing = "creature", "a close-up of its head and the front of its body (bust portrait)" + else: + thing, framing = "character", "a head-and-shoulders bust portrait" + p = TEMPLATE.format(name=c["name"], desc=c["desc"], thing=thing, framing=framing) + return p + ("\n\n" + extra if extra else "") diff --git a/art/generated/portraits/scripts/process.py b/art/generated/portraits/scripts/process.py new file mode 100644 index 0000000..98654d9 --- /dev/null +++ b/art/generated/portraits/scripts/process.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# Raw portraits (1024x1024) -> 128x128 pixel-art portraits (box downscale, 48 colours, no +# dithering, opaque) and the atlas assets/portraits/portraits.png (8 columns) + order.txt. +import os, sys +from PIL import Image, ImageEnhance + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from portraits import CREATURES + +ROOT = "/home/zink/Projects/dungeoncraft" +D = f"{ROOT}/art/generated/portraits" +OUT = f"{ROOT}/assets/portraits" +SIZE, COLORS, COLS = 128, 48, 8 +CROP = {} # id -> (l, t, r, b) fractions, default the whole picture + +os.makedirs(OUT, exist_ok=True) +rows = (len(CREATURES) + COLS - 1) // COLS +atlas = Image.new("RGB", (COLS * SIZE, rows * SIZE), (0, 0, 0)) +order = [] +for i, c in enumerate(CREATURES): + im = Image.open(f"{D}/{c['id']}-raw.png").convert("RGB") + l, t, r, b = CROP.get(c["id"], (0, 0, 1, 1)) + w, h = im.size + im = im.crop((round(l * w), round(t * h), round(r * w), round(b * h))) + im = im.resize((SIZE, SIZE), Image.BOX) + im = ImageEnhance.Sharpness(im).enhance(1.6) + im = im.quantize(colors=COLORS, method=Image.Quantize.MAXCOVERAGE, kmeans=4, dither=Image.Dither.NONE).convert("RGB") + im.save(f"{D}/{c['id']}-{SIZE}.png", optimize=True) + atlas.paste(im, ((i % COLS) * SIZE, (i // COLS) * SIZE)) + order.append(c["kind"]) +atlas.save(f"{OUT}/portraits.png", optimize=True) +open(f"{OUT}/order.txt", "w").write("\n".join(order) + "\n") +print(atlas.size, os.path.getsize(f"{OUT}/portraits.png"), "bytes") diff --git a/art/generated/portraits/spider-128.png b/art/generated/portraits/spider-128.png new file mode 100644 index 0000000..c3ec876 Binary files /dev/null and b/art/generated/portraits/spider-128.png differ diff --git a/art/generated/portraits/thief-128.png b/art/generated/portraits/thief-128.png new file mode 100644 index 0000000..aa4c18c Binary files /dev/null and b/art/generated/portraits/thief-128.png differ diff --git a/art/generated/portraits/trader-128.png b/art/generated/portraits/trader-128.png new file mode 100644 index 0000000..fa00097 Binary files /dev/null and b/art/generated/portraits/trader-128.png differ diff --git a/art/generated/portraits/wyrm-128.png b/art/generated/portraits/wyrm-128.png new file mode 100644 index 0000000..dcc9338 Binary files /dev/null and b/art/generated/portraits/wyrm-128.png differ diff --git a/assets/portraits/order.txt b/assets/portraits/order.txt new file mode 100644 index 0000000..3fc68f2 --- /dev/null +++ b/assets/portraits/order.txt @@ -0,0 +1,31 @@ +Worker +Kobold +Spider +Mage +Ogre +Knight +Archer +Priest +Dwarf +Paladin +Jailer +Priestess +Chainbreaker +Bat +Gargoyle +Owl +Wyrm +Cogling +Revenant +Giant +Fairy +BattleMage +Ram +Catapult +CaveBear +Beetle +Trader +Thief +Monk +Ranger +Inquisitor diff --git a/assets/portraits/portraits.png b/assets/portraits/portraits.png new file mode 100644 index 0000000..1abe135 Binary files /dev/null and b/assets/portraits/portraits.png differ diff --git a/src/main.rs b/src/main.rs index 18ab1df..f9d6206 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,6 +28,7 @@ mod menu; mod netplay; mod personality; mod platform; +mod portraits; mod orders; mod particles; mod props; diff --git a/src/menu/bestiary.rs b/src/menu/bestiary.rs index e5962e6..1596872 100644 --- a/src/menu/bestiary.rs +++ b/src/menu/bestiary.rs @@ -174,7 +174,19 @@ impl Menu { _ => level, }; match entry { - Entry::Unit(kind, _) => draw_sprite(gfx, kind, show_level, x + k, y + k, pic), + Entry::Unit(kind, _) => { + // the high-res style shows the creature's portrait filling the box + let box_size = pic + 2.0 * k; + match crate::portraits::rect(kind).filter(|_| crate::gfx::high_res_ui()) { + Some(src) => { + let m = 2.0; + draw_texture_ex(&crate::portraits::atlas(), x + m, y + m, WHITE, DrawTextureParams { source: Some(src), dest_size: Some(vec2(box_size - 2.0 * m, box_size - 2.0 * m)), ..Default::default() }); + draw_rectangle_lines(x + m, y + m, box_size - 2.0 * m, box_size - 2.0 * m, 2.0, Color::new(0.0, 0.0, 0.0, 0.8)); + draw_rectangle_lines(x, y, box_size, box_size, 2.0, FRAME); + } + None => draw_sprite(gfx, kind, show_level, x + k, y + k, pic), + } + } Entry::Rank(l) => draw_sprite(gfx, EK::Ogre, l, x + k, y + k, pic), Entry::Building(i) => { let icon = tool_info(TOOLS[i]).icon; @@ -321,3 +333,15 @@ fn draw_sprite(gfx: &Gfx, kind: EK, level: u32, x: f32, y: f32, size: f32) { let src = Gfx::sprite_cell(kind, false, level); draw_texture_ex(&gfx.sprites, x, y, WHITE, DrawTextureParams { source: Some(src), dest_size: Some(vec2(size, size)), ..Default::default() }); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_bestiary_unit_has_a_portrait() { + for &k in CREATURES.iter().chain(HUMANS.iter()) { + assert!(crate::portraits::rect(k).is_some(), "no portrait for {:?}", k); + } + } +} diff --git a/src/portraits.rs b/src/portraits.rs new file mode 100644 index 0000000..0c8eabe --- /dev/null +++ b/src/portraits.rs @@ -0,0 +1,57 @@ +// Bestiary portraits for the high-res style: one 128x128 pixel-art bust per creature kind in one +// atlas (8 columns), generated from the creatures' sprites (see art/generated/portraits/). + +use crate::game::EK; +use macroquad::prelude::*; + +const PNG: &[u8] = include_bytes!("../assets/portraits/portraits.png"); +pub const CELL: f32 = 128.0; +const COLS: usize = 8; + +// the atlas order, the same as assets/portraits/order.txt +const ORDER: [EK; 31] = [ + EK::Worker, EK::Kobold, EK::Spider, EK::Mage, EK::Ogre, EK::Knight, EK::Archer, EK::Priest, + EK::Dwarf, EK::Paladin, EK::Jailer, EK::Priestess, EK::Chainbreaker, EK::Bat, EK::Gargoyle, EK::Owl, + EK::Wyrm, EK::Cogling, EK::Revenant, EK::Giant, EK::Fairy, EK::BattleMage, EK::Ram, EK::Catapult, + EK::CaveBear, EK::Beetle, EK::Trader, EK::Thief, EK::Monk, EK::Ranger, EK::Inquisitor, +]; + +thread_local! { + static ATLAS: Texture2D = { + let t = Texture2D::from_file_with_format(PNG, Some(ImageFormat::Png)); + t.set_filter(FilterMode::Nearest); + t + }; +} + +// the atlas texture (loaded on first use) +pub fn atlas() -> Texture2D { + ATLAS.with(|t| t.clone()) +} + +// the source rect of a kind's portrait in the atlas, None for kinds without one +pub fn rect(kind: EK) -> Option { + let i = ORDER.iter().position(|&k| k == kind)?; + Some(Rect::new((i % COLS) as f32 * CELL, (i / COLS) as f32 * CELL, CELL, CELL)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn atlas_decodes_with_room_for_every_portrait() { + let img = Image::from_file_with_format(PNG, Some(ImageFormat::Png)).expect("atlas decodes"); + for &k in ORDER.iter() { + let r = rect(k).unwrap(); + assert!(r.x + r.w <= img.width as f32 && r.y + r.h <= img.height as f32, "{:?} outside the atlas", k); + } + } + + #[test] + fn order_matches_list() { + let list: Vec<&str> = include_str!("../assets/portraits/order.txt").lines().collect(); + let ours: Vec = ORDER.iter().map(|k| format!("{:?}", k)).collect(); + assert_eq!(list, ours); + } +}