Dynamics

Dynamics enables interactive movement, physics, and collision mechanics. Objects can respond to keyboard input, snap to grids, collide with each other, and interact through forces.

Quick Reference

Want to... Use Section
Make object keyboard-controlled set X movable + set X.input "keyboard" Input
Make object click-to-move set X movable + set X.input "click" Input
Make object physics-only (no input) set X movable Movable
Make object draggable by user set X draggable Draggable
Enable jumping set X jumpable Jumpable
Make camera follow object set X.subject true Camera Following
Snap object to grid set X.snapToGrid "Grid1" Grid Attachment
Move object to grid cell set X.cellX 2 Grid Position
Block other objects set X.blocking true Blocking
Make a one-way platform set X.oneWay true One-Way Platforms
Detect overlaps without blocking set X.sensor true Sensor
React to blocking collision onCollide: handler Collision Events
Set surface friction set X.friction 0.5 Friction
Set surface bounciness set X.restitution 0.5 Bounce
Set movement animation set X movable style slide Movement Style
Add gravity set Cell.gravity 0.5 Gravity
Add wind set Cell.wind 0.3 Wind
Set wind direction set Cell.windAngle 90 Wind
Add air resistance set Cell.airResistance 0.1 Air Resistance
Push objects with other objects Both need movable + blocking Momentum Transfer
Check if cell is empty isEmpty("Grid1", x, y) Grid Functions
Get all empty cells emptyCells("Grid1") Grid Functions
Read movement state if X.moving: Movement State
React to direction change onMove "down": Movement Events
React to stopping onStop: Movement Events
React to jumping onJump: Movement Events
React to landing onLanding: Movement Events
Get movement angle X.moveAngle Movement State
Make object pullable set X.pullable true Pullable
Wait for movement to finish wait movement Wait Movement

Input

By default, a movable object does not respond to user input. It only moves from physics forces (gravity, wind) or script commands. Input must be enabled explicitly.

Input Types

Type Description
keyboard Responds to WASD / arrow keys
click Click-to-move: clicks set destination, object moves toward it
(none) Physics-only: gravity, wind, collisions, and script commands

Enabling Keyboard Control

set Player movable                  # Enable physics movement
set Player.input "keyboard"         # Enable keyboard control

Both are required. movable enables the physics engine to process the object. input "keyboard" tells it to read key presses.

Click-to-Move

Objects with input: "click" move toward clicked points:

set Player movable                  # Enable physics movement
set Player.input "click"            # Enable click-to-move

When you click empty space in play mode:

Click-to-move works with grid snapping — the object moves cell-by-cell toward the clicked cell.

Player Bindings

Player 1 (default): WASD + Arrow keys. Objects without explicit input binding use Player 1 keys when input is "keyboard".


Draggable

Makes an object directly draggable by the user via mouse/touch.

Basic Usage

set Piece draggable                 # Enable drag
set Piece draggable false           # Disable drag

Drag Events

Objects with draggable fire three events during interaction:

Event When
onDragStart User starts dragging (after threshold)
onDrag Each frame during drag
onDragEnd User releases
onDragStart:
  set self.opacity 0.7
  set self.state "DRAGGING"

onDrag:
  # Called continuously while dragging
  log "dragging at" self.x self.y

onDragEnd:
  set self.opacity 1
  set self.state "IDLE"

Click vs Drag

An object can be both clickable and draggable. The system distinguishes:

Both onClick and drag events work on the same object:

onClick:
  self.toggle                       # Tap to toggle

onDragEnd:
  log "dropped at" self.x self.y    # Drag to move

Configuration Options

set Piece draggable once            # Can only drag one time
set Piece draggable collision       # Respect blockers while dragging
set Piece draggable discrete        # Grid objects drag cell-by-cell
Option Behavior
once Dragging disabled after first drag ends
collision Object collides with blockers during drag
discrete Grid-attached objects move cell-by-cell

Camera Behavior

When dragging objects, the camera pauses following subjects to prevent erratic movement. Camera resumes following when the drag ends.


Movable

Makes an object participate in the physics engine. The object can be affected by gravity, wind, collisions, and momentum transfer.

Basic Usage

set Player movable                  # Enable with defaults
set Player movable speed 0.5        # Custom speed
set Player movable speed 0.5 acceleration 0.2 deceleration 0.1

Configuration

Property Default Description
speed 0.3 Max velocity (cells/sec for grid, units/sec for free)
acceleration 0.1 How quickly reaches max speed
deceleration 0.1 Velocity decay when no input (acts as drag)
style teleport Movement animation: teleport, slide, jump, fade

Deceleration as Drag

The deceleration property controls how quickly an object slows down when there is no input driving it. This applies to all velocity sources — keyboard input, collision impulses, and momentum transfer.

In side-view (with gravity), deceleration mainly affects horizontal slowdown since gravity dominates the vertical axis. In top-down (no gravity), deceleration acts as floor drag — a pushed object slides and gradually stops.

Deceleration Behavior
0.02 - 0.05 Ice / slippery floor — objects slide a long way
0.1 - 0.15 Normal — moderate slide after being pushed
0.3+ Sticky / rough floor — objects stop quickly

Movement Style

Control how objects animate between grid cells:

set Player movable style teleport   # Instant snap (default)
set Player movable style slide      # Smooth interpolation
set Player movable style jump       # Arc motion (hop style)
set Player movable style fade       # Fade out/in transition

Disabling Movement

set Player.movable false            # Disable movement

Mass

Objects have a mass property that affects forces and momentum. Default mass is based on the object's area (width × height), set at creation.

set Player.mass 0.5               # Light (blown by wind, easy to push)
set Boulder.mass 5                # Heavy (resists wind, hard to push)
Force Mass Effect
Gravity No effect — all objects fall equally
Wind Heavier objects resist more
Input Heavier objects accelerate more slowly
Momentum Heavier objects push lighter ones farther

Components: mass is the sum of children (not directly editable).


Jumpable

Gives an object the ability to jump. Separate from movable — an object needs both to move and jump.

Basic Usage

set Player movable                  # Enable movement
set Player.input "keyboard"         # Enable keyboard control
set Player jumpable                 # Enable jump (Space key by default)

Configuration

set Player jumpable height 0.8     # Stronger jump
set Player jumpable multijump 2    # Double jump
set Player jumpable key "w"        # Custom jump key
Property Default Description
height 0.5 Upward impulse velocity
key Space Key that triggers jump
multijump 1 Max jumps before landing (2 = double jump)

Behavior


Physics

Cell-level forces that affect all movable objects. Set in the Cell properties panel or via script.

Gravity

Constant downward acceleration:

set Cell.gravity 0.5               # Gentle gravity
set Cell.gravity 2                 # Strong gravity
set Cell.gravity 0                 # Disable
set Cell.gravity -1                # Upward (reverse gravity)

Gravity is mass-independent — all objects fall at the same rate. Range: -10 to 10.

Wind

Wind pushes objects in a configurable direction:

set Cell.wind 0.3                  # Wind magnitude
set Cell.windAngle 0               # Rightward (default)
set Cell.windAngle 90              # Downward
set Cell.windAngle 180             # Leftward
set Cell.windAngle 270             # Upward
set Cell.windAngle 45              # Diagonal (down-right)

Wind is mass and area dependent — large light objects are blown more, heavy objects resist.

Air Resistance

Air resistance creates drag that opposes velocity. Objects slow down as they move through the medium, creating natural terminal velocity.

set Cell.airResistance 0           # Vacuum (no drag)
set Cell.airResistance 0.1         # Air-like
set Cell.airResistance 0.5         # Thick air / light water
set Cell.airResistance 1           # Water-like (strong drag)
Value Behavior
0 No drag — objects accelerate indefinitely (hard cap applies)
0.05 - 0.1 Light drag — realistic air resistance
0.2 - 0.5 Heavy drag — objects slow quickly when forces stop
1+ Very thick medium — strong velocity damping

Terminal velocity: With air resistance, objects naturally reach a terminal velocity where drag equals gravity. The relationship is approximately v_terminal ≈ gravity / airResistance. For example, with gravity 1 and airResistance 0.1, terminal velocity is about 10 units/sec.

Hard Velocity Cap

A hard cap of 50 units/sec exists as a safety net. With air resistance, this cap is rarely reached since drag naturally limits velocity.

Contact Blocking

When an object is resting on a surface (e.g., standing on ground with gravity), the surface blocks the force that would push it through. Gravity doesn't accumulate while grounded. Wind is similarly blocked when pressing against a wall.


Surface Physics

Per-object properties that control how surfaces interact during collisions. Set in the properties panel or via script.

Friction

Controls how much parallel velocity is dampened on contact. Think of it as surface roughness.

set Floor.friction 0.8             # Rough surface — objects slow quickly
set Ice.friction 0.05              # Slippery surface — objects slide
set Player.friction 0.3            # Player's own surface friction
Value Behavior
0 No friction — frictionless surface (ice)
0.1 - 0.3 Low friction — smooth sliding
0.5 Medium friction — moderate grip
0.8 - 1.0 High friction — objects stop quickly on contact

Combination rule: when two objects collide, their friction values are averaged. A low-friction object on a high-friction surface gets moderate friction: (0.1 + 0.8) / 2 = 0.45.

Bounce (Restitution)

Controls how much velocity is reflected on collision. A bouncy ball hitting a wall bounces back.

set Ball.restitution 0.7           # Bouncy ball
set Wall.restitution 0             # Dead wall (no bounce)
set Trampoline.restitution 0.9     # Very bouncy surface
Value Behavior
0 Dead stop — no bounce at all
0.3 Low bounce — slight rebound
0.5 Medium bounce
0.7 - 0.9 High bounce — energetic rebound

Maximum effective restitution is 0.95 to prevent infinite bouncing.

Combination rule: when two objects collide, the higher restitution wins. A bouncy ball (0.7) hitting a dead wall (0) still bounces at 0.7. Either surface being bouncy makes the collision bouncy.

Surface Property Summary

Property Range Default Combination Set on
friction 0 - 1 0 Average of both surfaces Any blocking object
restitution 0 - 1 0 Max of both surfaces Any blocking object

Both properties apply to the mover and the blocker. A bouncy player hitting a non-bouncy wall still bounces (player's restitution applies). A low-friction player on a high-friction floor gets moderate friction.


Blocking

The blocking property prevents other movable objects from passing through.

Basic Blocking

set Wall.blocking true              # Simple blocker
set Player.blocking true            # Player blocks others too

An object can be both movable and blocking — it moves but other objects can't pass through it.

Conditional Blocking

Use expressions for dynamic blocking rules:

# Block objects of different color (using tags)
set Piece.blocking "self.color != other.color"

# Block only larger objects
set Piece.blocking "other.width > self.width"

# Block if wrong size order (for stacking puzzles)
set Piece.blocking "self.size < other.size"

In blocking expressions:

One-Way Platforms

A one-way platform only blocks objects approaching from above. Objects can jump through from below and land on top.

set Platform.blocking true
set Platform.oneWay true
Approach Direction Blocked?
From above (landing) Yes
From below (jumping up) No — passes through
From the side No — passes through

One-way platforms are ideal for side-view platformer levels where the player jumps up through platforms.

Sensor

A sensor object detects overlaps without blocking movement. When sensor is true, the object fires onOverlap and onOverlapEnd events when it overlaps another object, but does not prevent passage.

set Coin.sensor true                # Detect overlaps, don't block
set DangerZone.sensor true          # Overlap detection only

At least one of the two overlapping objects must have sensor: true for overlap events to fire. Use the other keyword inside onOverlap/onOverlapEnd handlers to reference the other object.

Sensor vs Blocking:

Property Prevents passage Fires overlap events
blocking Yes No
sensor No Yes

Example: Collectible coins

onEnter:
  foreach coin in #coin {
    set coin.sensor true
  }

# On each coin object:
onOverlap:
  if other is #player:
    hide self
    set score score + 1

Example: Danger zone

# On the zone object:
onOverlap:
  if other is #player:
    set inZone true
    shake other 300

onOverlapEnd:
  if other is #player:
    set inZone false

You can check by name (other.name == "Player") or by tag (other is #player).

The sensor toggle appears in the Properties Panel alongside One-Way in the dynamics toggles row.

Pullable

A pullable object can be pulled by a movable object when the player holds Shift while moving away from it.

set Crate.pullable true
set Crate.blocking true

How it works:

  1. Player stands adjacent to a pullable object
  2. Player holds Shift and moves away from the object
  3. The pullable object follows the player into their vacated cell

This enables Sokoban-style puzzles where you can both push and pull crates.

Property Description
pullable When true, object can be pulled by Shift+move

Note: If the pull fails (e.g., destination blocked), the player also cannot move (grip behavior).

Collision Events (onCollide)

When a movable object collides with a blocking object, the onCollide event fires on the mover. Use the other keyword to reference the blocker.

# On the player object:
onCollide:
  if other.name == "Spike":
    goto "GameOver"
  if other.name == "Wall":
    shake self 200

onCollide vs onOverlap:

Event When it fires Movement
onCollide Mover hits blocker Mover is stopped
onOverlap Objects overlap (sensor) No blocking

Use onCollide for:


Momentum Transfer

When a movable object collides with another movable blocker, momentum is transferred based on their masses. The pushed object receives an impulse and starts moving.

Setup

Both objects need movable and blocking:

# Player (the pusher)
set Player movable speed 0.3
set Player.input "keyboard"
set Player.blocking true
set Player.mass 2

# Crate (pushable object)
set Crate movable speed 0.3
set Crate.blocking true
set Crate.mass 1

The pushed object does not need keyboard input — it moves purely from physics impulse.

Mass Effects

Momentum splits by mass ratio using conservation of momentum:

Scenario Result
Heavy pushes light Light object flies far, heavy barely slows
Equal masses Energy splits evenly
Light pushes heavy Heavy barely moves, light bounces back

Sliding After Push

After being pushed, the object slows down based on its deceleration value. In top-down mode (no gravity), this acts as floor drag. Lower deceleration = slides farther.

Surface Properties on Push

Friction and restitution affect the collision:

Example: Slippery Push Puzzle

# Ice floor: objects slide a long way after being pushed
foreach crate in #crate {
  set crate movable speed 0.5 deceleration 0.03
  set crate.blocking true
  set crate.friction 0.05
  set crate.restitution 0.3
  set crate.mass 1
}

# Player: heavy enough to push crates
set Player movable speed 0.4
set Player.input "keyboard"
set Player.blocking true
set Player.mass 3
set Player.friction 0.1

Camera Following

Make the camera follow a movable object:

set Player.subject true             # Camera follows this object

When subject is true, the camera tracks the object in cells larger than the viewport. See Camera for dead zone settings and multi-cell scrolling behavior.


Grid Objects

A Grid is a special object type that defines a coordinate system. Objects can attach to grids for cell-based positioning and movement.

Creating a Grid

Add a Grid from the right-click menu → Add to DOM → Grid. Configure in the properties panel:

Property Description
Columns Number of horizontal cells
Rows Number of vertical cells
Show Lines Display grid lines (build mode always shows)
Line Color Grid line color

Grid orientation


Two Grid Systems: Occupancy vs Cell Data

This is critical to understand. Grids have TWO completely separate systems for tracking state:

System What it tracks How to read How to write Cleared when
Occupancy Which objects are in which cells Player.cellX, isEmpty(), emptyCells() Move objects with cellX/cellY Objects move/destroyed
Cell Data Arbitrary data per cell Grid.cell[x][y].property set Grid.cell[x][y].prop value clear Grid or reset("session")

When to use which?

Use Case System Why
Track player position Occupancy Player is a visual object that moves
Place game pieces Occupancy Pieces are objects you see and interact with
Mark cells as "owned" (Connect4) Cell Data Ownership is metadata, not a visible object
Store mine positions (Minesweeper) Cell Data Hidden data, not visible objects
Count objects in cells Occupancy You're counting real objects
Check if cell was "revealed" Cell Data State data, not an object
Track which cells are walkable Either Objects for visible walls, data for invisible rules

Key differences

Occupancy:

Cell Data:

Common mistake

# WRONG: Trying to use cell data to track pieces
set Board.cell[3][4].hasPiece true    # This just stores data!
# The piece object is NOT at (3,4) unless you moved it there

# CORRECT: Move the actual object
set Piece.snapToGrid "Board"
set Piece.cellX 3
set Piece.cellY 4
# Now isEmpty("Board", 3, 4) returns false

Working together

Often you'll use BOTH systems together:

# Connect Four: Pieces fall and stack (occupancy)
# But we also track who owns each cell (data)

onSpawn:
  set self.snapToGrid "Board"
  set self.cellX col
  set self.cellY row
  set self.blocking true
  # Store ownership in cell data for win checking
  set Board.cell[col][row].owner currentPlayer

Grid Occupancy (Object Positions)

Track where objects physically are on the grid.

Grid Attachment

Attach objects to a grid using snapToGrid:

set Player.snapToGrid "Grid1"       # Attach to grid
set Bullet.snapToGrid "none"        # Detach (free movement)

When attached, the object:

Reading Object Position

log "Player at" Player.cellX Player.cellY

if Player.cellX == 3 and Player.cellY == 5:
  show Treasure

Setting Object Position

Move objects by setting their cell coordinates:

set Player.cellX 4
set Player.cellY 2

This teleports the object to that cell immediately.

Occupancy Functions

Query which cells have objects in them.

isEmpty(grid, x, y)

Returns true if no object occupies the cell at coordinates (x, y):

if isEmpty("Grid1", 2, 3):
  set Piece.cellX 2
  set Piece.cellY 3

Note: This checks for objects with snapToGrid set to this grid. It does NOT check cell data.

emptyCells(grid)

Returns an array of {x, y} objects for all cells with no objects:

set empty emptyCells("Grid1")
log "Empty cells:" length(empty)

# Pick a random empty cell
if length(empty) > 0:
  set cell pick(empty)
  set Piece.cellX cell.x
  set Piece.cellY cell.y

Initializing Grid Objects

When using occupancy functions, objects must have their positions tracked first. Set snapToGrid and wait briefly for the dynamics system to calculate positions:

onEnter:
  # Initialize blockers first
  foreach blocker in #blocker {
    set blocker.snapToGrid "Grid1"
    set blocker.blocking true
  }

  # Wait for positions to be calculated
  wait 100ms

  # Now emptyCells excludes blocker positions
  set empty emptyCells("Grid1")

Grid Cell Data (Metadata Storage)

Store arbitrary data per grid cell. This is completely separate from objects - cells can have data even with no object in them.

Writing Cell Data

set Board.cell[0][0].owner "RED"           # String
set Board.cell[x][y].revealed true         # Boolean
set Board.cell[col][row].mineCount 3       # Number
set Board.cell[i][j].state "FLAGGED"       # Any value

Indices can be literals (0), variables (x), or expressions (col + 1).

Reading Cell Data

# In conditions
if Board.cell[0][0].owner == "RED":
  log "Red owns top-left"

# As expression
set cellOwner Board.cell[x][y].owner

# Get entire cell's data object
set cellData Board.cell[0][0]
log cellData.owner cellData.revealed

Note: Unset properties return 0 by default.

Clearing Cell Data

clear Board                    # Clear ALL cells in entire grid
clear Board.cell[2][3]         # Clear just one cell

Cell data is also cleared on reset("session").

Querying Cell Data with cellsWhere

Find cells where stored data matches a condition:

# Find all cells owned by RED
set redCells cellsWhere("Board", "owner", "==", "RED")
log "Red owns" length(redCells) "cells"

# Find all revealed cells
set revealed cellsWhere("Board", "revealed", "==", true)

# Find cells with 3+ adjacent mines
set danger cellsWhere("Board", "mineCount", ">=", 3)

Returns array of {x, y} objects.

Cell Data vs Occupancy - Complete Example

Here's a Minesweeper-style game showing both systems:

onEnter:
  # CELL DATA: Randomly place mines (invisible data)
  set mineCount 10
  set placed 0
  while placed < mineCount:
    set x floor(random() * 10)
    set y floor(random() * 10)
    if Board.cell[x][y].mine != true:
      set Board.cell[x][y].mine true
      set placed placed + 1

  # OCCUPANCY: Create cover tiles (visible objects)
  foreach x in range(10):
    foreach y in range(10):
      spawn "CoverTile" {col: x, row: y}

# CoverTile template
onSpawn:
  set self.snapToGrid "Board"      # OCCUPANCY: Object goes to cell
  set self.cellX col
  set self.cellY row
  show self

onClick:
  # CELL DATA: Check if this cell has a mine
  if Board.cell[self.cellX][self.cellY].mine:
    shout "GAME_OVER"
  else:
    # CELL DATA: Mark as revealed
    set Board.cell[self.cellX][self.cellY].revealed true
    # OCCUPANCY: Remove the cover object
    destroy self

Key points:


AI with Minimax

For turn-based games like Connect Four or Tic-Tac-Toe, the minimax function finds the best move.

# minimax(grid, property, aiPlayer, humanPlayer, depth, emptyValue?, winLength?)

# Connect Four: Find best column for AI
set bestCol minimax("Board", "owner", "YELLOW", "RED", 5, 0, 4)

# Tic-Tac-Toe: 3x3 grid, 3 in a row wins
set bestCol minimax("Grid", "state", "O", "X", 9, "", 3)

Parameters:

Note: Minimax reads from cell data, not object positions. You must set cell data when pieces are placed:

# When placing a piece
action placePiece:
  spawn "Piece" {col: col, color: currentPlayer}
  # ALSO store in cell data for minimax
  set Board.cell[col][row].owner currentPlayer

Movement State

Read-only properties that reflect current movement:

Property Type Description
moving boolean Currently in motion
direction string "left", "right", "up", "down", "none"
velocityX number Horizontal velocity
velocityY number Vertical velocity
moveAngle number Movement angle in degrees (0=right, 90=down, -1=stopped)
moveSpeed number Movement speed magnitude

Movement Events

React to movement state changes with dedicated events:

# Fires when object starts moving in a direction
onMove "down":
  set self.state "walkDown"

onMove "left":
  set self.state "walkLeft"
  set self.flipX true

# Fires when object stops moving
onStop:
  set self.state "idle"

# Fires when object jumps (requires jumpable)
onJump:
  set self.state "jumping"

# Fires when object lands on a surface
onLanding:
  set self.state "idle"
  shake self 50

Use onMove without a direction parameter to trigger on any direction change. The cardinal() function converts angles to direction strings:

onMove:
  set self.state cardinal(self.moveAngle)  # "right", "down", "left", "up"

Polling Movement State

You can also poll movement properties directly:

# Animate while moving
if Player.moving:
  set Legs.rotation Legs.rotation + 5

# Face movement direction
if Player.direction == "left":
  set Player.flipX true
else if Player.direction == "right":
  set Player.flipX false

Component Animation

Children can query parent movement for coordinated animation:

# On a leg component inside Player
if parent.moving:
  set self.rotation self.rotation + 10
else:
  set self.rotation 0

Wait Movement

Scripts and dynamics run independently. When a key is pressed:

  1. onKeyDown fires immediately
  2. Dynamics processes movement over subsequent frames
  3. Position properties (cellX, cellY) update after movement completes

This means reading positions immediately in onKeyDown gives you the previous frame's values.

The Problem

onKeyDown:
  # BUG: This reads the OLD position before movement happens
  log "Position:" Player.cellX Player.cellY
  if Player.cellX == 5:
    show WinMessage

The Solution

Use wait movement to pause until all objects have finished moving:

onKeyDown:
  wait movement    # Pause until dynamics settles
  log "Position:" Player.cellX Player.cellY
  if Player.cellX == 5:
    show WinMessage

When to Use

Scenario Use wait movement?
Reading current positions Yes
Checking win/lose conditions Yes
Counting empty cells Yes
Playing a sound No (immediate is fine)
Showing/hiding UI No (immediate is fine)

Examples

Side-View Platformer

onEnter:
  set Cell.gravity 1.5

  # Player: movable + jumpable + keyboard
  set Player movable speed 0.4
  set Player.input "keyboard"
  set Player jumpable height 0.6 multijump 2
  set Player.subject true
  set Player.blocking true
  set Player.friction 0.3

  # Ground and walls
  foreach wall in #wall {
    set wall.blocking true
    set wall.friction 0.5
  }

  # Bouncy pad
  set BouncePad.blocking true
  set BouncePad.restitution 0.9

  # One-way platforms (jump through from below)
  foreach platform in #platform {
    set platform.blocking true
    set platform.oneWay true
  }

Top-Down Push Puzzle

onEnter:
  # No gravity in top-down
  set Cell.gravity 0

  # Player
  set Player movable speed 0.3 deceleration 0.2
  set Player.input "keyboard"
  set Player.blocking true
  set Player.mass 3

  # Pushable crates (no keyboard input, just physics)
  foreach crate in #crate {
    set crate movable speed 0.3 deceleration 0.08
    set crate.blocking true
    set crate.mass 1
    set crate.friction 0.1
  }

  # Walls (static blockers)
  foreach wall in #wall {
    set wall.blocking true
  }

Random Scatter

Place pieces on random empty grid cells:

onEnter:
  set empty emptyCells("Grid1")
  set shuffled shuffle(empty)
  set i 0
  foreach piece in #piece {
    set piece.snapToGrid "Grid1"
    set piece.blocking true
    set cell shuffled[i]
    set piece.cellX cell.x
    set piece.cellY cell.y
    show piece with scale 200
    wait 50ms
    set i i + 1
  }

Win Condition Check

Check game state after each move. Use wait movement to ensure positions are current:

# On the cell (Canvas script)
onKeyDown:
  wait movement
  self.checkGameState

action checkGameState {
  # Win: all cells filled
  set empty emptyCells("Grid1")
  if length(empty) == 0:
    show WinMessage

  # Lose: too few empty cells
  if length(empty) < 3:
    show LoseMessage
}

Color-Based Stacking

Allow same-color pieces to stack, block different colors:

foreach piece in #piece {
  set piece.snapToGrid "Grid1"
  set piece movable style slide
  set piece.blocking "self.color != other.color"
}

The color property can come from a tag or custom property set on each piece.