Claustrowordia

LD50 winner, UI tweens, audio juice, 56k-word dictionary scoring.

📄 Docs only

Upstream: https://github.com/anttihaavikko/claustrowordia

Tags: port tier-1

Claustrowordia (SimVX port)

SimVX port of anttihaavikko/claustrowordia LD50 jam winner. Crossword-on-a-grid puzzle: place letter tiles on a 7×7 board, score per valid English word formed in any direction.

Run

# from /home/fezzik/dev/simvx
uv run python ported_games/claustrowordia/simvx_port/main.py            # interactive
uv run python ported_games/claustrowordia/simvx_port/main.py --test     # headless capture (3 frames)
uv run python ported_games/claustrowordia/simvx_port/harness.py         # scripted-input capture (5 stages)
uv run simvx export web ported_games/claustrowordia/simvx_port/main.py \
    -o ported_games/claustrowordia/simvx_port/web/index.html

Controls

  • Click a letter in the hand picks up the tile (it follows the cursor).

  • Click on an empty grid cell drops the tile there. Words formed in rows / columns (forward and reversed) are scored.

  • Right-click while holding a tile cancels and returns it to the hand.

  • R restarts after game-over.

  • Escape quits.

Acceptance bar

  • [x] Source cloned to source/

  • [x] main.py launches the game with a fanned hand + 7×7 grid

  • [x] Tile placement, word validation, scoring, lose condition all wired

  • [x] UI tweens (damped-sine punch on place / score; bounce on hand layout)

  • [x] Audio juice: place sound, score note (per-letter ascending pitch), word-complete chime, game-over thunk: all procedural

  • [x] Running score in the HUD (Text2D)

  • [x] Headless screenshots at frame 30 / 60 / 120 in screenshots/

  • [x] Scripted harness covering 5 stages

  • [x] Web export to web/index.html

  • [x] NOTES.md with friction points + engine-gap candidates

Source

 1"""Claustrowordia: LD50 winner, UI tweens, audio juice, 56k-word dictionary scoring.
 2
 3# /// simvx
 4# tags = ["port", "tier-1"]
 5# upstream = "https://github.com/anttihaavikko/claustrowordia"
 6# web = { width = 1280, height = 800, responsive = true, disabled = true, reason = "Blocked on audio refactor (uses removed AudioStream.from_pcm(sample_rate=…) kwarg)." }
 7# ///
 8
 9Run:
10    uv run python ported_games/claustrowordia/simvx_port/main.py
11    uv run python ported_games/claustrowordia/simvx_port/main.py --test    # headless capture
12"""
13# /// script
14# requires-python = ">=3.13"
15# dependencies = ["numpy", "freetype-py"]
16# ///
17
18from __future__ import annotations
19
20import sys
21from pathlib import Path
22
23# Allow running from any cwd
24_PORT_DIR = Path(__file__).parent
25if str(_PORT_DIR) not in sys.path:
26    sys.path.insert(0, str(_PORT_DIR))
27
28from nodes.game import Game  # noqa: E402
29
30from simvx.core import Node2D  # noqa: E402
31from simvx.core.input.enums import Key, MouseButton  # noqa: E402
32from simvx.core.input.map import InputMap  # noqa: E402
33from simvx.core.math.types import Vec2  # noqa: E402
34from simvx.graphics import App  # noqa: E402
35
36WIDTH = 1280
37HEIGHT = 800
38
39
40class ClaustrowordiaRoot(Node2D):
41    """Root scene wrapper: registers input actions and adds the Game node."""
42
43    def on_ready(self) -> None:
44        # Input map (must live in root.on_ready(); web exporter skips main()).
45        InputMap.add_action("primary", [MouseButton.LEFT])
46        InputMap.add_action("secondary", [MouseButton.RIGHT])
47        InputMap.add_action("restart", [Key.R])
48        InputMap.add_action("quit", [Key.ESCAPE])
49
50        self.game = self.add_child(Game(viewport_size=Vec2(WIDTH, HEIGHT)))
51
52
53def main() -> None:
54    headless = "--test" in sys.argv
55    if headless:
56        from simvx.graphics import save_png
57
58        capture_at = [30, 60, 120]
59        app = App(width=WIDTH, height=HEIGHT, title="Claustrowordia (SimVX)", visible=False)
60        frames = app.run_headless(ClaustrowordiaRoot(), frames=130, capture_frames=capture_at)
61        out_dir = _PORT_DIR / "screenshots"
62        out_dir.mkdir(exist_ok=True)
63        for idx, img in zip(capture_at, frames, strict=False):
64            out_path = out_dir / f"frame_{idx}.png"
65            save_png(out_path, img)
66            print(f"saved {out_path}")
67    else:
68        app = App(width=WIDTH, height=HEIGHT, title="Claustrowordia (SimVX)")
69        app.run(ClaustrowordiaRoot())
70
71
72if __name__ == "__main__":
73    main()