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 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 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