← how-to

asteroids

providers / raylib

program
load core::c::*;
load core::random::*;
load provider::raylib;

-- The screen width.
val WIDTH: float = 800.0;
-- The screen height.
val HEIGHT: float = 600.0;

-- The frame per second number.
val FPS: int = 60;

-- The black color.
val COLOR_BLACK: int = 0xFF000000;
-- The blue color.
val COLOR_BLUE: int = 0xFFEFD966;
-- The green color.
val COLOR_GREEN: int = 0xFF3EFDCC;
-- The grey color.
val COLOR_GREY: int = 0xFFAAAAAA;
-- The white color.
val COLOR_WHITE: int = 0xFFFFFFFF;

-- raylib `KEY_*` codes.
val KEY_SPACE: int = 32;
val KEY_ENTER: int = 257;
val KEY_RIGHT: int = 262;
val KEY_LEFT: int = 263;
val KEY_DOWN: int = 264;
val KEY_UP: int = 265;

struct Size {
  width: int,
  height: int,
}

struct Position {
  x: float,
  y: float,
}

struct Velocity {
  x: float,
  y: float,
}

struct Laser {
  position: Position,
  speed: float,
  color: int,
}

struct Ship {
  position: Position,
  speed: float,
  alive: bool,
  color: int,
  size: Size,
}

struct Asteroid {
  position: Position,
  velocity: Velocity,
  size: float,
  color: int,
}

struct Text {
  content: CStr,
  position: Position,
  font_size: int,
  color: int,
}

-- Squared distance between two points — avoids a `sqrt`, compared against a
-- squared radius at the call site.
fun squared_distance(
  first_x: float,
  first_y: float,
  second_x: float,
  second_y: float,
) -> float {
  imu delta_x: float = first_x - second_x;
  imu delta_y: float = first_y - second_y;

  delta_x * delta_x + delta_y * delta_y
}

-- Wrap a coordinate around `[0, limit)` so a body that drifts off one edge
-- reappears on the opposite one.
fun wrap(value: float, limit: float) -> float {
  if value < 0.0 {
    value + limit
  } else if value > limit {
    value - limit
  } else {
    value
  }
}

-- Draw a `Text` label with `draw_text` at its position.
fun draw_label(label: Text) {
  raylib::draw_text(
    label.content,
    label.position.x as int,
    label.position.y as int,
    label.font_size,
    label.color,
  );
}

-- The ship's starting state: centered low, full speed, alive.
fun new_ship() -> Ship {
  Ship {
    position = Position { x = 384.0, y = 520.0 },
    speed = 320.0,
    alive = true,
    color = COLOR_GREEN,
    size = Size { width = 30, height = 30 },
  }
}

-- A fresh asteroid field in the upper screen, away from the ship's start, with
-- small drift velocities. Reused for the first round and every restart, so the
-- layout is the same each time.
fun spawn_asteroids() -> Vec<Asteroid> {
  mut asteroids: Vec<Asteroid> = Vec::new();
  mut rng: Rng = Rng::new(42);

  for i := 0..6 {
    asteroids.push(Asteroid {
      position = Position {
        x = rng.range(0, WIDTH as int) as float,
        y = rng.range(0, 280) as float,
      },
      velocity = Velocity {
        x = (rng.range(0, 7) as float) - 3.0,
        y = (rng.range(0, 7) as float) - 3.0,
      },
      size = 30.0,
      color = COLOR_GREY,
    });
  }

  asteroids
}

fun main() {
  raylib::init_window(WIDTH as int, HEIGHT as int, CStr::new("zo asteroids"));
  raylib::set_target_fps(FPS);

  mut ship: Ship = new_ship();
  mut lasers: Vec<Laser> = Vec::new();
  mut asteroids: Vec<Asteroid> = spawn_asteroids();

  loop {
    if raylib::window_should_close() { break }

    imu delta_time: float = raylib::get_frame_time();
    imu step: float = ship.speed * delta_time;

    if ship.alive && asteroids.len() > 0 {
      imu ship_half: float = (ship.size.width as float) / 2.0;

      if raylib::is_key_down(KEY_LEFT) { ship.position.x -= step }
      if raylib::is_key_down(KEY_RIGHT) { ship.position.x += step }
      if raylib::is_key_down(KEY_UP) { ship.position.y -= step }
      if raylib::is_key_down(KEY_DOWN) { ship.position.y += step }

      -- fire a laser from the ship's nose.
      if raylib::is_key_pressed(KEY_SPACE) {
        imu shot: Laser = Laser {
          position = Position {
            x = ship.position.x + ship_half,
            y = ship.position.y,
          },
          speed = 10.0,
          color = COLOR_WHITE,
        };

        lasers.push(shot);
      }

      -- advance lasers upward; drop the ones off the top.
      for laser_index := 0..lasers.len() {
        match lasers.get(laser_index) {
          Option::Some(laser) => {
            imu next_y: float = laser.position.y - laser.speed;

            if next_y < 0.0 {
              lasers.remove(laser_index);
            } else {
              imu moved: Laser = Laser {
                position = Position { x = laser.position.x, y = next_y },
                speed = laser.speed,
                color = laser.color,
              };

              lasers.set(laser_index, moved);
            }
          }
          Option::None => {}
        }
      }

      -- drift asteroids and wrap them at the edges.

      for asteroid_index := 0..asteroids.len() {
        match asteroids.get(asteroid_index) {
          Option::Some(asteroid) => {
            imu moved: Asteroid = Asteroid {
              position = Position {
                x = wrap(asteroid.position.x + asteroid.velocity.x, WIDTH),
                y = wrap(asteroid.position.y + asteroid.velocity.y, HEIGHT),
              },
              velocity = asteroid.velocity,
              size = asteroid.size,
              color = asteroid.color,
            };

            asteroids.set(asteroid_index, moved);
          }
          Option::None => {}
        }
      }

      -- laser hits asteroid: remove both.
      mut collision_index: int = 0;

      while collision_index < asteroids.len() {
        mut hit: bool = false;

        match asteroids.get(collision_index) {
          Option::Some(asteroid) => {
            mut laser_hit_index: int = 0;

            while laser_hit_index < lasers.len() {
              match lasers.get(laser_hit_index) {
                Option::Some(laser) => {
                  imu hit_distance: float = squared_distance(
                    laser.position.x,
                    laser.position.y,
                    asteroid.position.x,
                    asteroid.position.y,
                  );

                  if hit_distance < asteroid.size * asteroid.size {
                    hit = true;
                  }
                }
                Option::None => {}
              }

              if hit {
                lasers.remove(laser_hit_index);
                break;
              } else {
                laser_hit_index += 1;
              }
            }

            -- asteroid touches the ship: end the game.
            imu ship_distance: float = squared_distance(
              ship.position.x + ship_half,
              ship.position.y + ship_half,
              asteroid.position.x,
              asteroid.position.y,
            );

            imu reach: float = asteroid.size + ship_half;

            if ship_distance < reach * reach {
              ship.alive = false;
            }
          }
          Option::None => {}
        }

        if hit {
          asteroids.remove(collision_index);
        } else {
          collision_index += 1;
        }
      }
    } else if raylib::is_key_pressed(KEY_ENTER) {
      -- refresh the game.
      ship = new_ship();
      lasers.free();
      lasers = Vec::new();
      asteroids.free();
      asteroids = spawn_asteroids();
    }

    raylib::begin_drawing();
      raylib::clear_background(COLOR_BLACK);

      -- asteroids: grey filled circles.

      for asteroid_index := 0..asteroids.len() {
        match asteroids.get(asteroid_index) {
          Option::Some(asteroid) => {
            raylib::draw_circle(
              asteroid.position.x as int,
              asteroid.position.y as int,
              asteroid.size,
              asteroid.color,
            );
          }
          Option::None => {}
        }
      }

      -- lasers: small white dots.

      for laser_index := 0..lasers.len() {
        match lasers.get(laser_index) {
          Option::Some(laser) => {
            raylib::draw_circle(
              laser.position.x as int,
              laser.position.y as int,
              3.0,
              laser.color,
            );
          }
          Option::None => {}
        }
      }

      -- ship: a green rectangle.
      raylib::draw_rectangle(
        ship.position.x as int,
        ship.position.y as int,
        ship.size.width,
        ship.size.height,
        ship.color,
      );

      imu over: bool = !ship.alive;
      imu won: bool = asteroids.len() == 0;

      if over {
        imu label: Text = Text {
          content = CStr::new("GAME OVER"),
          position = Position { x = 300.0, y = 260.0 },
          font_size = 40,
          color = COLOR_BLUE,
        };

        draw_label(label);
      } else if won {
        imu label: Text = Text {
          content = CStr::new("YOU WIN"),
          position = Position { x = 323.0, y = 260.0 },
          font_size = 40,
          color = COLOR_BLUE,
        };

        draw_label(label);
      }

      if over || won {
        imu hint: Text = Text {
          content = CStr::new("PRESS ENTER"),
          position = Position { x = 312.0, y = 312.0 },
          font_size = 20,
          color = COLOR_WHITE,
        };

        draw_label(hint);
      }
      raylib::draw_fps(10, 10);
    raylib::end_drawing();
  }

  lasers.free();
  asteroids.free();
  raylib::close_window();
}
asteroids preview

reachout

echo -n 'dGhlQGNvbXBpbG9yZHMuaG91c2U=' | base64 --decode

For humans: faq.

For Ai agents: llms.txt (curated index) and llms-full.txt (full docs).

Privacy: No cookies, no ads, no tracking. It's like you were never here.