mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-26 05:44:46 -06:00
b76b2a98d0
The look of the next age — the modern equivalent of the ASCII->CP437 leap. Full Unicode is available now, but the whole stack (text frames, golden tests, the Watch's 1ch grid) assumes one glyph = one column, so the enabling piece is a WIDTH RULE, not the glyphs themselves. - textwidth.is_grid_safe: one code point, printable, East-Asian width not Wide/Fullwidth, no combining/format/control category. This is the one-glyph-one-column contract. Ambiguous-width glyphs are ACCEPTED on purpose — they ARE CP437 (the wall, the club-tree, the up-arrow forest) and render single-column on the Western-monospace metrics every surface uses; only genuinely double-width runes are barred. The loader enforces it on every map glyph; the player-name/free-text sanitizer enforces the same rule (the narrow ledger), so a wide name can't shear a frame. - Re-skin: water ~ -> ≋, inn -> ⌂, healer -> ✚, dungeon mouth -> ∩, and the other adventurer -> ☻ (CP437's own player glyph). The colour field the renderer has carried unused since v0.1 now has a second consumer. - Texture variants: grass and water vary by a deterministic per-coordinate hash, rendered identically in the Python frame builder and the Watch's JS. The two are kept in lockstep by shared hash constants + an agreement test that replays the JS arithmetic and asserts it equals the Python output for every variant over a grid — not a comment-coupled copy. - Watch glow-up: a Noto Sans Mono font stack and a UTC-hour day/night tint (the Vale darkens at dusk on the lobby TV). - The curated SAFE_PALETTE is enforced author-usable: a test asserts no palette glyph collides with the reserved player markers, so AUTHORING's generated appendix can't advertise a glyph the loader would reject. - Resume is identity-preserving: an existing character resumes by exact stored name without re-validating the width rule (which governs creation only) — resume must never lock anyone out. Tests 231 -> 283; width edges (CJK/emoji/combining/fullwidth), the Python<->JS lockstep, the palette/reserved guard, and resume-vs-create all pinned and revert-verified.
100 lines
3.5 KiB
Python
100 lines
3.5 KiB
Python
"""The one-glyph-one-column contract for everything drawn on the grid.
|
|
|
|
Every surface Understone paints — the bordered text frames, the golden frames
|
|
the screen tests pin, and the Watch's CSS ``1ch``-per-cell map — assumes each
|
|
map glyph occupies *exactly one* terminal column. A glyph that renders two
|
|
columns (a CJK ideograph, an emoji) shoves the row right and tears the
|
|
box-drawing border; a zero-width combining mark stacks onto its neighbour and
|
|
desynchronises the column count the other way. :func:`is_grid_safe` is the
|
|
single predicate that admits a character to the grid, and :data:`SAFE_PALETTE`
|
|
is the curated set of glyphs known to satisfy it with period CP437 flavour.
|
|
|
|
THE WESTERN-MONOSPACE ASSUMPTION. Width here is judged for the Western
|
|
monospace metrics every Understone surface actually uses — the pinned Watch
|
|
font stack and the monospace of a chat client's code block. Under those
|
|
metrics the East-Asian-Width *Ambiguous* class renders single-column, and
|
|
Ambiguous is the CP437 heartland: ``█ ♣ ↑ ∩ ≈ ★`` are all EAW=A. So the rule
|
|
bars only the genuinely double-width classes — Wide (``W``) and Fullwidth
|
|
(``F``) — and admits Ambiguous, Narrow, Neutral, and Halfwidth. The trade is
|
|
deliberate: on a CJK-width terminal an Ambiguous glyph would take two columns,
|
|
but Understone's surfaces are not those terminals.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unicodedata
|
|
|
|
# East-Asian-Width classes that render two columns under Western monospace and
|
|
# would therefore tear a frame; everything else (Na/N/H/A) renders one column.
|
|
_DOUBLE_WIDTH_EAW = frozenset({"W", "F"})
|
|
|
|
# Unicode general categories that carry no column of their own — combining
|
|
# marks (Mn/Mc/Me) stack onto a neighbour, format/control codes (Cf/Cc) are
|
|
# invisible — so a single such code point is not a paintable cell.
|
|
_ZERO_WIDTH_CATEGORIES = frozenset({"Mn", "Mc", "Me", "Cf", "Cc"})
|
|
|
|
|
|
def is_grid_safe(ch: str) -> bool:
|
|
"""Return whether *ch* may occupy a single grid cell.
|
|
|
|
A grid-safe character is exactly one code point, is printable, is not an
|
|
East-Asian Wide or Fullwidth glyph (the only classes that render two
|
|
columns under the Western monospace metrics our surfaces use — see the
|
|
module docstring), and is not a combining mark or format/control code (a
|
|
zero-width code point that would desynchronise the column count).
|
|
"""
|
|
if len(ch) != 1:
|
|
return False
|
|
if not ch.isprintable():
|
|
return False
|
|
if unicodedata.east_asian_width(ch) in _DOUBLE_WIDTH_EAW:
|
|
return False
|
|
return unicodedata.category(ch) not in _ZERO_WIDTH_CATEGORIES
|
|
|
|
|
|
# A curated set of single-column glyphs with BBS / CP437 character, grouped by
|
|
# the role an author is likely to want them for. Every entry is grid-safe AND
|
|
# free of the loader's reserved markers (two tests assert both), so a pack
|
|
# author can pull any of these for terrain, structures, or actors without
|
|
# risking a torn frame or colliding with the '@'/'☻' player markers. The black
|
|
# smiling face (☻) is the other-player marker and so is NOT here; its white
|
|
# twin (☺) is a free being glyph. The grouping is documentation; the set is
|
|
# what callers iterate.
|
|
SAFE_PALETTE: tuple[str, ...] = (
|
|
# terrain
|
|
"≋",
|
|
"≈",
|
|
"░",
|
|
"▒",
|
|
"▓",
|
|
"♣",
|
|
"↑",
|
|
"▲",
|
|
".",
|
|
",",
|
|
"'",
|
|
'"',
|
|
"=",
|
|
"~",
|
|
"§",
|
|
"ø",
|
|
"¤",
|
|
"Ω",
|
|
# structures
|
|
"⌂",
|
|
"✚",
|
|
"∩",
|
|
"†",
|
|
"‡",
|
|
"$",
|
|
"◊",
|
|
"☖",
|
|
# beings
|
|
"☺",
|
|
"¶",
|
|
# misc
|
|
"•",
|
|
"⁂",
|
|
"★",
|
|
)
|