PirateMaker

In-game level editor + play loop, proving editor = runtime.

▶ Run in browser

Upstream: 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: PirateMakerRoot toggles 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: --test deterministic capture mode.

  • nodes/editor.py: EditorMode, EditorObject, Cloud, CanvasTile, palette.

  • nodes/level.py: PlayMode builds 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 to levels/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()