Casual Crusade¶
js13k card/dungeon, domino-card placement with BFS scoring.
▶ Run in browserUpstream: https://github.com/anttihaavikko/casual-crusade
Tags: port tier-1
Casual Crusade (SimVX port)¶
A SimVX port of Antti Haavikko’s js13k 2023 entry, a card-laying dungeon-crawler / domino puzzle.
Run¶
All commands assume cwd = /home/fezzik/dev/simvx.
# Interactive (windowed)
uv run python ported_games/casual_crusade/simvx_port/main.py
# Headless capture (frames 30, 60, 120)
uv run python ported_games/casual_crusade/simvx_port/main.py --test
# Scripted multi-stage harness (7 captures)
uv run python ported_games/casual_crusade/simvx_port/harness.py
# Web export
uv run simvx export web ported_games/casual_crusade/simvx_port/main.py \
-o ported_games/casual_crusade/simvx_port/web/index.html
Controls¶
Action |
Input |
|---|---|
Place card |
Drag from hand onto a legal (highlighted) tile |
Cancel drag |
Right-click |
Save run |
|
Load run |
|
Restart |
|
Gameplay summary¶
Each card has 1-4 directional edges. Place a card so at least one edge meets a matching neighbour edge on the board.
The pilgrim walks the new path through connected cards, scoring along the way.
Stepping on a card next to a chest loots it, opening a reward picker, picking one card to add to your permanent deck.
Empty (non-reward) tiles at level end cost one life each. Run ends when life reaches zero. Press
Rto restart.
File map¶
simvx_port/
├── main.py # entry point + InputMap registration
├── harness.py # scripted-input capture (7 stages)
├── pyproject.toml # [tool.simvx] root = "CasualCrusadeRoot"
├── nodes/
│ ├── constants.py # widths/colours/level titles
│ ├── card_data.py # CardData + random_card + starter_deck
│ ├── card.py # Card (drag, hover, draw)
│ ├── tile.py # Tile (board cell, accepts/marked/hilite)
│ ├── dude.py # pilgrim (path-walk + draw)
│ └── game.py # Game (board, hand, scoring, picker, save/load)
├── saves/ # JSON save (created on first save)
├── screenshots/ # 7 harness + 3 idle captures
└── web/index.html # 2.5 MB self-contained Pyodide bundle
Source¶
1"""Casual Crusade: js13k card/dungeon, domino-card placement with BFS scoring.
2
3# /// simvx
4# tags = ["port", "tier-1"]
5# upstream = "https://github.com/anttihaavikko/casual-crusade"
6# web = { width = 1280, height = 720, responsive = true }
7# ///
8
9A card-laying dungeon-crawler / domino puzzle. Place cards from your hand onto
10the board so their direction-lines connect. The pilgrim walks the new path and
11scores. Reward chests trigger a card-pick after stepping on adjacent cards.
12
13Run:
14 uv run python ported_games/casual_crusade/simvx_port/main.py
15 uv run python ported_games/casual_crusade/simvx_port/main.py --test
16"""
17
18from __future__ import annotations
19
20import sys
21from pathlib import Path
22
23_PORT_DIR = Path(__file__).parent
24if str(_PORT_DIR) not in sys.path:
25 sys.path.insert(0, str(_PORT_DIR))
26
27from nodes.constants import HEIGHT, WIDTH # noqa: E402
28from nodes.game import Game # noqa: E402
29
30from simvx.core import InputMap, Key, MouseButton, Node2D # noqa: E402
31from simvx.graphics import App # noqa: E402
32
33
34class CasualCrusadeRoot(Node2D):
35 """Root node: wraps Game so the web exporter can pick it up via [tool.simvx]."""
36
37 autostart = False
38
39 def on_ready(self) -> None:
40 # Input actions belong on the root (web-export rule). Game subtree
41 # only consumes them.
42 InputMap.add_action("primary", [MouseButton.LEFT])
43 InputMap.add_action("secondary", [MouseButton.RIGHT])
44 InputMap.add_action("save", [Key.S])
45 InputMap.add_action("load", [Key.L])
46 InputMap.add_action("restart", [Key.R])
47 InputMap.add_action("escape", [Key.ESCAPE])
48 self.game = self.add_child(Game(autostart=self.autostart))
49
50
51def main() -> None:
52 headless = "--test" in sys.argv
53 if headless:
54 from simvx.graphics import save_png
55
56 capture_at = [30, 60, 120]
57 app = App(width=WIDTH, height=HEIGHT, title="Casual Crusade (SimVX)", visible=False)
58 # Run with autostart so the headless capture exercises gameplay UI.
59 root = CasualCrusadeRoot()
60 root.autostart = True
61 frames = app.run_headless(root, frames=121, capture_frames=capture_at)
62 out_dir = _PORT_DIR / "screenshots"
63 out_dir.mkdir(exist_ok=True)
64 for idx, img in zip(capture_at, frames, strict=False):
65 out_path = out_dir / f"frame_{idx}.png"
66 save_png(out_path, img)
67 print(f"saved {out_path}")
68 else:
69 app = App(width=WIDTH, height=HEIGHT, title="Casual Crusade (SimVX)")
70 app.run(CasualCrusadeRoot())
71
72
73if __name__ == "__main__":
74 main()