STASIS
Docs / game patterns

Make each system answer one question.

These patterns are small on purpose. Keep ownership and invariants near the data they protect.

State ownership

One root state.

struct GameState {
  score: i32;
  player_x: f32;
  player_y: f32;
}
global state: GameState;

Invariant: gameplay facts have one visible owner; render reads them.

Ordered tick

Input → intent → systems → commit.

function tick(): i32 {
  if (is_key_down(Scancode.Left)) { state.player_x -= state.speed; }
  if (is_key_down(Scancode.Right)) { state.player_x += state.speed; }
  if (state.player_x < 0.0) { state.player_x = 0.0; }
  return 0;
}

Invariant: changing system order is a deliberate behavior change.

Bounded storage

Capacity is part of the model.

global bullets: Bullet[128];

function spawn_bullet(): bool {
  for (let i = 0; i < 128; i += 1) {
    if (!bullets[i].active) {
      bullets[i].active = true;
      return true;
    }
  }
  return false;
}

Invariant: a tick has a known maximum scan and no hidden growth.

Query / materialize / commit

Separate decisions from writes.

let hit = aabb_intersects(
  state.player_x, state.player_y, state.player_x + 16.0, state.player_y + 16.0,
  state.target_x, state.target_y, state.target_x + 16.0, state.target_y + 16.0
);
if (hit) { state.target_health -= 1; }

Invariant: intermediate queries do not partially mutate the authoritative model.

System boundaries

Test the seam.

test `score increments once`(): bool {
  state.score = 0;
  state.score += 1;
  return state.score == 1;
}

Invariant: tests pin observable transitions, not implementation details.

Projection-only render

Draw what exists.

function render(): i32 {
  begin_frame();
  fill_rect(state.player_x, state.player_y, 16.0, 16.0, 1.0, 1.0, 1.0, 1.0);
  end_frame();
  return 0;
}

Invariant: rendering cannot become a second gameplay authority.

Practical examples

Four tiny decisions.

Pong scoring

if (state.ball_x < 0.0) {
  state.right_score += 1;
  state.ball_x = 400.0;
}

Invariant: one boundary crossing produces one score and one reset.

Breakout: one-brick collision

if (aabb_intersects(ball.x, ball.y, ball.x + 8.0, ball.y + 8.0,
                    brick.x, brick.y, brick.x + 32.0, brick.y + 12.0)) {
  brick.active = false;
  ball.dy = -ball.dy;
}

Invariant: a resolved brick cannot be resolved again in the same active slot.

Platformer landing

if (player.y < platform.top &&
    player.y + player.height >= platform.top) {
  player.y = platform.top - player.height;
  player.grounded = true;
  player.vy = 0.0;
}

Invariant: landing snaps to the platform boundary and clears downward velocity.

Snake reverse-turn rejection

if (requested.x != -state.direction.x ||
    requested.y != -state.direction.y) {
  state.next_direction = requested;
}

Invariant: a single tick cannot reverse the snake into its own head.