Claustrowordia¶
Port of Antti Haavikko’s LD50 jam winner, with procedural audio and word scoring.
▶ Run in browserUpstream: https://github.com/anttihaavikko/claustrowordia
Licence: this port's own code is offered under MIT, not the SimVX Examples Licence the rest of the gallery carries. See ATTRIBUTION.md for the upstream it re-implements, the terms of anything it bundles, and the credit each one requires.
Ports live in the repository only, not in the simvx-examples distribution, because each is a derivative work licensed individually against the game it re-implements. Read it with git clone https://git.simvx.com/simvx/simvx.
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 and score every valid English word they spell along a row or column, read either way.
Licensing: clean re-implementation under MIT; the bundled dictionary is the
public-domain ENABLE word list (3-7 letters, profanity-filtered), not the
upstream’s. See ATTRIBUTION.md and LICENSE.
Run¶
# from the repo root
uv run python examples/ports/claustrowordia/main.py # interactive
uv run python examples/ports/claustrowordia/main.py --test # headless capture (3 frames)
uv run python examples/ports/claustrowordia/harness.py # scripted-input capture (6 stages)
uv run simvx export web examples/ports/claustrowordia/main.py \
-o /tmp/claustrowordia.html
Controls¶
Click (or tap) the title screen to start.
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, or a click, restarts after game-over.
Escape quits.
What this port demonstrates¶
Procedural textures. Every tile face, grid cell, and drop preview is an anti-aliased rounded rectangle built as a numpy RGBA array and handed straight to
Sprite2D.texture: no image files ship with the port (nodes/textures.py).Procedural audio. The place pop, the per-letter ascending notes and the word chime are baked from
AudioSynth(oscillator + ADSR) intoAudioClips at load time. The pickup chirp and the game-over drop are hand-written numpy instead, because one needs a falling pitch sweep and the other concatenates notes, andAudioSynthmodels neither (nodes/audio.py).Coroutines for sequencing. Scoring is a generator: it walks the matched tiles, pulses each one, plays its note, and waits between them with
yield from wait(...), so the animation reads as straight-line code (Game._check_words/_announce_word).Tween state on a node. Tiles spring toward their target slot and layer a damped-sine punch, a decaying shake, and a scale pulse on top (
nodes/tile.py).Signals.
Gameemitsscore_changedandgame_over_changed, so a HUD, an achievement tracker, or a test can follow the run without polling the node.MSDF text. Letters and HUD are
Text2Dnodes ordered byz_index, so the same scene renders identically on desktop Vulkan and in the browser.Input actions. The root declares
input_actions, so the same bindings apply on desktop and on web, where touch arrives asMouseButton.LEFT.
Source files¶
File |
Summary |
Lines |
|---|---|---|
Claustrowordia: Port of Antti Haavikko’s LD50 jam winner, with procedural audio and word scoring. |
89 |
|
Scripted-input harness: exercises the title screen, pickup, drop, and scoring. |
180 |
|
Claustrowordia port: node modules. |
1 |
|
Procedural audio: built on the engine’s AudioSynth API. |
137 |
|
Word list + letter pool. |
101 |
|
Game: the top-level scene wiring grid, hand, scoring, and audio. |
502 |
|
Grid: the 7×7 board. |
117 |
|
Hand: the bottom row of letter tiles waiting to be placed. |
69 |
|
Procedural tile textures. |
129 |
|
Tile node: a single letter tile. |
179 |
Source¶
1"""Claustrowordia: Port of Antti Haavikko's LD50 jam winner, with procedural audio and word scoring.
2
3# /// simvx
4# tags = ["port", "tier-1"]
5# upstream = "https://github.com/anttihaavikko/claustrowordia"
6# web = { width = 1280, height = 800, responsive = true }
7# ///
8
9Run:
10 uv run python examples/ports/claustrowordia/main.py
11 uv run python examples/ports/claustrowordia/main.py --test # headless capture
12"""
13
14# /// script
15# requires-python = ">=3.14"
16# dependencies = ["numpy"]
17# ///
18
19from __future__ import annotations
20
21import sys
22from pathlib import Path
23
24# Allow running from any cwd
25_PORT_DIR = Path(__file__).parent
26if str(_PORT_DIR) not in sys.path:
27 sys.path.insert(0, str(_PORT_DIR))
28
29from nodes.game import Game # noqa: E402
30
31from simvx.core import Node2D # noqa: E402
32from simvx.core.input.enums import Key, MouseButton # 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: declares the input actions and adds the Game node."""
42
43 # Declared on the root so the scene tree registers them on mount: the web
44 # runtime instantiates this class directly and never calls main().
45 # Touch surfaces as MouseButton.LEFT, so "primary" covers mouse and finger.
46 input_actions = {
47 "primary": [MouseButton.LEFT],
48 "secondary": [MouseButton.RIGHT],
49 "start": [Key.ENTER, Key.SPACE],
50 "restart": [Key.R],
51 "quit": [Key.ESCAPE],
52 }
53
54 def on_ready(self) -> None:
55 self.game = self.add_child(Game(viewport_size=Vec2(WIDTH, HEIGHT)))
56
57
58def main() -> None:
59 headless = "--test" in sys.argv
60 if headless:
61 from simvx.core.testing import InputSimulator
62 from simvx.graphics import save_png
63
64 # The game opens on its title screen; dismiss it so the captures show
65 # the board rather than three copies of the menu.
66 sim = InputSimulator()
67
68 def on_frame(idx: int, _t: float) -> None:
69 if idx == 5:
70 sim.press_mouse(MouseButton.LEFT, (WIDTH / 2, HEIGHT / 2))
71 elif idx == 10:
72 sim.release_mouse(MouseButton.LEFT)
73
74 capture_at = [30, 60, 120]
75 app = App(width=WIDTH, height=HEIGHT, title="Claustrowordia (SimVX)", visible=False)
76 frames = app.run_headless(ClaustrowordiaRoot(), frames=130, on_frame=on_frame, capture_frames=capture_at)
77 out_dir = _PORT_DIR / "screenshots"
78 out_dir.mkdir(exist_ok=True)
79 for idx, img in zip(capture_at, frames, strict=False):
80 out_path = out_dir / f"frame_{idx}.png"
81 save_png(img, out_path)
82 print(f"saved {out_path}")
83 else:
84 app = App(width=WIDTH, height=HEIGHT, title="Claustrowordia (SimVX)")
85 app.run(ClaustrowordiaRoot())
86
87
88if __name__ == "__main__":
89 main()