The goal of this project is to build navigation algorithm for a 2D rocket navigating an asteroid field.
You will write two main C functions:
- a path generator that returns a collision-free waypoint path
- a controller that steers the rocket along that path
The Python simulator loads your C code, displays the path you generate, and shows how the rocket behaves in motion.
You should work mainly in these files:
- src/trajectory_generator.c: implement the path generator
- src/controller.c: implement the flight controller
- include/rocket_api.h: shared API used by the simulator
The simulator expects these exact function names:
void generate_trajectory(const PlanningProblem *problem, Trajectory *trajectory);
void controller_step(const ControlInput *input, ControlOutput *output);Your work has two parts:
- Generate a path from the start position to the goal while avoiding obstacles.
- Control the rocket so it follows the path and reaches the goal without collision.
In practice:
generate_trajectory()chooses where the rocket should gocontroller_step()chooses how the engines should fire
Required tools:
- Python 3
make- a C compiler such as
ccorgcc
Setup and build:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
make srcThis builds the shared library loaded by the simulator:
src/librocket.so
Rebuild with make src every time you change your C code.
Use plan mode when you want to inspect your path generator:
python -m sim.run --map moving_gate --mode plan --show-safety --show-predictionPlan mode is the best place to debug:
- the waypoint path you returned
- the planner graph you returned
- whether your path goes through obstacles
- whether your path is too long, too sharp, or badly placed
Use flight mode when you want to inspect your controller:
python -m sim.run --map moving_gate --mode flight --show-safety --show-predictionFlight mode lets you observe:
- whether the rocket turns toward the path correctly
- whether it accelerates too much or too little
- whether it overshoots waypoints
- whether it collides with obstacles or walls
- whether your controller remains stable over time
In flight mode, the simulator now calls generate_trajectory() at every simulation step before controller_step(). The Python side no longer decides when to replan. If you want to keep a previous graph or path, do it inside your C code.
A good way to go through this project is:
- Start with
planmode and make sure your path looks reasonable. - Then switch to
flightmode and see whether the rocket can track that path. - Rebuild with
make srcafter every edit. - Improve the planner and controller together.
Your path generator receives a PlanningProblem:
void generate_trajectory(const PlanningProblem *problem, Trajectory *trajectory);Important fields in problem:
| Field | Meaning |
|---|---|
width, height |
map dimensions |
start |
rocket start position |
goal |
final target position |
state.x, state.y |
current rocket position |
state.vx, state.vy |
current rocket velocity |
state.theta |
current rocket heading |
state.omega |
current rocket angular velocity |
state.mass |
current rocket mass |
start_heading |
initial rocket heading |
start_speed |
initial rocket speed |
cruise_speed |
recommended planning speed hint |
rocket_radius |
collision radius of the rocket |
obstacle_count |
number of valid obstacles |
obstacles[i].center |
obstacle position |
obstacles[i].radius |
obstacle radius |
obstacles[i].velocity |
obstacle velocity |
obstacles[i].is_moving |
1 if moving, 0 if static |
Important note:
- for moving scripted obstacles,
is_movingis more reliable thanvelocity - in flight mode,
generate_trajectory()is called every frame, so you can decide insidetrajectory_generator.cwhether to rebuild the graph, update only the path, or reuse cached results
Your function must fill a Trajectory:
| Field | Meaning |
|---|---|
waypoint_count |
number of valid points in the returned path |
waypoints[i] |
ordered path points that the rocket should follow from start toward goal |
graph_node_count |
number of valid nodes in the planner graph |
graph_nodes[i] |
positions of the planner graph nodes |
graph_edge_count |
number of valid graph edges |
graph_edges[i][0..1] |
indices of the two endpoint nodes of each graph edge |
The two parts of the output have different roles.
The waypoints array is the actual path output used by the simulator. It should be read as an ordered polyline:
waypoints[0]should usually be near the current rocket position or the current local planning start- the last waypoint should usually be near the goal
- consecutive waypoints define the path segments the controller will try to follow
In flight mode, the simulator passes this path to controller_step(). That means the quality of the waypoint sequence matters directly: a path with unnecessary zig-zags, sharp turns, or badly spaced points may be valid geometrically but still hard to track.
The graph output is different. graph_nodes and graph_edges are mainly there to expose what your planner considered internally. You can use them to return:
- a roadmap built by
PRM - a search graph explored by
A* - a tree or rewired tree built by
RRTorRRT* - any other node-edge structure that helps explain how the path was produced
Each edge graph_edges[k] = {i, j} means that node i and node j are connected. The simulator draws this graph, so it is useful for debugging sampling coverage, graph connectivity, and search behavior. The controller does not directly use the graph; it only uses the waypoint path.
Basic rules:
- return a valid path from start to goal
- keep waypoints inside the map
- avoid collisions with obstacles
- keep counts within the fixed array limits
- if you return graph edges, they must refer to valid graph nodes
The planner graph is displayed by the simulator, so you can use it to understand what your algorithm explored, while the waypoint list is what the rocket will actually try to track.
Your controller receives a ControlInput:
void controller_step(const ControlInput *input, ControlOutput *output);Important fields in input:
| Field | Meaning |
|---|---|
state.x, state.y |
rocket position |
state.vx, state.vy |
rocket velocity |
state.theta |
rocket heading |
state.omega |
angular velocity |
goal |
final target |
target_waypoint |
current waypoint selected by the simulator |
waypoint_index |
index of the current waypoint |
waypoint_count |
number of valid waypoints |
waypoints[i] |
full path |
obstacle_count |
number of obstacles |
obstacles[i] |
obstacle information |
Your controller must write a ControlOutput:
| Field | Meaning |
|---|---|
main_throttle |
main engine command |
left_booster |
left booster command |
right_booster |
right booster command |
Controller rules:
- output finite values only
- keep commands in the intended range
[0, 1] - remember the controller runs at
60 Hz
The simulator uses these conventions:
- map size is
1200 x 800 xincreases to the rightyincreases downwardtheta = 0means the rocket points upward- distances and speeds are simulator units, not SI units
Start with a planner that produces a path the controller can actually follow, not just a path that is collision-free on paper. In this project, a good path starts near the rocket, ends near the goal, stays inside the map, keeps clearance from obstacles, and avoids unnecessary zig-zags or very sharp turns.
If you want concrete families of methods to study, the most useful names are A*, visibility graphs, PRM, RRT, RRT*, and Hybrid A*. For this simulator, the most practical solutions are often a graph-based planner such as A* or a sampling-based planner such as RRT*, with a cost that balances path length and obstacle clearance.
When you inspect plan mode, focus on just a few questions:
- Does the path begin near
startand end neargoal? - Is every segment collision-free with enough margin for the rocket radius?
- Is the path smooth enough that the controller can turn and follow it?
The controller should first make the rocket point in the right direction, then add thrust once the attitude is acceptable. In other words, you are controlling both heading and forward speed, while trying to avoid oscillations and overshoot. A path that looks good in plan mode may still fail in flight mode if the controller asks for too much thrust while the rocket is misaligned.
Useful controller names to know are PID, PD, Pure Pursuit, Stanley, LQR, and MPC. A strong first implementation for this assignment is usually much simpler than full MPC: you can get good results from a waypoint-following controller built from heading error, angular-rate damping, and thrust reduction when alignment is poor.
When you inspect flight mode, focus on these points:
- Does the rocket turn toward the waypoint before accelerating?
- Does it oscillate, overshoot, or miss waypoints at high speed?
- Does it remain stable when the path bends or when obstacles force rerouting?
The controller is easier to write if you understand the physics in sim/physics.py.
Constants:
| Property | Value |
|---|---|
| collision radius | 18 |
| mass | 1.0 |
| max force per engine | 240 |
| translational force gain | 0.9 |
| linear damping | 0.11 |
| torque gain | 0.135 |
| angular damping | 0.72 |
| goal capture radius | 40 |
With time step
These limits are defined in include/rocket_api.h:
| Symbol | Value |
|---|---|
MAX_OBSTACLES |
128 |
MAX_WAYPOINTS |
256 |
MAX_GRAPH_NODES |
320 |
MAX_GRAPH_EDGES |
2048 |
Make sure your code never writes past these limits.
To start this project a good task ordering is:
- In src/trajectory_generator.c, return a very simple path.
- Run
planmode and confirm the path is visible. - Improve the path so it avoids obstacles.
- In src/controller.c, build a simple waypoint-following controller.
- Run
flightmode and watch how the rocket behaves. - Tune the planner and controller together.
The simulator provides the following environments, listed from easiest to hardest:
empty_debug: empty map for debugging controller alignment, turning, and thrust without obstacles.static_intro: first static planning map with a few large asteroids.narrow_passage: static map with bottlenecks that require a cleaner path.moving_gate: dynamic map with a timed moving opening.patrol_crossing: hardest environment, with several moving obstacles and crossing patrol patterns.
Build:
make srcShow the generated path:
python -m sim.run --map empty_debug --mode plan --show-safety --show-predictionShow the rocket behavior in flight:
python -m sim.run --map empty_debug --mode flight --show-safety --show-predictionTry a static obstacle map:
python -m sim.run --map static_intro --mode flight --show-safety --show-predictionTry the hardest dynamic map:
python -m sim.run --map patrol_crossing --mode flight --show-safety --show-prediction