Tower Defence¶
Waves, placement, currency UI, navigation curves.
▶ Run in browserUpstream: https://github.com/russs123/tower_defence_tut
Tags: port tier-1
Tower Defence (SimVX port)¶
SimVX port of russs123/tower_defence_tut (YouTube tutorial source, MIT-style permissive). Wave-based tower defence on a fixed map with a hand-authored enemy path; ports the upstream’s animated basic turret + four upgrade tiers and adds slow and sniper turret variants for the Tier-1 multi-tower-type acceptance bar.
Run¶
All commands from the SimVX repo root (uv workspace requirement):
# Interactive
uv run python ported_games/tower_defence_tut/simvx_port/main.py
# Headless smoke test (frame_30/60/120 captures)
uv run python ported_games/tower_defence_tut/simvx_port/main.py --test
# Scripted harness (10 stage screenshots covering 3+ waves)
uv run python ported_games/tower_defence_tut/simvx_port/harness.py
# Web export
uv run simvx export web ported_games/tower_defence_tut/simvx_port/main.py \
-o ported_games/tower_defence_tut/simvx_port/web/index.html
Controls¶
Action |
Keyboard |
Mouse |
|---|---|---|
Start (menu) |
|
Click anywhere |
Place BASIC turret |
|
“BUY BASIC” panel button |
Place SLOW turret |
|
Cycle TYPE then “BUY” |
Place SNIPER turret |
|
Cycle TYPE then “BUY” |
Cancel placement |
|
“CANCEL” panel button |
Begin wave |
|
“BEGIN WAVE” panel button |
Fast-forward x2 |
|
“FAST x2” panel button |
Upgrade selected turret |
|
“UPGRADE -> Lx (100c)” panel button |
Restart |
|
“RESTART” panel button |
Quit |
|
- |
Touch / mobile¶
Tower placement is a single tap on a grass tile – the SimVX web runtime
surfaces touchstart/end as MouseButton.LEFT, so the browser export is
playable on mobile without code changes.
Tower types¶
Type |
Cost |
Range (L1) |
DPS (L1) |
Notes |
|---|---|---|---|---|
Basic |
200 |
120 |
~5.5 |
Animated turret from upstream tutorial |
Slow |
250 |
70 |
~2.5 |
Applies a 0.5x slow debuff for 1 s on hit |
Sniper |
350 |
200 |
~5.8 |
Long range, slow rate, big single-shot damage |
All three types support 4-tier upgrades.
Source¶
1"""Tower Defence: Waves, placement, currency UI, navigation curves.
2
3# /// simvx
4# tags = ["port", "tier-1"]
5# upstream = "https://github.com/russs123/tower_defence_tut"
6# web = { width = 1280, height = 720, responsive = true }
7# ///
8
9Run:
10 uv run python ported_games/tower_defence_tut/simvx_port/main.py
11 uv run python ported_games/tower_defence_tut/simvx_port/main.py --test # headless capture
12 uv run python ported_games/tower_defence_tut/simvx_port/harness.py # scripted run
13 uv run simvx export web ported_games/tower_defence_tut/simvx_port/main.py -o ported_games/tower_defence_tut/simvx_port/web/index.html
14"""
15
16from __future__ import annotations
17
18import sys
19from pathlib import Path
20
21_PORT_DIR = Path(__file__).parent
22if str(_PORT_DIR) not in sys.path:
23 sys.path.insert(0, str(_PORT_DIR))
24
25from nodes.td_world import TowerDefenceWorld # noqa: E402
26
27from simvx.core import ( # noqa: E402
28 Input,
29 InputMap,
30 Key,
31 MouseButton,
32 Node2D,
33 Sprite2D,
34 Text2D,
35 Vec2,
36)
37from simvx.graphics import App # noqa: E402
38
39from nodes.td_data import ( # noqa: E402
40 SCREEN_HEIGHT,
41 SCREEN_WIDTH,
42 SIDE_PANEL,
43 WINDOW_HEIGHT,
44 WINDOW_WIDTH,
45)
46
47
48# ---------------------------------------------------------------------------
49# Menu / Root wrapper
50# ---------------------------------------------------------------------------
51
52
53class _Menu(Node2D):
54 """Simple menu screen -- press Enter to begin, Esc to quit."""
55
56 def __init__(self, root: TowerDefenceRoot, **kwargs):
57 super().__init__(**kwargs)
58 self._root = root
59 self._blink = 0.0
60
61 # Logo background -- the tutorial ships a logo PNG
62 self.add_child(
63 Sprite2D(
64 texture=str(_PORT_DIR / "assets" / "images" / "gui" / "logo.png"),
65 position=Vec2(WINDOW_WIDTH / 2, WINDOW_HEIGHT / 2 - 40),
66 width=300, height=320,
67 )
68 )
69 self.add_child(
70 Text2D(
71 text="TOWER DEFENCE",
72 x=WINDOW_WIDTH / 2 - 110, y=80,
73 font_scale=2.4,
74 colour=(1.0, 1.0, 1.0, 1.0),
75 )
76 )
77 self.add_child(
78 Text2D(
79 text="SimVX port of russs123/tower_defence_tut",
80 x=WINDOW_WIDTH / 2 - 200, y=130,
81 font_scale=1.0,
82 colour=(0.8, 0.85, 0.9, 1.0),
83 )
84 )
85 self._prompt = self.add_child(
86 Text2D(
87 text="PRESS ENTER OR CLICK TO START",
88 x=WINDOW_WIDTH / 2 - 195, y=WINDOW_HEIGHT - 130,
89 font_scale=1.3,
90 colour=(1.0, 0.95, 0.7, 1.0),
91 )
92 )
93 self.add_child(
94 Text2D(
95 text="Controls: 1 / 2 / 3 = type select - U upgrade - R restart - Q quit",
96 x=WINDOW_WIDTH / 2 - 295, y=WINDOW_HEIGHT - 70,
97 font_scale=0.9,
98 colour=(0.75, 0.78, 0.82, 1.0),
99 )
100 )
101
102 def on_process(self, dt: float) -> None:
103 self._blink += dt
104 self._prompt.colour = (
105 1.0, 0.95, 0.7, 0.55 + 0.45 * (0.5 + 0.5 * (1.0 if int(self._blink * 2) % 2 == 0 else -1.0))
106 )
107 if (
108 Input.is_action_just_pressed("menu_start")
109 or Input.is_mouse_button_just_pressed(MouseButton.LEFT)
110 ):
111 self._root.start_game()
112 if Input.is_action_just_pressed("menu_quit"):
113 self.app.quit()
114
115
116class TowerDefenceRoot(Node2D):
117 """Top-level scene swap: menu -> game -> menu (on restart key)."""
118
119 def __init__(self, **kwargs):
120 super().__init__(**kwargs)
121 self._menu: _Menu | None = None
122 self._world: TowerDefenceWorld | None = None
123 self._show_menu = True
124
125 def on_ready(self) -> None:
126 # ALL InputMap actions live on the root, registered once. Re-entering
127 # the menu and re-starting the world is therefore safe: no double
128 # registration and no actions lost on world destroy.
129 InputMap.add_action("menu_start", [Key.ENTER, Key.SPACE])
130 InputMap.add_action("menu_quit", [Key.ESCAPE, Key.Q])
131 InputMap.add_action("primary", [MouseButton.LEFT])
132 InputMap.add_action("begin_wave", [Key.ENTER, Key.SPACE])
133 InputMap.add_action("fast_forward", [Key.F])
134 InputMap.add_action("place_basic", [Key.KEY_1, Key.B])
135 InputMap.add_action("place_slow", [Key.KEY_2])
136 InputMap.add_action("place_sniper", [Key.KEY_3])
137 InputMap.add_action("cancel_place", [Key.ESCAPE])
138 InputMap.add_action("upgrade", [Key.U])
139 InputMap.add_action("restart", [Key.R])
140 InputMap.add_action("quit", [Key.Q])
141 self._show_menu_screen()
142
143 def _show_menu_screen(self) -> None:
144 for c in list(self.children):
145 c.destroy()
146 self._world = None
147 self._menu = self.add_child(_Menu(self))
148
149 def start_game(self) -> None:
150 for c in list(self.children):
151 c.destroy()
152 self._menu = None
153 self._world = self.add_child(TowerDefenceWorld())
154 self._world.game_lost.connect(self._on_game_end)
155 self._world.game_won.connect(self._on_game_end)
156
157 def _on_game_end(self) -> None:
158 # Stay on the world scene -- restart button cycles state in-place.
159 pass
160
161
162# ---------------------------------------------------------------------------
163# Entry
164# ---------------------------------------------------------------------------
165
166
167def main() -> None:
168 headless = "--test" in sys.argv
169 if headless:
170 from simvx.graphics import save_png
171
172 capture_at = [30, 60, 120]
173 app = App(
174 width=WINDOW_WIDTH, height=WINDOW_HEIGHT,
175 title="Tower Defence (SimVX)", visible=False,
176 )
177 frames = app.run_headless(
178 TowerDefenceRoot(), frames=130, capture_frames=capture_at,
179 )
180 out_dir = _PORT_DIR / "screenshots"
181 out_dir.mkdir(exist_ok=True)
182 for idx, img in zip(capture_at, frames, strict=False):
183 out_path = out_dir / f"frame_{idx}.png"
184 save_png(out_path, img)
185 print(f"saved {out_path}")
186 else:
187 app = App(
188 width=WINDOW_WIDTH, height=WINDOW_HEIGHT,
189 title="Tower Defence (SimVX)",
190 )
191 app.run(TowerDefenceRoot())
192
193
194if __name__ == "__main__":
195 main()