Your spacecraft stays in the middle of the view while the world scrolls around it. Write its Python flight controller to reach five navigation beacons through a field of debris.
Run applies your edited code without restarting the mission. Pause whenever you need time to inspect the sensors or change your logic. Your position, velocity, hull, destroyed obstacles, and checkpoint progress are preserved.
Your mission
Implement pilot(state). It runs ten times per simulated second, receiving a fresh dictionary of sensor inputs and navigation information. Return a command dictionary; there are no keyboard flight controls.
The starter runs, but its full-power steering ignores momentum and obstacles. Improve steering first, then add braking, laser targeting, and avoidance. This is an intermediate exercise using dictionaries, functions, loops, and coordinate math.
Reach all five checkpoints before 240 seconds of active simulation time elapse. Keep the ship's center inside a checkpoint's outer ring (78-unit radius) for three continuous seconds. Its circular loader starts as soon as you enter, even while braking, and resets if you leave. Completion also requires speed of at most 55 units/second; an amber loader and BRAKE message warn when you are too fast. A full loader waits for safe speed while you remain inside. Merely flying through at full speed does not count.
Flight commands
return {
"left": 0.0,
"right": 0.6,
"up": 0.0,
"down": 0.0,
"laser_angle": 315.0,
"fire": True,
}
left, right, up, down: numbers from 0.0 through 1.0, inclusive. These names describe the direction the ship accelerates, not the side emitting exhaust. Missing thrusters default to zero.
laser_angle: degrees from 0 through 360, inclusive. Zero points right, 90 down, 180 left, 270 up; 360 equals zero. Angles increase clockwise. The cannon rotates independently of the ship.
fire: True or False. A held fire command shoots whenever cooldown allows. Missing fire defaults to False; missing angle defaults to zero.
Thrusters provide acceleration, not instant movement. Velocity persists after power is removed, with gentle stabilization drag. Opposing thrust brakes the ship. Each axis supplies 110 units/second² before drag, drag multiplies velocity by exp(-0.3 * dt), and total speed is capped at 150 units/second. This is an arcade flight model, not an exact spaceflight simulation.
Exhaust exits opposite the acceleration direction. Power up to and including 0.2 produces small white gas particles; higher power produces a growing orange flame with a bright core, reaching full afterburner at 1.0. No power means no exhaust.
State reference
| Field | Meaning |
|---|
state["position"] | {"x": ..., "y": ...} in world coordinates; starts at (0, 0) |
state["velocity"] | {"x": ..., "y": ...} in units/second |
state["next_checkpoint"] | Target x, y, one-based index, radius (78), max_speed (55), hold_seconds (3), hold_elapsed, and progress (0–1) |
state["sensors"] | Twelve named ray readings, described below |
state["sensor_range"] | 300 world units |
state["hull"] | Hull health, initially 100 |
state["damage_dealt"] | Total HP actually removed from destructible asteroids/debris this attempt |
state["time"] | Active simulated seconds |
state["checkpoints_reached"] | Completed checkpoints, initially 0 |
state["laser"] | ready, remaining cooldown in seconds, and range (380) |
state["dt"] | 0.1 seconds between Python decisions |
Positive X is right; positive Y is down. The camera does not change world coordinates. math is already available, so do not import it.
dx = state["next_checkpoint"]["x"] - state["position"]["x"]
dy = state["next_checkpoint"]["y"] - state["position"]["y"]
angle = math.degrees(math.atan2(dy, dx)) % 360
distance = math.hypot(dx, dy)
Approach and braking
Your current motion is always available as state["velocity"]["x"] and state["velocity"]["y"]. These are signed world-axis velocities, not thruster settings: positive X means moving right, and negative Y means moving up. Total speed is the length of that velocity vector, calculated with math.hypot(vx, vy). The HUD shows total speed, and Show Sensors draws the velocity vector and both components.
Choose a desired velocity toward the checkpoint, then compare it with the actual velocity. Far away you can travel quickly; as distance shrinks, reduce your desired speed and fire against any excess momentum. For example, a craft still moving right needs leftward thrust to slow down, even if its destination is also to the right. Cutting thrust alone does not stop it immediately.
Start braking before the outer ring rather than waiting to cross it. As a rough starting estimate, stopping distance grows with speed squared: speed * speed / (2 * braking_acceleration). The game's drag, diagonal thrust, and moving obstacles make this an estimate rather than a guaranteed rule, so include a margin. Once inside, use small corrections to remain within the ring until the three-second loader finishes at safe speed.
Twelve ray-cast sensors
There are 12 evenly spaced rays, one every 30 degrees. The original six names and angles are unchanged; the six new rays fill the gaps. Names are directional shorthand: use the exact angle field for calculations.
| Sensor name | Angle | Sensor abbreviation |
|---|
east | 0° | E |
east_southeast | 30° | ESE |
southeast | 60° | SE |
south | 90° | S |
southwest | 120° | SW |
west_southwest | 150° | WSW |
west | 180° | W |
west_northwest | 210° | WNW |
northwest | 240° | NW |
north | 270° | N |
northeast | 300° | NE |
east_northeast | 330° | ENE |
Each reading contains angle, distance, and hit. Distance is measured from the ship's center to the first obstacle surface on that exact ray. A clear ray reports distance == 300 and hit is None.
When an obstacle is detected, hit contains id, world x, world y, collision radius, remaining hp, kind, and destructible. It also exposes vx and vy in world units/second, rotation in clockwise degrees, and angular_velocity in degrees/second. Indestructible obstacles have hp is None. Multiple rays can see the same object: use its id to avoid counting it twice.
for sensor in state["sensors"].values():
obstacle = sensor["hit"]
if obstacle is not None and obstacle["destructible"]:
# Calculate an angle to this obstacle's center.
pass
These are thin rays, not a complete obstacle map. Hazards can lie between rays, so a clear reading does not guarantee a safe corridor. The ship has an 18-unit collision radius: allow margin around both the ship and detected obstacles. The minimap shows the mission route for you, but is not additional Python sensor data.
Obstacles and laser
- Brown rock: one laser hit to destroy.
- Blue-gray armored debris with amber strips: three hits to destroy. Small bars beneath it show remaining hits.
- Hexagonal alloy with a red cross: indestructible; navigate around it.
All obstacles slowly drift back and forth along their own fixed path, up to 22–36 units from their starting position. Their peak drift speeds range from 3 to 9 units/second, and they rotate clockwise or counterclockwise at 3–9 degrees/second. This bounded, deterministic motion keeps the field active without letting the mission route empty out or become randomly blocked.
Sensors, laser hits, and ship collisions use the obstacles' current world positions. Rotation is visual: collision shapes remain circles of the exposed radius, so turning a sprite does not unpredictably change its collision boundary. Obstacles do not collide with each other.
For avoidance, you can estimate a near-future position with x + vx * seconds and y + vy * seconds. That is a short-term estimate, not an exact prediction, because drift gradually reverses. The laser hits instantly, so aim at the obstacle's current position rather than leading the shot.
Laser shots instantly hit the nearest obstacle on their line, up to 380 units away. They do not pass through debris or alloy. Each hit deals one damage; the cannon has a 0.28-second cooldown, resolved on the 0.05-second physics clock. Firing while cooling down has no effect. Ammunition is unlimited.
Aiming and firing: the idea
Pick a detected destructible obstacle that threatens your route. Subtract the ship's world position from the obstacle's current world position to get the target direction, then convert that direction to a clockwise angle. math.atan2(vertical_difference, horizontal_difference) handles all quadrants; it returns radians, so convert to degrees and wrap into 0–360. The same angle convention shown above applies to the laser: right is 0°, down is 90°, left is 180°, and up is 270°.
Return that angle in laser_angle, and set fire to True when you want a shot. You can check state["laser"]["ready"] first, or keep requesting fire while cooldown runs. Aim and firing are independent: changing the angle alone never shoots. The beam is instantaneous, so do not lead a moving target. It hits the first obstacle along that angle, meaning alloy or another rock can block the target. Armored debris requires repeated hits, and the ship may need to brake while clearing it.
Avoidance: the idea
React to hazards along the direction you are actually moving, not just the direction of the checkpoint. Use detected obstacle positions, radii, and your velocity to judge whether your current path will come too close. Treat each obstacle as larger than its drawn circle by adding the ship's 18-unit radius and an extra safety margin. Several rays can report one object, so reason about unique obstacle IDs rather than treating every ray hit as a different threat.
For a near-future collision check, compare relative position with relative velocity: obstacle velocity minus ship velocity. A nearby object moving across your route may become dangerous even when it is not directly ahead now. Prediction should cover enough time to brake, but short predictions are more reliable because debris drift reverses gradually.
Brake before making a sharp avoidance turn. For destructible debris you can stop short, aim, and clear the path; for alloy, choose a side and steer around it with clearance, then return toward the checkpoint. Avoid rapidly switching sides on consecutive decisions. A clear sensor ray is only a thin line, not proof that a ship-width corridor is clear, so keep checking as you move. You choose the thresholds and steering algorithm.
Every damaging collision removes 16 hull, deals one damage to destructible debris, and bounces the ship away. A rock breaks on impact; armored debris can survive and loses one remaining-hit bar. Alloy stays indestructible. A shared 0.8-second damage cooldown prevents a single contact from draining hull or obstacle health every frame; lingering contact can damage again after the cooldown. Collision damage counts toward asteroid damage just like laser damage, but ramming risks losing the mission and its entire score.
Zero hull ends the mission immediately. The camera locks at the loss location, the ship breaks into spinning fragments, and those fragments inherit its incoming velocity rather than stopping in midair. A brief explosion plays before the failure panel appears. This is a visual aftermath only: mission time, scoring, collisions, and Python execution have already stopped.
Show Sensors, pause, and live editing
- Show Sensors: toggle all 12 sensor rays, names and distances, the ship collision circle, a blue ship velocity vector, small obstacle velocity vectors, and an amber target direction. Amber rays indicate detected obstacles; mint rays indicate no hit. Labels stay on a stable ring and use the abbreviations above in compact panels. Helpers remain visible while paused.
- Pause / Resume: freeze and resume ship movement, obstacle drift and rotation, laser cooldowns, checkpoint dwell, and mission time. Audio is silenced while paused.
- Run: compile and validate new Python. Running missions resume with the new controller; manually paused missions stay paused until Resume. Python globals reset on each successful upload.
- Restart: begin a new mission with the last successfully applied code. It resets all mission state and Python globals, using the same map seed.
Invalid Python or commands freeze the mission and show an error. Fix the code and press Run; Resume cannot bypass an unresolved error. A failed edit does not replace your last working code. Run after success, destruction, or timeout can update the controller, but only Restart creates a new attempt.
Rapid Run requests use the latest submitted code. Background tabs pause simulation, and long rendering stalls are clamped rather than fast-forwarding into a collision. The preview's 2× speed control changes display speed, not the physics or scoring rules.
Subtle audio feedback
Quiet synthesized effects accompany thruster power, laser shots, debris destruction, hull impacts, checkpoints, success, and mission failure. There is no music. These are optional game feedback, not realistic sound propagation in a vacuum.
Use the page's existing sound control to mute or unmute; the game does not add another mute button inside the canvas. The standalone preview provides an equivalent control in its page toolbar. If autoplay is blocked, click Run or another page control to unlock audio.
Muting suppresses new effects and stops active ones. Pause, code compilation, Python errors, background tabs, and disposal silence the game; resuming starts only current thruster audio and new events, never a backlog. A missing or unavailable audio device does not prevent play.
Flight score and leaderboard
Complete all five checkpoints before the 240-second limit to earn a Flight score. The score rewards two things: finishing quickly and removing HP from destructible asteroids and armored debris. Higher is better.
time_bonus = round(max(0, 240 - completion_seconds) * 100)
damage_bonus = asteroid_hp_removed * 100
flight_score = time_bonus + damage_bonus
Each second saved is worth 100 points, and each asteroid HP removed is worth 100 points. Removing all three HP from armored debris therefore earns 300 damage points; partially damaging armor still counts. Both laser and collision damage count, but misses, shots blocked by indestructible alloy, and overkill do not. A destroyed obstacle cannot give points again.
For example, finishing in 80 seconds with 12 HP of asteroid damage gives 16,000 time points + 1,200 damage points = 17,200. Detouring for one extra HP is worthwhile only if it costs less than about one second. Paused time and code-editing time do not count toward completion time; the score uses active simulation time, not wall-clock time.
You must finish: destruction or timeout scores zero, regardless of damage dealt. This prevents an early crash from earning a better speed score than completing the challenge. Hull and thruster usage no longer directly add or subtract points, though staying alive remains essential. The HUD shows damage dealt, and the terminal result shows the score breakdown.
The enabled per-exercise leaderboard records the engine-calculated Flight score once when the attempt ends. It is separate from the site's XP leaderboard; notifyWin() still signals exercise completion only on success. Use the in-game High scores button for your result and the public top five, including in fullscreen.
Only the platform Run button creates a leaderboard ticket. Applying code mid-flight never submits a score. After a finished attempt, press Run and then Restart for another scored flight; Restart alone cannot create a new ticket. Server responses determine whether a personal best was saved or improved and whether it is publicly eligible. Signing in is required to save a best, and private results never display a public rank. The platform's existing three-second minimum Run age remains in effect.
Suggested approach
Start with a desired velocity toward the checkpoint. Reduce it as you approach, then compare it with actual velocity to choose acceleration or braking. Clamp each thruster to the valid range rather than returning negative power.
Next, inspect detected obstacles in the direction of travel. Brake and shoot destructible debris; steer around alloy. There is no required single algorithm, and even twelve thin rays leave blind sectors worth experimenting with.
Normal loops, helper functions, and built-ins such as min, max, sorted, sum, range, and enumerate are supported. Imports and private attributes are disabled; a traced execution budget catches runaway Python loops. This is an educational guardrail, not a general-purpose hostile-code sandbox. Code runs locally in the browser using Python.