Casual Crusade

js13k card/dungeon, domino-card placement with BFS scoring.

▶ Run in browser

Upstream: 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

S

Load run

L

Restart

R

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 R to 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()