Casual Crusade¶
a card-laying dungeon crawler with path-walk scoring.
▶ Run in browserUpstream: https://github.com/anttihaavikko/casual-crusade
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
Casual Crusade (SimVX port)¶
A SimVX port of Antti Haavikko’s js13k 2023 entry, a card-laying dungeon crawler on a domino-style board.
Licensing: a clean re-implementation under MIT, drawn entirely with procedural
on_draw calls (no upstream art, audio, or fonts are bundled). See
ATTRIBUTION.md and LICENSE.
Run¶
# from the repo root
uv run python examples/ports/casual_crusade/main.py # interactive
uv run python examples/ports/casual_crusade/main.py --test # headless capture (3 frames)
uv run python examples/ports/casual_crusade/harness.py # scripted-input capture (8 stages)
uv run simvx export web examples/ports/casual_crusade/main.py \
-o /tmp/casual_crusade.html
Controls¶
Action |
Input |
|---|---|
Place card |
Drag from hand onto a legal (highlighted) tile |
Cancel drag |
Right-click |
Restart |
|
Load run |
|
Save run |
|
Everything is reachable with the mouse alone (so it plays on touch): the three chips on the bottom controls strip mirror the three keys.
Gameplay summary¶
Each card has 1-4 directional edges. Place a card so at least one edge meets a matching neighbour edge already on the board.
The pilgrim then walks the newly connected path, scoring at each step. Later steps in a walk are worth more, and the level number multiplies the lot.
Stepping next to a chest loots it and opens a reward picker: the card you pick joins your permanent deck. Each option shows its gem’s name and effect.
Gems bend the rules: Fibonacci’s Boon draws an extra card, Pope’s Blessing heals, Khan’s Legacy fills the neighbouring tiles with blanks, Penance recycles a card in hand, Dynasty doubles the running step multiplier, and Indulgence scores tenfold.
Empty (non-reward) tiles at level end cost one life each. The run ends at zero lives.
Layout¶
The port lays out in a fixed 1280x720 design space: every rect and hit-test in
nodes/constants.py and nodes/game.py is in those coordinates.
Saving writes saves/casual_crusade.json relative to the directory you launched
from, never into the example’s source tree.
File map¶
casual_crusade/
├── main.py # entry point: root node + input actions
├── harness.py # scripted-input capture (8 stages)
├── nodes/
│ ├── constants.py # design resolution, colours, gem table, level titles
│ ├── card_data.py # CardData + random_card + starter_deck
│ ├── card.py # Card (drag, hover, procedural draw)
│ ├── tile.py # Tile (board cell, accepts/marked/hilite, chest)
│ ├── dude.py # the pilgrim (path-walk hop + procedural draw)
│ └── game.py # Game (board, hand, scoring, picker, save/load)
└── screenshots/ # harness + idle captures
Source files¶
File |
Summary |
Lines |
|---|---|---|
Casual Crusade: a card-laying dungeon crawler with path-walk scoring. |
81 |
|
Scripted-input harness: exercises menu, hover, drag-drop, scoring, save/load. |
154 |
|
0 |
||
Card: interactive node that draws a card face procedurally. |
188 |
|
Card data record and deck-generation helpers. |
55 |
|
Casual Crusade: global constants. |
94 |
|
Dude: pilgrim avatar that walks the path of placed cards. |
83 |
|
Game: the scene managing board, hand, scoring, pilgrim and save/load. |
1160 |
|
Tile: single board cell. Owns optional Card content; draws grass or chest. |
111 |
Source¶
1"""Casual Crusade: a card-laying dungeon crawler with path-walk 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 port of Antti Haavikko's js13k 2023 entry. Drag cards from your hand onto the
10board so their direction lines meet; the pilgrim then walks the newly connected
11path and scores every step. Stepping beside a chest loots it for a reward card,
12gems on the cards bend the rules, and every tile still empty when the level ends
13costs a life.
14
15Everything on screen is drawn procedurally in ``on_draw`` (no image assets):
16coroutines animate the walk and time the banner messages, polled input actions
17drive the drag-and-drop with snap targets, ``Text2D`` carries the HUD, and a run
18saves to and loads from JSON.
19
20Run:
21 uv run python examples/ports/casual_crusade/main.py
22 uv run python examples/ports/casual_crusade/main.py --test
23"""
24
25from __future__ import annotations
26
27import sys
28from pathlib import Path
29
30_PORT_DIR = Path(__file__).parent
31if str(_PORT_DIR) not in sys.path:
32 sys.path.insert(0, str(_PORT_DIR))
33
34from nodes.constants import HEIGHT, WIDTH # noqa: E402
35from nodes.game import Game # noqa: E402
36
37from simvx.core import InputMap, Key, MouseButton, Node2D # noqa: E402
38from simvx.graphics import App # noqa: E402
39
40
41class CasualCrusadeRoot(Node2D):
42 """Root node: owns the input actions and hosts the Game subtree."""
43
44 autostart = False
45
46 def on_ready(self) -> None:
47 # Actions are registered here, in the root's ready path, because the web
48 # export instantiates the root directly and never calls ``main()``.
49 # Everything the Game polls goes through these names.
50 InputMap.add_action("primary", [MouseButton.LEFT])
51 InputMap.add_action("secondary", [MouseButton.RIGHT])
52 InputMap.add_action("save", [Key.S])
53 InputMap.add_action("load", [Key.L])
54 InputMap.add_action("restart", [Key.R])
55 self.game = self.add_child(Game(autostart=self.autostart))
56
57
58def main() -> None:
59 headless = "--test" in sys.argv
60 if headless:
61 from simvx.graphics import save_png
62
63 capture_at = [30, 60, 120]
64 app = App(width=WIDTH, height=HEIGHT, title="Casual Crusade (SimVX)", visible=False)
65 # Run with autostart so the headless capture exercises gameplay UI.
66 root = CasualCrusadeRoot()
67 root.autostart = True
68 frames = app.run_headless(root, frames=121, capture_frames=capture_at)
69 out_dir = _PORT_DIR / "screenshots"
70 out_dir.mkdir(exist_ok=True)
71 for idx, img in zip(capture_at, frames, strict=False):
72 out_path = out_dir / f"frame_{idx}.png"
73 save_png(img, out_path)
74 print(f"saved {out_path}")
75 else:
76 app = App(width=WIDTH, height=HEIGHT, title="Casual Crusade (SimVX)")
77 app.run(CasualCrusadeRoot())
78
79
80if __name__ == "__main__":
81 main()