Skins
Representation leaves game logic. An app's board is a grid of role integers (0 = void); a skin is the one dictionary that says what each role wears; faces just apply it.
The law
- Apps emit role ids. No color names, chars, or emoji tables in
mrlyapps or in web views - ever.
- A skin is data: role -> visual. Curated per-app dictionaries live in
mrlyui::skin::<app> presets, keyed by the app's skin setting.
- The state carries the resolved dictionary next to the ids, so the web
grid, the canvas, the CLI, and PNG all read the same one.
The shape
mrlyui::skin defines the closed enum:
Visual { bg, motif, face }- a cell background, an optional motif
pattern (bg doubles as its color), an optional face on top.
Face::Glyph(text)- baked from the bitmap font.Face::Emoji(value)- a unicode string; DOM-only until the emoji
atlas exists (FACE.md), so canvas surfaces skip it.
Face::Sprite(rows)- a small bitmap, e.g. chess pieces.
On the wire, state gains two keys: ids (rows of role integers) and skin (an array indexed by role; each entry { bg?, motif?, face?: { as, value | rows } }).
Skin::tileset(k, ink) builds the canvas TileSet from the same dictionary: solid or motif base, glyph faces baked in ink, emoji faces skipped. Web views use src/skin.tsx (visual(), face()).
Why
Same seed, same verbs, N skins: identical id grids, N renders. One replayed episode times N skins is N perfectly labeled samples with identical semantics - the gym's augmentation story lives in this layer.