GitHub - jackdoe/roblox-python-tower-defense: save the core by writing python!

8 min read Original article ↗

PythonTD

NB: 90% written by AI, nudged by jackdoe; run with rojo serve Tower defense with programmable units.

while True:
    enemies = self.scan()
    if len(enemies) > 0:
        self.set_target(nearest(enemies))
        self.fire(BULLET)

b a

Ammo Types

Gundams use ammo from their magazine. Player weapons cost scrap (half gundam cost, half damage).

Gundam Weapons

Name Dmg Ammo Rate Notes
BULLET 15 1 0.2s Fast
ROCKET 80 4 1.0s Splash, applies OILED
LASER 40 2 0.1s Burns
ICE 20 2 0.4s 60% slow
GRENADE 60 5 1.5s Splash

Player Weapons

Name Dmg Scrap Rate
BULLET 7 1S 0.3s
ROCKET 40 2S 1.0s
LASER 20 1S 0.5s
ICE 10 1S 0.4s
GRENADE 30 2S 1.5s

Player ROCKET applies OILED. Both ICE weapons freeze (60% slow).

Debuffs

Debuff Effect
SLOW Enemy moves at reduced speed. New slow replaces old.
OILED Coated in oil for 5 seconds. Amplifies SHOCK damage 2x.
BURNING Takes 5 damage every 0.5s for 3s (30 total). From LASER.
FROZEN Slowed by 50% or more. ROCKET shatters for 1.5x damage.

Combos

Bots can SHOCK enemies to trigger powerful combos:

Combo Effect
SHOCK + OILED 2x damage, explosion visual, clears oil
SHOCK + BURNING 1.5x damage, chains lightning to 3 nearby (30% dmg)
SHOCK + SLOW (1 + slowAmount)x damage (60% slow = 1.6x)
ROCKET + FROZEN SHATTER: 1.5x damage, clears ice (needs 50%+ slow)

Combos stack MULTIPLICATIVELY! Order: cargo -> oiled -> burning -> slow

Example: 5 cargo + oiled + burning + 60% slow on 30 base shock:

30 × 7.6 (cargo) × 2 (oil) × 1.5 (burn) × 1.6 (slow) = 1094 damage!
  • Chain lightning range: 15 units from target
  • Bot stuns for 2s after shocking (vulnerable!)

Types

Type Description
Enemy A single enemy. Has hp, pos, is_boss, etc.
[Enemy] A list of enemies. Use len() to count, [0] to get first.
[x, z] A position. Two numbers: x (left/right), z (forward/back).
AmmoType One of: BULLET, ROCKET, LASER, ICE, GRENADE
number A whole or decimal number like 10, 3.5, -20
bool True or False

Objects: Self and Others

Each unit (Gundam, Bot, Player) is an OBJECT with data and actions.

Self - Your Unit

self refers to the unit running the code:

self.pos                 # read your position
self.carrying            # read how much scrap you have
self.scan()              # perform a scan action
self.fire(BULLET)        # perform a fire action

Other Units - G1, G2, B1, B2, etc.

You can read data from other units on your team:

B1.pos                   # where is Bot 1?
B1.carrying              # how much scrap does Bot 1 have?
G3.ammo                  # how much ammo does Gundam 3 have?

Properties vs Methods

Properties are data you READ (no parentheses):

self.pos                 # [x, z] position
self.carrying            # number of scrap
self.ammo                # current ammo (Gundam)
self.max_ammo            # max ammo capacity (Gundam)
self.max_capacity        # max scrap capacity (Bot/Player)
enemy.hp                 # enemy health
enemy.is_boss            # True/False

Methods are actions you DO (use parentheses):

self.scan()              # returns list of enemies
self.fire(BULLET)        # shoots, returns True/False
self.collect()           # picks up scrap
self.teleport([0, 0])    # moves to position
self.set_target(enemy)   # aims at enemy

Example - Coordinated Attack

# Bot checks if Gundam needs help
if G1.ammo < 50 and self.carrying > 0:
    self.say("G1 low on ammo!")
    self.teleport([0, 0])
    self.deposit()

Bot Abilities

Method/Property Returns Description
self.collect() bool Finds nearest scrap cluster and moves to collect it.
self.deposit() bool Deposits carried scrap at core. Must be within 15 units.
self.shock(enemy) bool Teleports to enemy and zaps for 30 base damage. Triggers combos! Damage scales with cargo: 30 × 1.5^cargo. Consumes HALF cargo (rounded up).
self.teleport([x, z]) bool Instantly moves to position. [0,0] is the core.
self.explode() bool Sacrifices 10+ cargo for massive AOE. Bot is destroyed.
self.forward(n) bool Walks forward n units in facing direction.
self.backward(n) bool Walks backward n units.
self.left(degrees) bool Rotates left. Default 90 degrees.
self.right(degrees) bool Rotates right. Default 90 degrees.
self.scan() [Enemy] Returns list of enemies within range 50.
self.say("message") nothing Shows a speech bubble above bot for 3 seconds.
self.carrying number How much scrap bot is carrying.
self.max_capacity number Maximum scrap bot can carry (10).
self.pos [x, z] Current position relative to core.
self.hacker Player Access your owner's player. Use self.hacker.target() etc.

Shock damage examples: 5 cargo = 228 dmg, 10 cargo = 1732 dmg!

Gundam Abilities

Method/Property Returns Description
self.fire(AMMO) bool [BLOCKING] Fires at current target. Waits for cooldown (scales with CPU).
self.scan() [Enemy] Returns list of enemies within range.
self.set_target(enemy) nothing Locks aim onto an enemy. Required before fire().
self.target() Enemy/None Returns currently targeted enemy, or None.
self.set_range(n) nothing Sets base scan/attack range (10-60). Less range = more damage (40/range). Power upgrades extend effective range (+20% per power).
self.reload() number Reloads ammo from scrap (2S per ammo). Returns amount reloaded.
self.ammo number Current ammo in magazine.
self.max_ammo number Maximum ammo capacity (300).
self.hacker Player Access your owner's player. Use self.hacker.target() etc.
self.pos [x, z] Current position relative to core.

Player Abilities

Method/Property Returns Description
self.fire(AMMO) bool [BLOCKING] Fire at target. Costs scrap (see table). Waits for cooldown (scales with CPU).
self.scan() [Enemy] Returns enemies within range 50.
self.set_target(enemy) nothing Lock aim onto an enemy.
self.collect() bool Picks up scrap within range 8.
self.teleport([x, z]) bool Instantly moves to position.
self.deposit() bool Deposits scrap at core. Must be within 15 units.
self.drop(n) bool Drops n scrap pieces (costs n×10 from wallet). Share with team!
self.explode() bool AOE attack that consumes carried scrap.
self.carrying number How much scrap player is carrying.
self.max_capacity number Maximum scrap player can carry (10).
self.hacker Player Returns yourself. Try self.hacker.hacker.hacker.target()!
self.pos [x, z] Current position relative to core.

Selectors

These pick ONE enemy from a list:

Function Description
nearest([Enemy]) Returns enemy closest to you.
furthest([Enemy]) Returns enemy farthest from you.
weakest([Enemy]) Returns enemy with lowest HP.
strongest([Enemy]) Returns enemy with highest HP.

Example:

enemies = self.scan()          # get list of enemies
target = nearest(enemies)      # pick the closest one
self.set_target(target)        # aim at it

Enemy Properties

When you scan, you get Enemy objects:

Property Type Description
enemy.hp number Current health
enemy.max_hp number Maximum health
enemy.pos [x, z] Position
enemy.is_boss bool Is it a boss?
enemy.burning bool Is it on fire?
enemy.frozen bool Is it frozen (50%+ slow)?
enemy.slowed bool Is it slowed at all?
enemy.oiled bool Is it oiled?

Global Variables

Variable Description
CORE.hp Current core health
CORE.pos Core position [0, 0]
G1, G2, ... G10 Access other gundams by ID
B1, B2, B3, ... Access bots by ID

How Python Works

Your code goes through 3 stages before running:

  1. LEXER - Reads your text and breaks it into tokens. if x > 5: becomes: [IF] [x] [>] [5] [:]. Tracks indentation for Python blocks.

  2. COMPILER - Converts tokens into bytecode instructions. x = 5 becomes: LOAD_CONST 5, STORE_VAR x. Checks for errors like undefined variables.

  3. VIRTUAL MACHINE (VM) - Executes bytecode one instruction at a time. Uses a stack to compute values. Each CPU tick runs multiple instructions.

CPU, RAM, and Power

Each unit has CPU speed (Hz), RAM (bytes), and Power multiplier.

CPU (Speed)

  • Base: 1 Hz
  • Each upgrade: +2 Hz
  • Cost: 200 scrap, then x2 per upgrade
Upgrade Cost
1 Hz -> 3 Hz 200 scrap
3 Hz -> 5 Hz 400 scrap
5 Hz -> 7 Hz 800 scrap

Higher Hz = your program runs faster AND you fire faster!

  • Fire rate scales with CPU: actualRate = baseRate / cpuHz
  • At 5 Hz with BULLET (0.2s): fires every 0.04s instead of 0.2s!
  • Bot movement speed also scales: baseSpeed + (cpuHz - 1) × 4

fire() is BLOCKING - your program waits for the cooldown.

RAM (Program Size)

  • Base: 32 bytes
  • Each upgrade: +32 bytes
  • Cost: 75 scrap, then x1.5 per upgrade
Upgrade Cost
32B -> 64B 75 scrap
64B -> 96B 112 scrap
96B -> 128B 168 scrap

RAM limits your program size (bytecode instructions).

  • Simple loop = ~15 bytes
  • Full AI = ~60+ bytes

Power (Damage Multiplier)

  • Base: x1
  • Each upgrade: +1x
  • Cost: 200 scrap, then x2 per upgrade
Upgrade Cost
x1 -> x2 200 scrap
x2 -> x3 400 scrap
x3 -> x4 800 scrap

Power multiplies all damage dealt by the unit. Also extends effective range (+20% per power level).

Economy

  • Starting scrap: 1000
  • Each player has their own scrap balance (not shared!)
  • Scrap drops from killed enemies
  • Your bots deposit scrap to YOUR balance
  • Use self.drop(n) to share scrap with teammates
  • Each scrap piece = 10 scrap currency
  • Damage scoreboard shows who's contributing!

Costs

Item Cost
Gundam 100 scrap base (×1.5 per gundam)
Bot 75 scrap base (×1.3 per bot, unlimited)
CPU/RAM/Power upgrades See tables above
Player weapons 1-2S per shot (see table)

First Challenge

The default gundam program fires but NEVER RELOADS!

while True:
    enemies = self.scan()
    if len(enemies) > 0:
        self.set_target(nearest(enemies))
        self.fire(BULLET)

Your first task: add reload logic. But there's a catch...

Base RAM (32 bytes) is NOT enough for a full reload program!

You must choose wisely:

  • Write minimal code that fits in 32 bytes
  • Or upgrade RAM first (costs scrap)
  • Or accept running dry and reloading manually

This is the machinist's dilemma: code efficiency vs upgrades.

Tips

  • ICE enemies before ROCKET for shatter bonus (freezes at 50%+ slow)
  • SHOCK oiled enemies for 2x damage + explosion
  • SHOCK burning enemies for 1.5x + chain lightning
  • Stack debuffs before SHOCK for multiplicative damage!
  • SHOCK teleports bot to target, then consumes all cargo
  • Bots can teleport([0,0]) to instantly return to core
  • Lower range = more damage (40/range). Power boosts both damage and range.
  • Check enemy.is_boss to prioritize threats
  • Use B1.carrying to coordinate between units