PirateMaker¶
In-game level editor + play loop, proving editor = runtime.
▶ Run in browserUpstream: https://github.com/clear-code-projects/PirateMaker
Tags: port tier-2
PirateMaker: SimVX port¶
Side-scrolling pirate platformer with an in-game level editor. Tier 2 #20: port of clear-code-projects/PirateMaker (CC0 code + Pixelfrog CC0 art).
Run¶
# Desktop
cd /home/fezzik/dev/simvx
uv run --with pillow python ../ported_games/piratemaker/simvx_port/main.py
# Headless screenshots (writes 7 PNGs to screenshots/)
uv run --with pillow python ../ported_games/piratemaker/simvx_port/main.py --test
Controls¶
Editor: LMB drag-paint, RMB drag-erase, MMB pan, mouse-wheel pan,
[/]cycle selection, Enter play, S save, L load, Q quit.Play: Left/Right (or A/D) to move, Space (or W/Up) to jump, Esc back to editor.
Layout¶
main.py:PirateMakerRoottoggles between editor and play modes.settings.py: TILE_SIZE, screen size, the EDITOR_DATA tile-id table.support.py: folder-of-PNGs animation loaders.harness.py:--testdeterministic capture mode.nodes/editor.py:EditorMode,EditorObject,Cloud,CanvasTile, palette.nodes/level.py:PlayModebuilds a runnable level from the editor’s grid dict.nodes/player.py: Player movement, gravity, AABB collision, animation.nodes/enemies.py: Spikes, Tooth, Shell, Pearl.nodes/folder_sprite.py: Sprite2D that cycles paths.nodes/save_load.py: JSON save/load tolevels/level.json.
See ../NOTES.md for engine friction and design deviations.
Source¶
1#!/usr/bin/env python3
2"""PirateMaker: In-game level editor + play loop, proving editor = runtime.
3
4# /// simvx
5# tags = ["port", "tier-2"]
6# upstream = "https://github.com/clear-code-projects/PirateMaker"
7# web = { width = 1280, height = 720, responsive = true }
8# ///
9
10Editor-inside-game platformer: place tiles in editor mode,
11press Enter to play the level.
12
13Run:
14 uv run python ported_games/piratemaker/simvx_port/main.py
15 uv run python ported_games/piratemaker/simvx_port/main.py --test
16"""
17from __future__ import annotations
18
19import argparse
20import sys
21from pathlib import Path
22
23_PORT_DIR = Path(__file__).resolve().parent
24if str(_PORT_DIR) not in sys.path:
25 sys.path.insert(0, str(_PORT_DIR))
26
27from settings import WINDOW_HEIGHT, WINDOW_WIDTH
28
29from simvx.core import Input, InputMap, Key, MouseButton, Node2D
30from simvx.graphics import App
31
32
33class PirateMakerRoot(Node2D):
34 """Root scene: owns editor and (when active) play modes.
35
36 Mode swap is local: editor stays as a child, hidden when level plays.
37 InputMap actions registered once here so they survive mode swaps.
38 """
39
40 def on_ready(self) -> None:
41 # Movement (play mode)
42 InputMap.add_action("move_left", [Key.LEFT, Key.A])
43 InputMap.add_action("move_right", [Key.RIGHT, Key.D])
44 InputMap.add_action("jump", [Key.SPACE, Key.UP, Key.W])
45
46 # Editor selection
47 InputMap.add_action("select_prev", [Key.LEFT_BRACKET])
48 InputMap.add_action("select_next", [Key.RIGHT_BRACKET])
49
50 # Mode swap
51 InputMap.add_action("play", [Key.ENTER])
52 InputMap.add_action("editor", [Key.ESCAPE])
53
54 # Painting / panning
55 InputMap.add_action("paint", [MouseButton.LEFT])
56 InputMap.add_action("erase", [MouseButton.RIGHT])
57 InputMap.add_action("pan", [MouseButton.MIDDLE])
58
59 # Save / load slot
60 InputMap.add_action("save_level", [Key.S])
61 InputMap.add_action("load_level", [Key.L])
62
63 # Quit
64 InputMap.add_action("quit_app", [Key.Q])
65
66 # Lazy import: nodes module needs InputMap registered first.
67 from nodes.editor import EditorMode
68
69 self.editor: EditorMode = self.add_child(EditorMode())
70 self.level = None # PlayMode | None
71
72 def on_process(self, dt: float) -> None:
73 # Editor → play (Enter)
74 if self.editor and self.editor.visible and Input.is_action_just_pressed("play"):
75 self.start_play()
76
77 # Play → editor (Esc)
78 if self.level is not None and Input.is_action_just_pressed("editor"):
79 self.return_to_editor()
80
81 # Hard quit (Q from editor only)
82 if self.editor and self.editor.visible and Input.is_action_just_pressed("quit_app"):
83 self.app.quit()
84
85 def start_play(self) -> None:
86 """Build a level dict from editor canvas; spawn PlayMode child."""
87 from nodes.level import PlayMode
88
89 grid = self.editor.create_grid()
90 if not grid["terrain"] and not grid["water"]:
91 # Empty level: refuse to play
92 return
93 self.editor.visible = False
94 self.editor.stop_music()
95 self.level = self.add_child(PlayMode(grid))
96
97 def return_to_editor(self) -> None:
98 if self.level is not None:
99 self.level.stop_music()
100 self.level.destroy()
101 self.level = None
102 if self.editor:
103 self.editor.visible = True
104 self.editor.start_music()
105
106
107def main() -> None:
108 parser = argparse.ArgumentParser()
109 parser.add_argument("--test", action="store_true", help="Headless capture mode for screenshots.")
110 args = parser.parse_args()
111
112 if args.test:
113 from harness import run_test
114 run_test()
115 return
116
117 App(width=WINDOW_WIDTH, height=WINDOW_HEIGHT, title="PirateMaker").run(PirateMakerRoot())
118
119
120if __name__ == "__main__":
121 main()