mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -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.
82 lines
3.3 KiB
Python
82 lines
3.3 KiB
Python
"""The one-glyph-one-column grid contract (understone.engine.textwidth).
|
||
|
||
Pins the accept/reject boundary of :func:`is_grid_safe` and proves every
|
||
:data:`SAFE_PALETTE` entry clears it. The acceptances include the
|
||
East-Asian-Width *Ambiguous* CP437 glyphs the game leans on (``█ ♣ ↑ ∩ ≈ ★``),
|
||
which render single-column under the Western monospace our surfaces use; the
|
||
rejections are the genuinely double-width and zero-width classes that tear a
|
||
frame.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import unicodedata
|
||
|
||
import pytest
|
||
|
||
from understone.engine.textwidth import SAFE_PALETTE, is_grid_safe
|
||
from understone.world.loader import RESERVED_GLYPHS
|
||
|
||
# Single-column glyphs that must be admitted: plain ASCII, a Latin accent that
|
||
# is one composed code point, and the Ambiguous-width CP437 set the re-skin uses.
|
||
_ACCEPTED = ["a", "Z", "ö", "☻", "≋", "█", "∩", "★", "♣", "↑", ".", "$", " "]
|
||
|
||
# Must be rejected, with the reason each one trips the gate.
|
||
_REJECTED = {
|
||
"龍": "wide CJK ideograph (EAW=W) — two columns",
|
||
"🌲": "emoji (EAW=W) — two columns",
|
||
"A": "fullwidth Latin A (EAW=F) — two columns",
|
||
"é": "decomposed e + combining acute — two code points",
|
||
"́": "a lone combining acute — zero width",
|
||
"👨👩": "ZWJ sequence — multiple code points",
|
||
"ab": "two characters",
|
||
"": "empty string",
|
||
"\t": "a control character",
|
||
}
|
||
|
||
|
||
@pytest.mark.parametrize("ch", _ACCEPTED)
|
||
def test_is_grid_safe_accepts(ch: str) -> None:
|
||
assert is_grid_safe(ch) is True
|
||
|
||
|
||
@pytest.mark.parametrize("text", list(_REJECTED), ids=list(_REJECTED.values()))
|
||
def test_is_grid_safe_rejects(text: str) -> None:
|
||
assert is_grid_safe(text) is False
|
||
|
||
|
||
def test_safe_palette_is_all_grid_safe() -> None:
|
||
"""Every curated palette glyph clears the gate — the appendix can't ship a dud."""
|
||
bad = [g for g in SAFE_PALETTE if not is_grid_safe(g)]
|
||
assert bad == [], f"palette has non-grid-safe glyphs: {bad}"
|
||
|
||
|
||
def test_safe_palette_has_no_reserved_glyphs() -> None:
|
||
"""No palette glyph is a loader-reserved marker — the 'author-usable' promise.
|
||
|
||
The appendix tells a pack author to pull any palette glyph for terrain,
|
||
structures, or actors, but the loader rejects the box-drawing frame lines
|
||
and the '@'/'☻' player markers (``loader.RESERVED_GLYPHS``). A palette entry
|
||
that is also reserved would hand the author a glyph that load-fails — the
|
||
exact doc-vs-enforcement trap. Guarding the intersection keeps "all tested
|
||
safe AND author-usable" enforced, not merely asserted on width.
|
||
"""
|
||
collisions = set(SAFE_PALETTE) & RESERVED_GLYPHS
|
||
assert collisions == set(), f"palette offers loader-reserved glyphs: {sorted(collisions)}"
|
||
|
||
|
||
def test_safe_palette_has_no_duplicates() -> None:
|
||
"""The palette is a set in spirit; a dupe would be an authoring slip."""
|
||
assert len(SAFE_PALETTE) == len(set(SAFE_PALETTE))
|
||
|
||
|
||
def test_ambiguous_width_glyphs_are_accepted() -> None:
|
||
"""Document the load-bearing call: EAW=Ambiguous is admitted, not barred.
|
||
|
||
These are the CP437 glyphs the game depends on; if a future tightening
|
||
barred Ambiguous, the whole re-skin would vanish from the map.
|
||
"""
|
||
for ch in "█♣↑∩≈★":
|
||
assert unicodedata.east_asian_width(ch) == "A"
|
||
assert is_grid_safe(ch) is True
|