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¶
# the export runs in a copy made by `simvx examples copy ports_claustrowordia`
simvx examples run ports_claustrowordia # interactive
simvx examples run ports_claustrowordia --test # headless capture (3 frames)
simvx export web main.py \
-o 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_context, so the same bindings apply on desktop and on web, where touch arrives asMouse.LEFT.
Source files¶
File |
Summary |
Lines |
|---|---|---|
Claustrowordia: Port of Antti Haavikko’s LD50 jam winner, with procedural audio and word scoring. |
86 |
|
Scripted-input harness: exercises the title screen, pickup, drop, and scoring. |
181 |
|
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# /// simvx
2# tags = ["port", "tier-1"]
3# upstream = "https://github.com/anttihaavikko/claustrowordia"
4# web = { width = 1280, height = 800, responsive = true }
5# ///
6"""Claustrowordia: Port of Antti Haavikko's LD50 jam winner, with procedural audio and word scoring.
7
8Run:
9 simvx examples run ports_claustrowordia
10 simvx examples run ports_claustrowordia --test # headless capture
11"""
12
13# /// script
14# requires-python = ">=3.14"
15# dependencies = ["numpy"]
16# ///
17
18from __future__ import annotations
19
20import sys
21from pathlib import Path
22
23_PORT_DIR = Path(__file__).parent
24
25from nodes.game import Game # noqa: E402
26
27from simvx.core import Node2D # noqa: E402
28from simvx.core.input.controls import Key, Mouse
29from simvx.core.math.types import Vec2 # noqa: E402
30from simvx.graphics import App # noqa: E402
31
32WIDTH = 1280
33HEIGHT = 800
34
35
36class ClaustrowordiaRoot(Node2D):
37 """Root scene wrapper: declares the input actions and adds the Game node."""
38
39 # Declared on the root, so they are in force for the whole tree while this
40 # scene is mounted, on the desktop and the web alike.
41 # Touch surfaces as Mouse.LEFT, so "primary" covers mouse and finger.
42 input_context = {
43 "primary": [Mouse.LEFT],
44 "secondary": [Mouse.RIGHT],
45 "start": [Key.ENTER, Key.SPACE],
46 "restart": [Key.R],
47 "quit": [Key.ESCAPE],
48 }
49
50 def on_ready(self) -> None:
51 self.game = self.add_child(Game(viewport_size=Vec2(WIDTH, HEIGHT)))
52
53
54def main() -> None:
55 headless = "--test" in sys.argv
56 if headless:
57 from simvx.core.testing import InputSimulator
58 from simvx.graphics import save_png
59
60 # The game opens on its title screen; dismiss it so the captures show
61 # the board rather than three copies of the menu.
62 root = ClaustrowordiaRoot()
63 sim = InputSimulator(root)
64
65 def on_frame(idx: int, _t: float) -> None:
66 if idx == 5:
67 sim.press_mouse(Mouse.LEFT, (WIDTH / 2, HEIGHT / 2))
68 elif idx == 10:
69 sim.release_mouse(Mouse.LEFT)
70
71 capture_at = [30, 60, 120]
72 app = App(width=WIDTH, height=HEIGHT, title="Claustrowordia (SimVX)", visible=False)
73 frames = app.run_headless(root, frames=130, on_frame=on_frame, capture_frames=capture_at)
74 out_dir = _PORT_DIR / "screenshots"
75 out_dir.mkdir(exist_ok=True)
76 for idx, img in zip(capture_at, frames, strict=False):
77 out_path = out_dir / f"frame_{idx}.png"
78 save_png(img, out_path)
79 print(f"saved {out_path}")
80 else:
81 app = App(width=WIDTH, height=HEIGHT, title="Claustrowordia (SimVX)")
82 app.run(ClaustrowordiaRoot())
83
84
85if __name__ == "__main__":
86 main()