tiramisu/simulate

Testing utilities for Tiramisu games.

The simulate module allows you to test game logic without a browser, similar to lustre/dev/simulate. It provides frame-by-frame game simulation with input mocking and effect recording.

Basic Usage

import tiramisu/simulate
import tiramisu/input
import gleam/time/duration

pub fn player_moves_when_key_pressed_test() {
  let sim = simulate.start(my_game.init, my_game.update, my_game.view)

  // Simulate pressing W key
  let sim = simulate.with_key_pressed(sim, input.KeyW)
  let sim = simulate.frame(sim, delta: duration.milliseconds(16))

  let model = simulate.model(sim)
  assert model.player.position.y > 0.0
}

Types

Recorded effect for inspection

Effects in simulations are recorded but NOT executed, allowing you to verify that your game logic produces the expected effects.

pub type RecordedEffect(msg) {
  RecordedDispatch(msg: msg)
  RecordedDelay(delay: duration.Duration, msg: msg)
  RecordedInterval(interval: duration.Duration, msg: msg)
}

Constructors

  • RecordedDispatch(msg: msg)

    A message was dispatched via effect.dispatch

  • RecordedDelay(delay: duration.Duration, msg: msg)

    A delayed message via effect.delay

  • RecordedInterval(interval: duration.Duration, msg: msg)

    An interval was created via effect.interval

Opaque simulation state that holds the game model, effects, and context

pub opaque type Simulation(model, msg)

Values

pub fn clear_effects(
  sim: Simulation(model, msg),
) -> Simulation(model, msg)

Clear all recorded effects

pub fn clear_input(
  sim: Simulation(model, msg),
) -> Simulation(model, msg)

Clear per-frame input state (just_pressed, just_released, deltas)

This is automatically called by frame(), but you can call it manually if needed.

pub fn dispatch(
  sim: Simulation(model, msg),
  msg: msg,
) -> Simulation(model, msg)

Queue a message to be processed on the next frame

Example

let sim = simulate.dispatch(sim, Jump)
let sim = simulate.frame(sim, delta: duration.milliseconds(16))
pub fn dispatch_now(
  sim: Simulation(model, msg),
  msg: msg,
) -> Simulation(model, msg)

Dispatch a message and immediately process it (within the same frame)

Example

let sim = simulate.dispatch_now(sim, StartGame)
let model = simulate.model(sim)
pub fn dispatched_messages(
  sim: Simulation(model, msg),
) -> List(msg)

Get all dispatched messages from effects

pub fn effects(
  sim: Simulation(model, msg),
) -> List(RecordedEffect(msg))

Get all recorded effects

pub fn frame(
  sim: Simulation(model, msg),
  delta delta: duration.Duration,
) -> Simulation(model, msg)

Advance simulation by one frame with specified delta time

This processes all pending messages and updates the frame counter. Physics stepping is NOT done automatically - if your game uses physics, call physics.step in your update function.

Example

let sim = simulate.frame(sim, delta: duration.milliseconds(16))
pub fn frame_count(sim: Simulation(model, msg)) -> Int

Get current frame count

pub fn frames(
  sim: Simulation(model, msg),
  count count: Int,
  delta delta: duration.Duration,
) -> Simulation(model, msg)

Advance simulation by N frames with fixed delta time

Example

// Advance 60 frames at 16ms each (roughly 1 second)
let sim = simulate.frames(sim, count: 60, delta: duration.milliseconds(16))
pub fn get_body_transform(
  sim: Simulation(model, msg),
  id: String,
) -> Result(transform.Transform, Nil)

Get transform of a physics body

pub fn get_collision_events(
  sim: Simulation(model, msg),
) -> List(physics.CollisionEvent)

Get collision events from last physics step

pub fn has_effect(
  sim: Simulation(model, msg),
  predicate: fn(RecordedEffect(msg)) -> Bool,
) -> Bool

Check if a specific effect was recorded

Example

let has_jump_sound = simulate.has_effect(sim, fn(e) {
  case e {
    simulate.RecordedDispatch(PlaySound("jump")) -> True
    _ -> False
  }
})
pub fn input_state(
  sim: Simulation(model, msg),
) -> input.InputState

Get the current input state

pub fn model(sim: Simulation(model, msg)) -> model

Get the current model

pub fn physics_world(
  sim: Simulation(model, msg),
) -> option.Option(physics.PhysicsWorld)

Get the physics world (if physics is enabled)

pub fn start(
  init init: fn(tiramisu.Context) -> #(
    model,
    effect.Effect(msg),
    option.Option(physics.PhysicsWorld),
  ),
  update update: fn(model, msg, tiramisu.Context) -> #(
    model,
    effect.Effect(msg),
    option.Option(physics.PhysicsWorld),
  ),
  view view: fn(model, tiramisu.Context) -> scene.Node,
  canvas_size canvas_size: vec2.Vec2(Float),
) -> Simulation(model, msg)

Start a simulation with a specific canvas size

Example

let sim = simulate.start(
  init: game.init,
  update: game.update,
  view: game.view,
  canvas_size: vec2.Vec2(1920.0, 1080.0),
)
pub fn step_physics(
  sim: Simulation(model, msg),
  delta: duration.Duration,
) -> Simulation(model, msg)

Step physics explicitly

Physics is NOT stepped automatically by frame(). Use this helper to step physics in your tests, or call physics.step in your game’s update function.

pub fn total_time(
  sim: Simulation(model, msg),
) -> duration.Duration

Get total simulation time

pub fn view(sim: Simulation(model, msg)) -> scene.Node

Get the current scene node (result of calling view)

pub fn with_input(
  sim: Simulation(model, msg),
  input_state: input.InputState,
) -> Simulation(model, msg)

Set full input state (for complex scenarios)

pub fn with_key_just_pressed(
  sim: Simulation(model, msg),
  key: input.Key,
) -> Simulation(model, msg)

Set a key as just pressed (for this frame only)

This sets the key as both pressed AND just_pressed.

Example

let sim = simulate.with_key_just_pressed(sim, input.Space)
let sim = simulate.frame(sim, delta: duration.milliseconds(16))
// After frame(), just_pressed is cleared but pressed remains
pub fn with_key_pressed(
  sim: Simulation(model, msg),
  key: input.Key,
) -> Simulation(model, msg)

Set a key as pressed (held down)

Example

let sim = simulate.with_key_pressed(sim, input.KeyW)
pub fn with_key_released(
  sim: Simulation(model, msg),
  key: input.Key,
) -> Simulation(model, msg)

Release a key

This removes the key from pressed and sets it as just_released.

pub fn with_left_button_just_pressed(
  sim: Simulation(model, msg),
) -> Simulation(model, msg)

Set left mouse button as just pressed (for this frame only)

pub fn with_left_button_pressed(
  sim: Simulation(model, msg),
) -> Simulation(model, msg)

Set left mouse button as pressed

pub fn with_mouse_delta(
  sim: Simulation(model, msg),
  dx: Float,
  dy: Float,
) -> Simulation(model, msg)

Set mouse delta (movement since last frame)

pub fn with_mouse_position(
  sim: Simulation(model, msg),
  x: Float,
  y: Float,
) -> Simulation(model, msg)

Set mouse position

Example

let sim = simulate.with_mouse_position(sim, 400.0, 300.0)
pub fn with_right_button_pressed(
  sim: Simulation(model, msg),
) -> Simulation(model, msg)

Set right mouse button as pressed

Search Document