Your turret is surrounded. You do not aim with the mouse: your Python strategy controls every shot. Improve your code as the battle unfolds and keep your turret alive as long as possible.
Edit live, even mid-match: change your Python in the editor and press RUN to deploy it immediately. You do not need to wait for an upgrade break. Your health, enemies, score, upgrades, and elapsed time are preserved; if the code has a bug, the game pauses until you fix it and press RUN again.
Your mission
Implement decide(state). The game calls it 10 times per simulated second with a fresh dictionary describing the arena. Return one firing command, or None to wait:
return {"target": enemy["id"], "weapon": "rail"}
The runnable starter has deliberately bad targeting: it loops through living enemies in ID order, choosing a different target each decision and wrapping to the beginning. It always tries Pulse, ignoring distance, danger, weaknesses, energy, and cooldowns. This spreads damage and wastes decisions on unavailable shots. The last_target_id variable remembers its place between decisions and resets when you Run again. Replace this logic to improve target priority, weapon selection, energy efficiency, crowd control, and cooldown usage. There is no single perfect solution.
State reference
Use the reference below when you need a field name or want to inspect a nested dictionary. All values are a fresh snapshot of the current game; changing this dictionary does not change the simulation.
The top-level state dictionary
| Key | Python type | Meaning |
|---|
time | number | Active simulated seconds since this attempt started. Pauses and upgrade choices do not advance it. |
wave | int | Current wave number, starting at 1. |
hp | number | Current turret hull. The attempt ends at zero. |
max_hp | number | Maximum turret hull, initially 100; upgrades can increase it. |
energy | number | Current energy available to spend on weapons. |
max_energy | number | Maximum energy, initially 100; upgrades can increase it. |
energy_regen | number | Energy restored per active simulated second, initially 18. |
kills | int | Number of enemies destroyed during this attempt. |
enemies | list[dict] | Currently living enemies. An empty list means there is no target. |
weapons | dict[str, dict] | Four weapon dictionaries: pulse, rail, arc, and cryo. |
upgrades | dict[str, int] | Selected upgrade IDs mapped to selection counts. Unselected IDs are absent. |
“Number” means an int or float; use numeric comparisons rather than relying on the exact type. Distances and speeds use game-world units, not screen pixels, so the same strategy works on mobile and desktop.
state["enemies"]: every field in an enemy dictionary
Use for enemy in state["enemies"]: to inspect living enemies. Do not assume the list is sorted by danger or distance.
| Key | Python type | Meaning |
|---|
id | int | Unique enemy ID for this attempt. Use this value as the returned command's target, not the enemy's list index. |
kind | str | runner, swarm, armored, or shielded. |
hp | number | Remaining enemy health. |
max_hp | number | Health when this enemy spawned, including its wave scaling. |
x | number | Horizontal position relative to the turret: positive is right. |
y | number | Vertical position relative to the turret: positive is down. |
distance | number | Straight-line distance from the turret, equivalent to (x*x + y*y) ** 0.5. |
speed | number | Current movement speed in units per second, including an active Cryo slow. |
eta | number | Estimated seconds until contact: max(0, (distance - 28) / speed). Assumes current speed persists; it does not predict Cryo wearing off. |
damage | number | Hull damage this enemy will inflict if it reaches the turret. |
slowed | bool | Whether Cryo is currently slowing this enemy. |
weakness | str | Suggested weapon name for this enemy type. Use the numeric multipliers and current weapon stats for more precise choices. |
multipliers | dict[str, number] | Four damage multipliers, one for each weapon; explained below. |
nearby | int | Living enemies within the current Arc hop range of this enemy, including itself. A clustering estimate, not a guaranteed chain count. |
enemy["multipliers"]: Pulse, Rail, Arc, and Cryo effectiveness
The keys are pulse, rail, arc, and cryo. A multiplier above 1 means extra damage, below 1 means resistance, and 1 means normal damage.
| Enemy kind | pulse | rail | arc | cryo |
|---|
runner | 1.4 | 1 | 1 | 1 |
swarm | 1 | 0.8 | 1.6 | 1.2 |
armored | 0.3 | 2 | 0.6 | 0.5 |
shielded | 0.5 | 0.35 | 2.3 | 0.8 |
weapon_name = "rail"
weapon = state["weapons"][weapon_name]
damage_to_this_enemy = weapon["damage"] * enemy["multipliers"][weapon_name]
This estimates damage before Arc's additional chain reduction. The first Arc target takes full calculated damage; each later hop multiplies it by another 0.85.
state["weapons"]: common fields and each weapon's extra fields
state["weapons"] is a dictionary of dictionaries, not a list. For example, state["weapons"]["pulse"]["damage"] reads the current Pulse damage, including upgrades.
Every weapon dictionary contains these six fields:
| Key | Python type | Meaning |
|---|
damage | number | Current base damage before the enemy's multiplier and any Arc hop reduction. Damage upgrades are already included. |
cost | number | Energy consumed by a valid shot. Efficiency upgrades are already included. |
range | number | Maximum distance to the selected target, measured from the turret. |
base_cooldown | number | Full cooldown duration after firing, in seconds, with upgrades included. “Base” does not mean the original, unupgraded value. |
cooldown | number | Seconds remaining before this weapon can fire again. 0 means its timer has finished. |
ready | bool | Whether the cooldown has finished AND enough energy is available. It does not check a specific enemy's range. |
Each weapon has its own cooldown. Only one command can be returned per decision, so readiness does not mean all ready weapons fire together.
state["weapons"]["pulse"]: rapid single-target weapon
Pulse has only the six common fields. Its initial dictionary is:
{
"damage": 14,
"cost": 4,
"range": 260,
"base_cooldown": 0.25,
"cooldown": 0,
"ready": True,
}
It has no splash, chain, or piercing fields. Read its current values from state instead of hard-coding the initial stats.
state["weapons"]["rail"]: piercing line weapon
Rail has the six common fields plus line_half_width, a number specifying the beam's half-width in world units. The hit test adds each enemy's own radius to this value.
{
"damage": 68,
"cost": 24,
"range": 330,
"base_cooldown": 1.4,
"cooldown": 0,
"line_half_width": 5,
"ready": True,
}
The chosen target determines the firing direction. Rail can hit other enemies along that line out to its maximum range, including enemies beyond the selected target.
state["weapons"]["arc"]: chaining weapon
Arc has the six common fields plus:
| Key | Python type | Meaning |
|---|
max_targets | int | Maximum total enemies in the chain, including the initial target. |
hop_range | number | Maximum distance from the previous enemy to the next enemy in the chain. This is different from the weapon's turret-to-first-target range. |
{
"damage": 24,
"cost": 20,
"range": 240,
"base_cooldown": 1.05,
"cooldown": 0,
"max_targets": 4,
"hop_range": 85,
"ready": True,
}
The engine chooses the subsequent chain targets. enemy["nearby"] can help you find clusters, but it does not expose the exact chain.
state["weapons"]["cryo"]: area damage and slowing
Cryo has the six common fields plus:
| Key | Python type | Meaning |
|---|
blast_radius | number | Radius around the selected enemy within which enemies take damage and receive the slow. |
slow_duration | number | How many simulated seconds the slow lasts after the hit. |
{
"damage": 9,
"cost": 12,
"range": 240,
"base_cooldown": 1.8,
"cooldown": 0,
"blast_radius": 65,
"slow_duration": 2.2,
"ready": True,
}
Cryo reduces movement speed by 55%, leaving enemies moving at 45% of their normal speed. That percentage is a game rule, not an extra field in the weapon dictionary.
A safe readiness and range check
weapon_name = "pulse"
weapon = state["weapons"][weapon_name]
for enemy in state["enemies"]:
if weapon["ready"] and enemy["distance"] <= weapon["range"]:
return {"target": enemy["id"], "weapon": weapon_name}
return None
This is a valid basic check, not an optimized strategy: it selects the first reachable enemy rather than the most dangerous one. Put this logic inside decide(state).
state["upgrades"]: selected upgrade IDs and stack counts
This dictionary starts empty. After choices it might look like:
{"pulse_damage": 2, "reactor": 1, "hull": 1}
Those values are selection counts, not the resulting damage or hull values. Read the already-updated totals from state["weapons"], state["max_hp"], and the other top-level fields.
| Possible key | What the count records |
|---|
pulse_damage, rail_damage, arc_damage, cryo_damage | Damage amplifier selections for that weapon. |
pulse_cycle, rail_cycle, arc_cycle, cryo_cycle | Cooldown reduction selections for that weapon. |
reactor | Energy regeneration upgrades. |
battery | Maximum energy upgrades. |
efficiency | All-weapon energy cost reductions. |
hull | Maximum hull increases with an immediate repair. |
repair | Field repair selections; these are one-time heals, not ongoing regeneration. |
arc_chain | Arc maximum-target increases. |
arc_reach | Arc hop-range increases. |
cryo_radius | Cryo blast-radius increases. |
cryo_duration | Cryo slow-duration increases. |
rail_width | Rail beam-width increases. |
Use .get() when an upgrade might not have been chosen:
pulse_amplifiers = state["upgrades"].get("pulse_damage", 0)
Upgrade offers are not part of state. Choose a card in the game UI; Python sees its effects in later snapshots.
Inspect an actual snapshot
def decide(state):
print(state["weapons"]["pulse"])
return None
Open the Output tab to see this dictionary. Printing every decision creates ten messages per simulated second, so use it briefly for debugging and remove it when you have the information you need.
Your arsenal
| Weapon | Base damage | Cooldown | Energy | Range | Special behavior |
|---|
pulse | 14 | 0.25 s | 4 | 260 | Cheap, rapid single-target fire |
rail | 68 | 1.4 s | 24 | 330 | Hits every enemy along its firing line, through to maximum range |
arc | 24 | 1.05 s | 20 | 240 | Chains to up to 4 enemies, each next hop within 85 units; damage ×0.85 per hop |
cryo | 9 | 1.8 s | 12 | 240 | Hits all enemies within 65 units of the target; slows them by 55% for 2.2 seconds |
Only one weapon can be selected per decision, but each has an independent cooldown. There is unlimited ammunition, not unlimited energy. Because decisions occur every 0.1 seconds, effective firing intervals round up to a decision tick. All attacks apply damage immediately; visible projectiles and beams are feedback, not extra travel time.
Rail can destroy a wave-one runner as soon as it spawns. The enemy breaks into drifting pieces that retain its incoming momentum. These pieces are visual feedback only: the enemy is already eliminated and is no longer a target.
Know the enemy
The base stats (before wave scaling) and weapon damage multipliers are:
| Enemy | HP | Speed | Contact damage | Pulse | Rail | Arc | Cryo |
|---|
| Runner, pink triangle | 27 | 35 | 9 | 1.4× | 1× | 1× | 1× |
| Swarm, yellow diamond | 17 | 25 | 6 | 1× | 0.8× | 1.6× | 1.2× |
| Armored, orange square | 88 | 17 | 20 | 0.3× | 2× | 0.6× | 0.5× |
| Shielded, violet hexagon | 64 | 23 | 15 | 0.5× | 0.35× | 2.3× | 0.8× |
Swarm enemies arrive in groups of three. Cryo slows every type, including armored and shielded enemies. A hit removes an enemy from the arena and damages the turret; at zero hull, the attempt ends immediately.
Escalation and scoring
There is no final wave. Each wave lasts 20 seconds of active combat, then freezes for an upgrade choice. Remaining enemies stay in place and carry into the next wave. Newly spawned enemies gain 18% compounded HP per wave, 7% of their base speed per wave, and 8% of base contact damage per wave (rounded to an integer).
Wave 1 is a gentle introduction: six runners only, one every 3.5 seconds, starting at 0.5 seconds. Swarms join in wave 2, armored enemies in wave 3, and shielded enemies in wave 4. Spawn rates are 0.50 groups/second in wave 2, 0.74 in wave 3, 0.84 in wave 4, 0.96 in wave 5, and 1.10 in wave 6. From wave 7, the rate increases by 0.18 groups/second per wave. Wave 4 introduces exactly two shielded enemies, about 5.3 and 13.6 seconds into the wave, replacing ordinary spawn groups rather than adding extra enemies. Its other groups use the wave-3 enemy mix; wave 5 begins mixing shields in randomly, and wave 6 uses the full enemy mix. Each new wave's first group arrives 0.5 seconds after its announcement.
A large wave banner announces each transition and any newly introduced enemy type. The countdown shows when the next upgrade break begins. Health does not regenerate automatically, but repair upgrades can restore it. Your score is active survival time: time spent picking upgrades or editing code does not count. SESSION BEST tracks this mounted game session. When the exercise leaderboard is enabled, turret destruction submits survival time once using the scoring ticket created by the platform Run button. The in-game High scores panel displays the server result and public board. Sign in to save an account best; anonymous play does not create an account record. After a ticket is used, press Run before the next scored attempt: Restart alone does not create another ticket.
The enemy seed is 240926, so enemy types and approach directions are repeatable. Entry positions depend on the visible world bounds; compare strategies at the same panel size for consistent conditions. Upgrade offers use a separate randomly generated seed each new run. Changing your code during a break does not reroll the offers. Since different upgrades affect performance, compare both your code and your selected build when judging scores.
Select your upgrade
Survive a wave to receive three distinct random upgrade cards. Pick one card to apply it immediately and start the next wave. You cannot skip the choice, collect two cards, or reroll the cards by pressing Run. The arena, cooldowns, enemies, regeneration, and survival clock stay frozen until you choose.
There are 18 upgrade types:
| Upgrade | Effect per selection | Limit |
|---|
| Pulse / Rail / Arc / Cryo amplifier (4 types) | +15% damage for that weapon, multiplicatively | Unlimited |
| Pulse rapid cycle | −0.1 seconds cooldown, minimum 0.1 seconds | 2 |
| Rail rapid cycle | −0.2 seconds cooldown | 4 |
| Arc rapid cycle | −0.15 seconds cooldown | 4 |
| Cryo rapid cycle | −0.2 seconds cooldown | 5 |
| Reactor tuning | +3 energy regeneration per second | 6 |
| Expanded capacitor | +25 maximum energy and +25 current energy | 4 |
| Efficient discharge | All weapon energy costs ×0.92 | 5 |
| Reinforced hull | +15 maximum hull and repair 15 hull | 6 |
| Field repairs | Repair 35 hull, capped at maximum | Repeatable when damaged |
| Arc conductor | +1 maximum target, up to 8 | 4 |
| Extended discharge | +15 Arc hop range | 4 |
| Cryo dispersal | +12 Cryo blast radius | 4 |
| Deep freeze | +0.4 seconds Cryo slow duration | 4 |
| Wide-bore rail | +4 Rail line half-width | 4 |
Maxed-out upgrades are removed from the offer pool. Field repairs is not offered at full hull. Damage upgrades remain repeatable, so late waves still provide three choices. Open Upgrades in the game UI to inspect your build. The HUD and Python weapon stats reflect applied bonuses. Cooldown changes still obey the 0.1-second decision tick; faster cooldowns cannot create more than one command per decision.
Apply code without restarting
At any time, edit the normal Python editor and press the platform's Run button. Run never resets an existing attempt. During an upgrade break, choose Edit code first if you want to uncover the arena.
The game pauses while compiling and checking the new strategy, preserving enemies, time, health, energy, score, cooldowns, upgrades, and any offered cards. Successful code replaces your decision function immediately and resumes combat if it was running. Python globals reset when code is applied, but the game does not reset. A manually paused game stays paused until Resume; an upgrade break still requires one card selection.
If compilation, validation, or a later Python decision fails, a readable error appears and the game freezes until you fix the code and press Run again. Enemies cannot move or damage you, and time, cooldowns, energy regeneration, and upgrade selection remain frozen. A failed decision rolls back that simulation tick. Failed edits do not replace the last successfully applied strategy, but the game will not automatically fall back to it or let Resume bypass the error.
The engine cannot read unsaved editor changes automatically. Press Run before selecting the card if you want those changes to take effect. In the arena, Restart always starts a fresh attempt using the last successfully applied strategy and a fresh upgrade roll.
Run, inspect, improve
- Press Run to compile your Python and start the first attempt from full hull and energy.
- Watch the current decision, weapon cooldowns, energy bar, and incoming enemies.
- Use the in-game Pause, Stop, and Restart controls below SESSION BEST in the left HUD (along the bottom edge inside the game on narrow screens). Pause lets you inspect the battlefield. Stop freezes it; Run applies code and continues from that state (or returns to a pending upgrade choice).
- Change one part of your strategy and press Run to apply it without losing progress. Use Restart only when you want a fresh attempt. After the turret dies, Run can still apply code, but only Restart starts a new game.
Each weapon has its own sound, with additional cues for kills, hull damage, wave changes, and destruction. Use the page's built-in audio control to mute or unmute; the game follows that setting and has no separate sound button. If the browser blocks audio, unmute in the page UI and click Run or interact with the game to unlock playback.
The battlefield fills the entire available Game panel on desktop, phones, and tablets. Both world axes use the same scale: ranges stay circular, movement is not stretched, and artwork keeps its proportions. Wider panels reveal more horizontal world; taller panels reveal more vertical world. New enemies enter from the visible world edges, with a minimum approach distance on very short panels. Resizing changes future entry points but never teleports existing enemies or restarts the match. Edge arrows point toward enemies outside the current view. Controls stay inside the game; no separate toolbar reduces the playing area. Simulation distances are unchanged. Use the page’s Editor, Game, and Output tabs to switch views on mobile.
If multiple code updates are submitted while Python is loading, only the latest is allowed to start or replace the strategy. Stop cancels pending updates. Background tabs pause simulation; long rendering stalls are clamped instead of suddenly advancing enemies.
A syntactically valid but unavailable shot does not consume energy. The decision strip explains whether the weapon was cooling down, energy was insufficient, or the target was outside range. Invalid return values or Python errors pause the attempt with a readable message until corrected.
Imports and private-attribute access are disabled in this learning environment. Use supplied data and ordinary Python built-ins such as print, type, repr, len, min, max, and range. Normal loops are supported; a traced execution budget catches runaway Python loops. This is not a general-purpose hostile-code sandbox.
This is an endless optimization challenge, not a pass/fail exercise. It deliberately does not trigger a course-completion popup.
Start by selecting the enemy with the smallest eta, not merely the smallest distance. Then match weapons to weaknesses, check range and readiness, and avoid wasting high-damage shots on nearly defeated targets.