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 (Coding With Russ YouTube tutorial; the upstream repo declares no licence, so nothing from it is redistributed here). Wave-based tower defence with an animated basic turret + four upgrade tiers, plus slow and sniper turret variants added by the port.
Licensing: clean re-implementation under MIT; the level is an original design
and all art is Kenney CC0 or procedural, audio is synthesised. See
ATTRIBUTION.md and LICENSE.
Run¶
All commands from the SimVX repo root (uv workspace requirement):
# Interactive
uv run python examples/ports/tower_defence_tut/main.py
# Headless smoke test (frame_30/60/120 captures)
uv run python examples/ports/tower_defence_tut/main.py --test
# Scripted harness (10 stage screenshots covering 3+ waves)
uv run python examples/ports/tower_defence_tut/harness.py
# Web export
uv run simvx export web examples/ports/tower_defence_tut/main.py -o /tmp/tower_defence.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 (Kenney CC0 parts, baked 8-frame fire cycle) |
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 examples/ports/tower_defence_tut/main.py
11 uv run python examples/ports/tower_defence_tut/main.py --test # headless capture
12 uv run python examples/ports/tower_defence_tut/harness.py # scripted run
13 uv run simvx export web examples/ports/tower_defence_tut/main.py -o /tmp/tower_defence.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_data import ( # noqa: E402
26 WINDOW_HEIGHT,
27 WINDOW_WIDTH,
28)
29from nodes.td_world import TowerDefenceWorld # noqa: E402
30
31from simvx.core import ( # noqa: E402
32 Input,
33 InputMap,
34 Key,
35 MouseButton,
36 Node2D,
37 Sprite2D,
38 Text2D,
39 Vec2,
40)
41from simvx.graphics import App # noqa: E402
42
43# ---------------------------------------------------------------------------
44# Menu / Root wrapper
45# ---------------------------------------------------------------------------
46
47
48class _Menu(Node2D):
49 """Simple menu screen -- press Enter to begin, Esc to quit."""
50
51 def __init__(self, root: TowerDefenceRoot, **kwargs):
52 super().__init__(**kwargs)
53 self._root = root
54 self._blink = 0.0
55
56 # Logo background -- the tutorial ships a logo PNG
57 self.add_child(
58 Sprite2D(
59 texture=str(_PORT_DIR / "assets" / "images" / "gui" / "logo.png"),
60 position=Vec2(WINDOW_WIDTH / 2, WINDOW_HEIGHT / 2 - 40),
61 width=300, height=320,
62 )
63 )
64 self.add_child(
65 Text2D(
66 text="TOWER DEFENCE",
67 position=(WINDOW_WIDTH / 2 - 110, 80), font_scale=2.4,
68 colour=(1.0, 1.0, 1.0, 1.0),
69 )
70 )
71 self.add_child(
72 Text2D(
73 text="SimVX port of russs123/tower_defence_tut",
74 position=(WINDOW_WIDTH / 2 - 200, 130), font_scale=1.0,
75 colour=(0.8, 0.85, 0.9, 1.0),
76 )
77 )
78 self._prompt = self.add_child(
79 Text2D(
80 text="PRESS ENTER OR CLICK TO START",
81 position=(WINDOW_WIDTH / 2 - 195, WINDOW_HEIGHT - 130), font_scale=1.3,
82 colour=(1.0, 0.95, 0.7, 1.0),
83 )
84 )
85 self.add_child(
86 Text2D(
87 text="Controls: 1 / 2 / 3 = type select - U upgrade - R restart - Q quit",
88 position=(WINDOW_WIDTH / 2 - 295, WINDOW_HEIGHT - 70), font_scale=0.9,
89 colour=(0.75, 0.78, 0.82, 1.0),
90 )
91 )
92
93 def on_update(self, dt: float) -> None:
94 self._blink += dt
95 self._prompt.colour = (
96 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))
97 )
98 if (
99 Input.is_action_just_pressed("menu_start")
100 or Input.is_mouse_button_just_pressed(MouseButton.LEFT)
101 ):
102 self._root.start_game()
103 if Input.is_action_just_pressed("menu_quit"):
104 self.app.quit()
105
106
107class TowerDefenceRoot(Node2D):
108 """Top-level scene swap: menu -> game -> menu (on restart key)."""
109
110 def __init__(self, **kwargs):
111 super().__init__(**kwargs)
112 self._menu: _Menu | None = None
113 self._world: TowerDefenceWorld | None = None
114 self._show_menu = True
115
116 def on_ready(self) -> None:
117 # ALL InputMap actions live on the root, registered once. Re-entering
118 # the menu and re-starting the world is therefore safe: no double
119 # registration and no actions lost on world destroy.
120 InputMap.add_action("menu_start", [Key.ENTER, Key.SPACE])
121 InputMap.add_action("menu_quit", [Key.ESCAPE, Key.Q])
122 InputMap.add_action("primary", [MouseButton.LEFT])
123 InputMap.add_action("begin_wave", [Key.ENTER, Key.SPACE])
124 InputMap.add_action("fast_forward", [Key.F])
125 InputMap.add_action("place_basic", [Key.KEY_1, Key.B])
126 InputMap.add_action("place_slow", [Key.KEY_2])
127 InputMap.add_action("place_sniper", [Key.KEY_3])
128 InputMap.add_action("cancel_place", [Key.ESCAPE])
129 InputMap.add_action("upgrade", [Key.U])
130 InputMap.add_action("restart", [Key.R])
131 InputMap.add_action("quit", [Key.Q])
132 self._show_menu_screen()
133
134 def _show_menu_screen(self) -> None:
135 for c in list(self.children):
136 c.destroy()
137 self._world = None
138 self._menu = self.add_child(_Menu(self))
139
140 def start_game(self) -> None:
141 for c in list(self.children):
142 c.destroy()
143 self._menu = None
144 self._world = self.add_child(TowerDefenceWorld())
145 self._world.game_lost.connect(self._on_game_end)
146 self._world.game_won.connect(self._on_game_end)
147
148 def _on_game_end(self) -> None:
149 # Stay on the world scene -- restart button cycles state in-place.
150 pass
151
152
153# ---------------------------------------------------------------------------
154# Entry
155# ---------------------------------------------------------------------------
156
157
158def main() -> None:
159 headless = "--test" in sys.argv
160 if headless:
161 from simvx.graphics import save_png
162
163 capture_at = [30, 60, 120]
164 app = App(
165 width=WINDOW_WIDTH, height=WINDOW_HEIGHT,
166 title="Tower Defence (SimVX)", visible=False,
167 )
168 frames = app.run_headless(
169 TowerDefenceRoot(), frames=130, capture_frames=capture_at,
170 )
171 out_dir = _PORT_DIR / "screenshots"
172 out_dir.mkdir(exist_ok=True)
173 for idx, img in zip(capture_at, frames, strict=False):
174 out_path = out_dir / f"frame_{idx}.png"
175 save_png(out_path, img)
176 print(f"saved {out_path}")
177 else:
178 app = App(
179 width=WINDOW_WIDTH, height=WINDOW_HEIGHT,
180 title="Tower Defence (SimVX)",
181 )
182 app.run(TowerDefenceRoot())
183
184
185if __name__ == "__main__":
186 main()