Claustrowordia¶
LD50 winner, UI tweens, audio juice, 56k-word dictionary scoring.
📄 Docs onlyUpstream: 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.pylaunches 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()