AstroBots is a simple space-combat sandbox where each ship is controlled by a tiny domain-specific language (DSL). You “program” a ship by writing a SetupShip() function that emits a sequence of opcodes (instructions). Every turn, the game runs that opcode sequence to decide the ship’s actions.
The core DSL and interpreter live in:
classes/AstroBots.h(the user-facing DSL macros)classes/AstroBots.cpp(the VM/interpreter)classes/AstroTypes.handclasses/AstroArena.h/.cpp(opcodes, costs, and gameplay rules)
- Arena: a (2048 \times 2048) world that wraps at the edges (a torus).
- Ships: start in a circle, each with HP and fuel.
- Asteroids: drift around. Shooting/breaking them can indirectly help you survive (fuel mechanics).
- Win condition: the simulation ends when there is one ship left alive, or it reaches the turn limit.
Your ship code does not directly set position. Instead it uses actions like thrust, turning, scanning, and firing weapons.
Each ship implements:
int SetupShip()to build itscode(astd::vector<int>)Finalize()appendsENDand returns the script cost
The game logs script cost X/30 for each ship at startup.
Each turn:
- The arena resets certain per-turn state (cooldowns tick down, scan results are cleared).
- Every alive ship runs its program from the start (opcode 0) until
END.
Important consequences:
- Scan results do not persist:
scan_hitis cleared at the start of every turn. If you want to react to what you “see”, you generally mustSCAN()earlier in the same turn. - Signals do not persist:
signalis reset each turn (it is currently more of a hook/visual than a full communication system).
In this codebase, the number 30 is the maximum script cost budget (ASTRO_MAX_SCRIPT_COST = 30), not the number of opcode types.
- Each DSL action has a cost (for example,
THRUSTcosts 2,FIRE_PHOTONcosts 4). - Conditionals and flow-control are part of the bytecode but are not charged as action cost by the DSL macros.
So when people say “30 total opcodes”, what the engine enforces is: your SetupShip() program should cost (\le 30).
You write C++ that uses the DSL macros inside SetupShip():
int MyShip::SetupShip() {
SCAN();
IF_SEEN() {
TURN_TO_SCAN();
IF_SCAN_LE(500) {
IF_SHIP_CAN_FIRE_PHASER() { FIRE_PHASER(); }
IF_SHIP_CAN_FIRE_PHOTON() { FIRE_PHOTON(); }
}
THRUST(2);
} ELSE() {
THRUST(4);
}
return Finalize();
}The interpreter is a small switch statement in ShipBase::Run(). The opcodes are defined in classes/AstroTypes.h.
-
WAIT(ASTRO_OP_WAIT)- Does nothing.
- DSL macro:
WAIT_()
-
THRUST power(ASTRO_OP_THRUST, parameter)- Accelerates the ship forward in the direction it is currently facing.
- DSL macro:
THRUST(P)wherePis stored as(int)(P * 10)and reconstructed asP/10.0at runtime. - Gameplay notes:
- Fuel cost:
power * THRUST_FUEL_COST(seeclasses/AstroTypes.h). - If fuel is low/empty, thrust is reduced (never fully zeroed).
- Velocity is clamped to
MAX_VELOCITY.
- Fuel cost:
-
TURN_DEG degrees(ASTRO_OP_TURN_DEG, parameter)- Sets your ship’s target heading (absolute degrees 0–360), not a relative “turn by”.
- The ship rotates toward
targetAngleatROTATION_SPEEDdegrees per turn. - DSL macro:
TURN_DEG(D)
-
FIRE_PHASER(ASTRO_OP_FIRE_PHASER)- Fires a hitscan ray out to
PHASER_RANGE. - If it hits a ship: deals
PHASER_DAMAGEand can destroy the ship at 0 HP. - If it hits an asteroid: breaks/damages it and grants a small fuel reward.
- Has a cooldown (
PHASER_COOLDOWNturns). - DSL macro:
FIRE_PHASER()
- Fires a hitscan ray out to
-
FIRE_PHOTON(ASTRO_OP_FIRE_PHOTON)- Spawns a photon torpedo moving forward at
PHOTON_SPEED(plus your current ship velocity). - Torpedoes have
PHOTON_LIFETIMEand dealPHOTON_DAMAGEon hit. - Has a cooldown (
PHOTON_COOLDOWNturns). - DSL macro:
FIRE_PHOTON()
- Spawns a photon torpedo moving forward at
-
SCAN(ASTRO_OP_SCAN)- Finds the closest ship or asteroid within
ASTRO_SCAN_RANGE. - Sets:
scan_hit(boolean)scan_dist(distance to target)scan_angle(absolute angle to target, 0–360)
- DSL macro:
SCAN()
- Finds the closest ship or asteroid within
-
SIGNAL value(ASTRO_OP_SIGNAL, parameter)- Sets the ship’s
signalvalue for this turn and records its position intosignals. - Currently, scanning does not use signals; think of this as a minimal communication/visual hook.
- DSL macro:
SIGNAL(V)
- Sets the ship’s
-
TURN_TO_SCAN(ASTRO_OP_TURN_TO_SCAN)- If
scan_hitis true, setstargetAngle = scan_angle. - DSL macro:
TURN_TO_SCAN()
- If
Conditions do not directly branch; they just compute a boolean flag. Branching is done by JUMP_IF_FALSE (which is inserted automatically by the IF_* DSL macros).
-
IF_SEEN(ASTRO_OP_IF_SEEN)flag = scan_hit- DSL macro:
IF_SEEN() { ... }
-
IF_SCAN_LE range(ASTRO_OP_IF_SCAN_LE, parameter)flag = (scan_hit && scan_dist <= range)- DSL macro:
IF_SCAN_LE(R) { ... }
-
IF_DAMAGED(ASTRO_OP_IF_DAMAGED)flag = (hp < ASTRO_START_HP)- DSL macro:
IF_SHIP_DAMAGED() { ... }
-
IF_HP_LE hp(ASTRO_OP_IF_HP_LE, parameter)flag = (hp <= value)- DSL macro:
IF_SHIP_HP_LE(N) { ... }
-
IF_FUEL_LE fuel(ASTRO_OP_IF_FUEL_LE, parameter)flag = (fuel <= value)- DSL macro:
IF_SHIP_FUEL_LE(N) { ... }
-
IF_CAN_FIRE_PHASER(ASTRO_OP_IF_CAN_FIRE_PHASER)flag = (phaser_cooldown == 0)- DSL macro:
IF_SHIP_CAN_FIRE_PHASER() { ... }
-
IF_CAN_FIRE_PHOTON(ASTRO_OP_IF_CAN_FIRE_PHOTON)flag = (photon_cooldown == 0)- DSL macro:
IF_SHIP_CAN_FIRE_PHOTON() { ... }
These are primarily emitted by the IF_* / ELSE() DSL helpers:
-
JUMP target(ASTRO_OP_JUMP, parameter)- Unconditional jump (sets the program counter to
target).
- Unconditional jump (sets the program counter to
-
JUMP_IF_FALSE target(ASTRO_OP_JUMP_IF_FALSE, parameter)- If the current
flagis false, jump totarget.
- If the current
-
END(ASTRO_OP_END)- Stops execution of the ship program for the current turn.
Action costs are defined in classes/AstroTypes.h:
WAIT: 0TURN_DEG: 1SCAN: 1SIGNAL: 1THRUST: 2FIRE_PHASER: 3FIRE_PHOTON: 4
These costs add up during SetupShip() and are logged. If your ship exceeds the 30-point budget, it will still run, but the log will mark it as exceeding the limit.
- Always scan before reacting:
SCAN()early, then useIF_SEEN()/IF_SCAN_LE(...). - Turn is smooth:
TURN_DEGandTURN_TO_SCANset a target angle; rotation takes time. - Manage cooldowns: check
IF_SHIP_CAN_FIRE_*()before firing to avoid wasted instructions. - Asteroids are resources and hazards: collisions hurt; breaking asteroids can lead to fuel pickups.