2026-07-14 19:14:49 -04:00
|
|
|
# mcrf: objects=[BSP,Caption,Color,ColorLayer,Grid,Scene] verified=0.2.8-dev status=ok
|
test(snippets): the docs site's 130 code samples are now part of the test suite
The samples published on mcrogueface.github.io were executed by nothing at all.
Each carried a machine-readable header -- objects=[...] verified=0.2.8@b6720f6
status=ok -- and every one of those fields was hand-typed. 130 snippets asserted
"status=ok" while no run had ever confirmed it. This is the same failure mode as
the 82 rotted tests in 112f357, at a larger scale and with no gate whatsoever.
The samples now live here, in the engine repo, so that breaking the API breaks the
build. The site pulls them from tests/snippets/ rather than keeping a copy to rot
in parallel (build_library.py, site-side).
They are display scripts: each builds a scene and stops. They deliberately do NOT
call sys.exit(), because they must stay copy-pasteable -- a reader who pastes one
into their game should get a running scene, not an interpreter that quits. Since
#350 that also makes them un-runnable on their own, so _harness.py is chained as a
second --exec to supply the ending the snippet must not have. Chaining rather than
exec()-ing the source is deliberate: the snippet travels the same path a real
user's script travels, and nothing has to read the file -- which is how the first
draft of this harness tripped over #378 and mistook three healthy snippets for
broken ones.
A snippet passes only if it runs clean AND leaves a scene with something in it.
Not raising is not enough: a sample whose body silently no-ops still renders as
code on the docs site, and would still be wrong.
tools/stamp_snippets.py makes the header a measurement instead of a claim: status
comes from an actual run, verified from the engine that ran it, and objects is
derived from the source intersected with what the engine exports -- so a snippet
that starts using a new type gets tagged for it without anyone remembering to. It
immediately found tags the humans had missed (062 uses Transition, Key and
InputState; the hand-written header listed none of them). --check is the CI gate.
The gate earned its keep on the first run, catching #379 (fixed here): configuring
default_transition before the first scene activation made the engine transition
FROM the internal "uitest" bootstrap scene, which is not a PyScene and has no
Python wrapper -- so mcrfpy.current_scene reported None for the transition's full
duration, immediately after the user had assigned it. A transition with nothing
meaningful to transition from now changes scene immediately. Scene-to-scene
transitions still report the outgoing scene until they complete.
Suite: 468/468 (336 + 130 snippets + 2 regressions).
closes #379
2026-07-14 18:29:37 -04:00
|
|
|
# BSP Basic - Binary space partition for dungeon generation
|
|
|
|
|
import mcrfpy
|
|
|
|
|
|
|
|
|
|
scene = mcrfpy.Scene("demo")
|
|
|
|
|
mcrfpy.current_scene = scene
|
|
|
|
|
|
|
|
|
|
grid = mcrfpy.Grid(
|
|
|
|
|
grid_size=(16, 12),
|
|
|
|
|
texture=mcrfpy.default_texture,
|
|
|
|
|
pos=(0, 0),
|
|
|
|
|
size=(1024, 768),
|
|
|
|
|
zoom=4.0
|
|
|
|
|
)
|
|
|
|
|
scene.children.append(grid)
|
|
|
|
|
|
|
|
|
|
# Add color layer for room visualization
|
|
|
|
|
room_layer = mcrfpy.ColorLayer(z_index=1, name="rooms")
|
|
|
|
|
grid.add_layer(room_layer)
|
|
|
|
|
|
|
|
|
|
# Fill with walls
|
|
|
|
|
for y in range(12):
|
|
|
|
|
for x in range(16):
|
|
|
|
|
grid.at(x, y).tilesprite = 1
|
|
|
|
|
grid.at(x, y).walkable = False
|
|
|
|
|
|
|
|
|
|
# Create BSP tree (pos and size as tuples)
|
|
|
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(16, 12))
|
feat(docs): deterministic snippet screenshots — Phase 1 (STATIC capture)
Generate a byte-stable preview PNG for every docs snippet, as the visual-
regression oracle foundation of #381. A changed image means the snippet's
behaviour changed and wants review.
Pipeline (no C++ change; existing headless build suffices):
- _seed.py: chained as the FIRST --exec so random.seed(42) governs the
snippet's RNG before it draws (snippets draw at import; seeding after is
too late). Shared-interpreter seeding covers all 25 random-module snippets.
- _screenshot.py: chained as the LAST --exec; steps setup frames then
automation.screenshot(). Kept separate from _harness.py so the pass/fail
suite stays fast and untouched.
- tools/generate_snippet_shots.py: orchestrator; runs seed->snippet->shot,
writes gitignored snippet-shots/ (images belong in the doc-site repo,
which pulls them from here). Per-snippet capture params live in a sidecar
OVERRIDES table, not the mcrf: header (which would fight stamp_snippets
and can't hold Phase-3 interaction scripts).
- make snippet-shots target; snippet-shots/ gitignored.
The 4 BSP snippets that needed deterministic pixels (060, 102, 224, 243)
now pass seed=42 — libtcod's global RNG is time(0)-seeded and unreachable
from Python, and seed= also documents reproducible generation for readers.
Validated: 272 captured, 10 skipped (noshot opt-outs), 0 failed; all 272
byte-identical across two full runs (~49s).
Addresses #381.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LTh2ZW7bd3XSd9qK86Z2CE
2026-07-15 20:57:04 -04:00
|
|
|
bsp.split_recursive(depth=3, min_size=(3, 3), seed=42)
|
test(snippets): the docs site's 130 code samples are now part of the test suite
The samples published on mcrogueface.github.io were executed by nothing at all.
Each carried a machine-readable header -- objects=[...] verified=0.2.8@b6720f6
status=ok -- and every one of those fields was hand-typed. 130 snippets asserted
"status=ok" while no run had ever confirmed it. This is the same failure mode as
the 82 rotted tests in 112f357, at a larger scale and with no gate whatsoever.
The samples now live here, in the engine repo, so that breaking the API breaks the
build. The site pulls them from tests/snippets/ rather than keeping a copy to rot
in parallel (build_library.py, site-side).
They are display scripts: each builds a scene and stops. They deliberately do NOT
call sys.exit(), because they must stay copy-pasteable -- a reader who pastes one
into their game should get a running scene, not an interpreter that quits. Since
#350 that also makes them un-runnable on their own, so _harness.py is chained as a
second --exec to supply the ending the snippet must not have. Chaining rather than
exec()-ing the source is deliberate: the snippet travels the same path a real
user's script travels, and nothing has to read the file -- which is how the first
draft of this harness tripped over #378 and mistook three healthy snippets for
broken ones.
A snippet passes only if it runs clean AND leaves a scene with something in it.
Not raising is not enough: a sample whose body silently no-ops still renders as
code on the docs site, and would still be wrong.
tools/stamp_snippets.py makes the header a measurement instead of a claim: status
comes from an actual run, verified from the engine that ran it, and objects is
derived from the source intersected with what the engine exports -- so a snippet
that starts using a new type gets tagged for it without anyone remembering to. It
immediately found tags the humans had missed (062 uses Transition, Key and
InputState; the hand-written header listed none of them). --check is the CI gate.
The gate earned its keep on the first run, catching #379 (fixed here): configuring
default_transition before the first scene activation made the engine transition
FROM the internal "uitest" bootstrap scene, which is not a PyScene and has no
Python wrapper -- so mcrfpy.current_scene reported None for the transition's full
duration, immediately after the user had assigned it. A transition with nothing
meaningful to transition from now changes scene immediately. Scene-to-scene
transitions still report the outgoing scene until they complete.
Suite: 468/468 (336 + 130 snippets + 2 regressions).
closes #379
2026-07-14 18:29:37 -04:00
|
|
|
|
|
|
|
|
# Room colors
|
|
|
|
|
colors = [
|
|
|
|
|
mcrfpy.Color(255, 150, 150, 150),
|
|
|
|
|
mcrfpy.Color(150, 255, 150, 150),
|
|
|
|
|
mcrfpy.Color(150, 150, 255, 150),
|
|
|
|
|
mcrfpy.Color(255, 255, 150, 150),
|
|
|
|
|
mcrfpy.Color(255, 150, 255, 150),
|
|
|
|
|
mcrfpy.Color(150, 255, 255, 150),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# Carve rooms from leaves
|
|
|
|
|
for i, leaf in enumerate(bsp):
|
|
|
|
|
x, y = leaf.pos
|
|
|
|
|
w, h = leaf.size
|
|
|
|
|
# Shrink room slightly
|
|
|
|
|
for ry in range(y + 1, y + h - 1):
|
|
|
|
|
for rx in range(x + 1, x + w - 1):
|
|
|
|
|
if 0 <= rx < 16 and 0 <= ry < 12:
|
|
|
|
|
grid.at(rx, ry).tilesprite = 48
|
|
|
|
|
grid.at(rx, ry).walkable = True
|
|
|
|
|
room_layer.set((rx, ry), colors[i % len(colors)])
|
|
|
|
|
|
|
|
|
|
status = mcrfpy.Caption(text="BSP dungeon - each color is a leaf node", pos=(320, 720))
|
|
|
|
|
status.outline = 2
|
|
|
|
|
status.outline_color = mcrfpy.Color(0, 0, 0)
|
|
|
|
|
scene.children.append(status)
|