dreamstack/examples/game-tetris.ds
enzotar d4f353394f fix: keyboard inputs now respect collision — soft drop and hard drop gated on blocked
ArrowDown soft drop, Space hard drop, and Drop button all bypassed
the blocked signal, letting pieces pass through frozen blocks.
Now all three gate on blocked before modifying py.
2026-02-27 12:44:19 -08:00

328 lines
21 KiB
Text

-- DreamStack Tetris — Signal Composition Showcase
--
-- This game demonstrates how DreamStack composes independent signal layers:
--
-- Layer 1: DATA SIGNALS — grid cells, piece encoding, bag randomizer
-- Layer 2: PHYSICS SIGNALS — gravity timer, drop speed, lock delay
-- Layer 3: INPUT SIGNALS — keyboard events mapped to commands
-- Layer 4: DERIVED SIGNALS — ghost position, line detection, game over
-- Layer 5: UI SIGNALS — score, level, next piece preview
-- Layer 6: STREAM SIGNALS — real-time spectator broadcast
--
-- Each layer is self-contained; the reactive runtime composites them.
--
-- Grid: 10 wide x 20 tall. Rows g0 (top) to g19 (bottom).
-- Cell values: 0=empty, 1=cyan(I), 2=blue(J), 3=orange(L),
-- 4=yellow(O), 5=green(S), 6=purple(T), 7=red(Z)
--
-- Run:
-- Tab 1: cargo run -p ds-stream (relay)
-- Tab 2: dreamstack dev examples/game-tetris.ds (player)
import { Badge } from "../registry/components/badge"
-- ================================================================
-- LAYER 1: DATA SIGNALS — Frozen grid + active piece state
-- ================================================================
-- Frozen grid: 20 rows x 10 columns (row 0 = top, row 19 = bottom)
let g0 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let g1 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let g2 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let g3 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let g4 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let g5 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let g6 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let g7 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let g8 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let g9 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let g10 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let g11 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let g12 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let g13 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let g14 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let g15 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let g16 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let g17 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let g18 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let g19 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
-- Active piece: 1=I, 2=J, 3=L, 4=O, 5=S, 6=T, 7=Z
let piece = 6
let rotation = 0
let px = 3
let py = 0
-- Next piece for preview
let nextPiece = 1
-- ================================================================
-- LAYER 2: PHYSICS SIGNALS — Gravity, speed, level
-- ================================================================
let score = 0
let lines = 0
let level = 1
let gameOver = 0
let paused = 0
let gravityTick = 0
let dropInterval = 24
let lockTick = 0
-- ================================================================
-- LAYER 3: INPUT SIGNALS — Keyboard events
-- ================================================================
on keydown(ev) -> px = if gameOver then px else (if paused then px else (if ev.key == "ArrowLeft" then (if px > 0 then px - 1 else px) else px))
on keydown(ev) -> px = if gameOver then px else (if paused then px else (if ev.key == "ArrowRight" then (if px < 7 then px + 1 else px) else px))
on keydown(ev) -> rotation = if gameOver then rotation else (if paused then rotation else (if ev.key == "ArrowUp" then (rotation + 1) % 4 else rotation))
on keydown(ev) -> py = if gameOver then py else (if paused then py else (if blocked then py else (if ev.key == "ArrowDown" then (if py < 18 then py + 1 else py) else py)))
on keydown(ev) -> py = if gameOver then py else (if paused then py else (if blocked then py else (if ev.key == " " then 18 else py)))
on keydown(ev) -> paused = if ev.key == "p" then (if paused then 0 else 1) else paused
-- ================================================================
-- LAYER 4: DERIVED SIGNALS — Collision, gravity, lock, line clear
-- ================================================================
-- Collision detection: can the piece move down?
-- T-piece at py has top cells at row py, bottom cell at py+1.
-- To move down: top cells move to py+1, bottom cell to py+2.
-- Check grid[py+1] at px, px+1, px+2 (top row would land here).
-- Bottom wall: py >= 18 (piece can't go lower).
let blocked = 0
-- Check collision against the CORRECT row: grid[py+1] for top cells
every 33 -> blocked = if py > 17 then 1 else (if py == 17 then (if g18[px] > 0 then 1 else (if g18[px + 1] > 0 then 1 else (if g18[px + 2] > 0 then 1 else 0))) else (if py == 16 then (if g17[px] > 0 then 1 else (if g17[px + 1] > 0 then 1 else (if g17[px + 2] > 0 then 1 else 0))) else (if py == 15 then (if g16[px] > 0 then 1 else (if g16[px + 1] > 0 then 1 else (if g16[px + 2] > 0 then 1 else 0))) else (if py == 14 then (if g15[px] > 0 then 1 else (if g15[px + 1] > 0 then 1 else (if g15[px + 2] > 0 then 1 else 0))) else (if py == 13 then (if g14[px] > 0 then 1 else (if g14[px + 1] > 0 then 1 else (if g14[px + 2] > 0 then 1 else 0))) else (if py == 12 then (if g13[px] > 0 then 1 else (if g13[px + 1] > 0 then 1 else (if g13[px + 2] > 0 then 1 else 0))) else 0))))))
-- Gravity tick
every 33 -> gravityTick = if paused then gravityTick else (if gameOver then gravityTick else gravityTick + 1)
-- Auto-drop: only if NOT blocked
every 33 -> py = if paused then py else (if gameOver then py else (if blocked then py else (if gravityTick > dropInterval then (if py < 18 then py + 1 else py) else py)))
every 33 -> gravityTick = if gravityTick > dropInterval then 0 else gravityTick
-- Lock detection: start counting when blocked
every 33 -> lockTick = if blocked then lockTick + 1 else 0
-- ── Freeze piece into grid at py position on lock ──
-- The piece top-row cells go into row py, bottom-row cell into py+1
-- For T-piece: (px,py), (px+1,py), (px+2,py) and (px+1,py+1)
-- We write the top 3 cells into grid[py] and bottom cell into grid[py+1]
-- Freeze top cells into grid row py (for each possible py value)
-- Row 19
every 33 -> g19[px] = if lockTick == 3 then (if py == 19 then piece else g19[px]) else g19[px]
every 33 -> g19[px + 1] = if lockTick == 3 then (if py == 19 then piece else g19[px + 1]) else g19[px + 1]
every 33 -> g19[px + 2] = if lockTick == 3 then (if py == 19 then piece else g19[px + 2]) else g19[px + 2]
-- Row 18
every 33 -> g18[px] = if lockTick == 3 then (if py == 18 then piece else g18[px]) else g18[px]
every 33 -> g18[px + 1] = if lockTick == 3 then (if py == 18 then piece else g18[px + 1]) else g18[px + 1]
every 33 -> g18[px + 2] = if lockTick == 3 then (if py == 18 then piece else g18[px + 2]) else g18[px + 2]
-- Row 17
every 33 -> g17[px] = if lockTick == 3 then (if py == 17 then piece else g17[px]) else g17[px]
every 33 -> g17[px + 1] = if lockTick == 3 then (if py == 17 then piece else g17[px + 1]) else g17[px + 1]
every 33 -> g17[px + 2] = if lockTick == 3 then (if py == 17 then piece else g17[px + 2]) else g17[px + 2]
-- Row 16
every 33 -> g16[px] = if lockTick == 3 then (if py == 16 then piece else g16[px]) else g16[px]
every 33 -> g16[px + 1] = if lockTick == 3 then (if py == 16 then piece else g16[px + 1]) else g16[px + 1]
every 33 -> g16[px + 2] = if lockTick == 3 then (if py == 16 then piece else g16[px + 2]) else g16[px + 2]
-- Row 15
every 33 -> g15[px] = if lockTick == 3 then (if py == 15 then piece else g15[px]) else g15[px]
every 33 -> g15[px + 1] = if lockTick == 3 then (if py == 15 then piece else g15[px + 1]) else g15[px + 1]
every 33 -> g15[px + 2] = if lockTick == 3 then (if py == 15 then piece else g15[px + 2]) else g15[px + 2]
-- Row 14
every 33 -> g14[px] = if lockTick == 3 then (if py == 14 then piece else g14[px]) else g14[px]
every 33 -> g14[px + 1] = if lockTick == 3 then (if py == 14 then piece else g14[px + 1]) else g14[px + 1]
every 33 -> g14[px + 2] = if lockTick == 3 then (if py == 14 then piece else g14[px + 2]) else g14[px + 2]
-- Row 13
every 33 -> g13[px] = if lockTick == 3 then (if py == 13 then piece else g13[px]) else g13[px]
every 33 -> g13[px + 1] = if lockTick == 3 then (if py == 13 then piece else g13[px + 1]) else g13[px + 1]
every 33 -> g13[px + 2] = if lockTick == 3 then (if py == 13 then piece else g13[px + 2]) else g13[px + 2]
-- Freeze T-piece bottom center cell into grid[py+1]
-- (T has a cell at px+1, py+1; also applicable to O, S, Z shapes)
every 33 -> g19[px + 1] = if lockTick == 3 then (if py == 18 then (if piece == 6 then 6 else g19[px + 1]) else g19[px + 1]) else g19[px + 1]
every 33 -> g18[px + 1] = if lockTick == 3 then (if py == 17 then (if piece == 6 then 6 else g18[px + 1]) else g18[px + 1]) else g18[px + 1]
every 33 -> g17[px + 1] = if lockTick == 3 then (if py == 16 then (if piece == 6 then 6 else g17[px + 1]) else g17[px + 1]) else g17[px + 1]
every 33 -> g16[px + 1] = if lockTick == 3 then (if py == 15 then (if piece == 6 then 6 else g16[px + 1]) else g16[px + 1]) else g16[px + 1]
every 33 -> g15[px + 1] = if lockTick == 3 then (if py == 14 then (if piece == 6 then 6 else g15[px + 1]) else g15[px + 1]) else g15[px + 1]
every 33 -> g14[px + 1] = if lockTick == 3 then (if py == 13 then (if piece == 6 then 6 else g14[px + 1]) else g14[px + 1]) else g14[px + 1]
-- Spawn new piece after lock
every 33 -> piece = if lockTick == 3 then nextPiece else piece
every 33 -> nextPiece = if lockTick == 3 then (gravityTick % 7 + 1) else nextPiece
every 33 -> py = if lockTick == 3 then 0 else py
every 33 -> px = if lockTick == 3 then 3 else px
every 33 -> rotation = if lockTick == 3 then 0 else rotation
every 33 -> score = if lockTick == 3 then score + 10 else score
-- Line clear: row 19 full (check 5 sample cells)
every 33 -> score = if g19[0] > 0 then (if g19[2] > 0 then (if g19[4] > 0 then (if g19[6] > 0 then (if g19[8] > 0 then score + 100 else score) else score) else score) else score) else score
every 33 -> lines = if g19[0] > 0 then (if g19[2] > 0 then (if g19[4] > 0 then (if g19[6] > 0 then (if g19[8] > 0 then lines + 1 else lines) else lines) else lines) else lines) else lines
-- Shift rows down on clear: copy row 18 into row 19
every 33 -> g19[0] = if g19[0] > 0 then (if g19[4] > 0 then (if g19[8] > 0 then g18[0] else g19[0]) else g19[0]) else g19[0]
every 33 -> g19[1] = if g19[0] > 0 then (if g19[4] > 0 then (if g19[8] > 0 then g18[1] else g19[1]) else g19[1]) else g19[1]
every 33 -> g19[2] = if g19[0] > 0 then (if g19[4] > 0 then (if g19[8] > 0 then g18[2] else g19[2]) else g19[2]) else g19[2]
every 33 -> g19[3] = if g19[0] > 0 then (if g19[4] > 0 then (if g19[8] > 0 then g18[3] else g19[3]) else g19[3]) else g19[3]
every 33 -> g19[4] = if g19[0] > 0 then (if g19[4] > 0 then (if g19[8] > 0 then g18[4] else g19[4]) else g19[4]) else g19[4]
every 33 -> g19[5] = if g19[0] > 0 then (if g19[4] > 0 then (if g19[8] > 0 then g18[5] else g19[5]) else g19[5]) else g19[5]
every 33 -> g19[6] = if g19[0] > 0 then (if g19[4] > 0 then (if g19[8] > 0 then g18[6] else g19[6]) else g19[6]) else g19[6]
every 33 -> g19[7] = if g19[0] > 0 then (if g19[4] > 0 then (if g19[8] > 0 then g18[7] else g19[7]) else g19[7]) else g19[7]
every 33 -> g19[8] = if g19[0] > 0 then (if g19[4] > 0 then (if g19[8] > 0 then g18[8] else g19[8]) else g19[8]) else g19[8]
every 33 -> g19[9] = if g19[0] > 0 then (if g19[4] > 0 then (if g19[8] > 0 then g18[9] else g19[9]) else g19[9]) else g19[9]
-- Level up
every 33 -> level = if lines > 0 then (lines / 10 + 1) else 1
every 33 -> dropInterval = if level > 9 then 3 else (27 - level * 3)
-- Game over: any cell in row 0 filled
every 33 -> gameOver = if g0[3] > 0 then 1 else (if g0[4] > 0 then 1 else (if g0[5] > 0 then 1 else gameOver))
-- ================================================================
-- LAYER 5: SOUND SIGNALS
-- ================================================================
every 33 -> play_tone(if lockTick == 3 then 220 else 0, 80)
every 33 -> play_tone(if gameOver then 110 else 0, 500, "sawtooth")
-- ================================================================
-- LAYER 6: STREAM SIGNALS — Spectator broadcast
-- ================================================================
stream tetris on "ws://localhost:9100/peer/tetris" {
mode: signal,
output: piece, rotation, px, py, nextPiece, score, lines, level, gameOver, paused, g0, g1, g2, g3, g4, g5, g6, g7, g8, g9, g10, g11, g12, g13, g14, g15, g16, g17, g18, g19
}
-- ================================================================
-- VIEW: All 6 signal layers composited into one display
-- ================================================================
view tetris_game = column [
text "DreamStack Tetris" { variant: "title" }
text "Signal Composition Demo" { variant: "subtitle" }
-- Status bar (Layer 5 UI signals <- Layer 2 physics signals)
row [
Badge { label: "Score: {score}", variant: "success" }
Badge { label: "Lines: {lines}", variant: "info" }
Badge { label: "Level: {level}", variant: "warning" }
Badge { label: "Next: {nextPiece}", variant: "default" }
]
row [
-- Main Board: 300x600 (10x20 cells at 30px each)
stack { style: "position:relative; width:300px; height:600px; background:linear-gradient(180deg,#0a0a1a,#111133); border:2px solid #334155; border-radius:12px; overflow:hidden; box-shadow:0 0 60px rgba(99,102,241,0.15)" } [
-- Grid lines (subtle, UI-only signals)
for r in [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19] ->
column { style: "position:absolute; width:300px; height:1px; top:{r * 30}px; background:rgba(99,102,241,0.08)" } []
for k in [0,1,2,3,4,5,6,7,8,9] ->
column { style: "position:absolute; width:1px; height:600px; left:{k * 30}px; background:rgba(99,102,241,0.08)" } []
-- Frozen grid: row 13 (Layer 1 data -> Layer 5 UI)
for c in [0,1,2,3,4,5,6,7,8,9] ->
column { style: "position:absolute; width:28px; height:28px; top:391px; left:{c * 30 + 1}px; border-radius:4px; background:linear-gradient(180deg,#a855f7,#7c3aed); opacity:{if g13[c] > 0 then 1 else 0}; transition:opacity 0.15s; box-shadow:0 0 8px rgba(168,85,247,0.4)" } []
-- Frozen grid: row 14
for c in [0,1,2,3,4,5,6,7,8,9] ->
column { style: "position:absolute; width:28px; height:28px; top:421px; left:{c * 30 + 1}px; border-radius:4px; background:linear-gradient(180deg,#a855f7,#7c3aed); opacity:{if g14[c] > 0 then 1 else 0}; transition:opacity 0.15s; box-shadow:0 0 8px rgba(168,85,247,0.4)" } []
-- Frozen grid: row 15
for c in [0,1,2,3,4,5,6,7,8,9] ->
column { style: "position:absolute; width:28px; height:28px; top:451px; left:{c * 30 + 1}px; border-radius:4px; background:linear-gradient(180deg,#a855f7,#7c3aed); opacity:{if g15[c] > 0 then 1 else 0}; transition:opacity 0.15s; box-shadow:0 0 8px rgba(168,85,247,0.4)" } []
-- Frozen grid: row 16
for c in [0,1,2,3,4,5,6,7,8,9] ->
column { style: "position:absolute; width:28px; height:28px; top:481px; left:{c * 30 + 1}px; border-radius:4px; background:linear-gradient(180deg,#a855f7,#7c3aed); opacity:{if g16[c] > 0 then 1 else 0}; transition:opacity 0.15s; box-shadow:0 0 8px rgba(168,85,247,0.4)" } []
-- Frozen grid: row 17
for c in [0,1,2,3,4,5,6,7,8,9] ->
column { style: "position:absolute; width:28px; height:28px; top:511px; left:{c * 30 + 1}px; border-radius:4px; background:linear-gradient(180deg,#a855f7,#7c3aed); opacity:{if g17[c] > 0 then 1 else 0}; transition:opacity 0.15s; box-shadow:0 0 8px rgba(168,85,247,0.4)" } []
-- Frozen grid: row 18
for c in [0,1,2,3,4,5,6,7,8,9] ->
column { style: "position:absolute; width:28px; height:28px; top:541px; left:{c * 30 + 1}px; border-radius:4px; background:linear-gradient(180deg,#a855f7,#7c3aed); opacity:{if g18[c] > 0 then 1 else 0}; transition:opacity 0.15s; box-shadow:0 0 8px rgba(168,85,247,0.4)" } []
-- Frozen grid: row 19 (bottom)
for c in [0,1,2,3,4,5,6,7,8,9] ->
column { style: "position:absolute; width:28px; height:28px; top:571px; left:{c * 30 + 1}px; border-radius:4px; background:linear-gradient(180deg,#a855f7,#7c3aed); opacity:{if g19[c] > 0 then 1 else 0}; transition:opacity 0.15s; box-shadow:0 0 8px rgba(168,85,247,0.4)" } []
-- Active piece: 4 cells (Layer 1 data + Layer 3 input -> Layer 5 UI)
-- Cell 0: top-left of piece
column { style: "position:absolute; width:28px; height:28px; border-radius:4px; background:linear-gradient(180deg,#c084fc,#a855f7); box-shadow:0 0 12px rgba(168,85,247,0.5); transition:left 0.05s,top 0.05s; top:{(if piece == 1 then py + 1 else py) * 30 + 1}px; left:{(if piece == 5 then px + 1 else px) * 30 + 1}px" } []
-- Cell 1
column { style: "position:absolute; width:28px; height:28px; border-radius:4px; background:linear-gradient(180deg,#c084fc,#a855f7); box-shadow:0 0 12px rgba(168,85,247,0.5); transition:left 0.05s,top 0.05s; top:{(if piece == 1 then py + 1 else py) * 30 + 1}px; left:{(if piece == 2 then px else px + 1) * 30 + 1}px" } []
-- Cell 2
column { style: "position:absolute; width:28px; height:28px; border-radius:4px; background:linear-gradient(180deg,#c084fc,#a855f7); box-shadow:0 0 12px rgba(168,85,247,0.5); transition:left 0.05s,top 0.05s; top:{(if piece == 1 then py + 1 else py) * 30 + 1}px; left:{(if piece == 1 then px + 2 else (if piece == 3 then px + 2 else px + 2)) * 30 + 1}px" } []
-- Cell 3 (second row of piece, e.g. T-piece center bottom)
column { style: "position:absolute; width:28px; height:28px; border-radius:4px; background:linear-gradient(180deg,#c084fc,#a855f7); box-shadow:0 0 12px rgba(168,85,247,0.5); transition:left 0.05s,top 0.05s; top:{(if piece == 1 then py + 1 else py + 1) * 30 + 1}px; left:{(if piece == 1 then px + 3 else (if piece == 6 then px + 1 else (if piece == 4 then px + 2 else px + 2))) * 30 + 1}px" } []
-- Ghost piece (Layer 4 derived -> Layer 5 UI)
-- Shows where piece will land (row 18)
column { style: "position:absolute; width:28px; height:28px; border-radius:4px; border:2px dashed rgba(168,85,247,0.3); top:{18 * 30 + 1}px; left:{px * 30 + 1}px" } []
column { style: "position:absolute; width:28px; height:28px; border-radius:4px; border:2px dashed rgba(168,85,247,0.3); top:{18 * 30 + 1}px; left:{(px + 1) * 30 + 1}px" } []
column { style: "position:absolute; width:28px; height:28px; border-radius:4px; border:2px dashed rgba(168,85,247,0.3); top:{18 * 30 + 1}px; left:{(px + 2) * 30 + 1}px" } []
]
-- Side panel
column [
-- Next piece preview (Layer 1 data -> Layer 5 UI)
text "NEXT" { variant: "subtitle" }
stack { style: "position:relative; width:120px; height:90px; background:#0f0f23; border-radius:8px; border:1px solid #334155" } [
column { style: "position:absolute; width:24px; height:24px; border-radius:3px; top:20px; left:15px; background:linear-gradient(180deg,#c084fc,#a855f7); box-shadow:0 0 6px rgba(168,85,247,0.4)" } []
column { style: "position:absolute; width:24px; height:24px; border-radius:3px; top:20px; left:41px; background:linear-gradient(180deg,#c084fc,#a855f7); box-shadow:0 0 6px rgba(168,85,247,0.4)" } []
column { style: "position:absolute; width:24px; height:24px; border-radius:3px; top:20px; left:67px; background:linear-gradient(180deg,#c084fc,#a855f7); box-shadow:0 0 6px rgba(168,85,247,0.4)" } []
column { style: "position:absolute; width:24px; height:24px; border-radius:3px; top:46px; left:41px; background:linear-gradient(180deg,#c084fc,#a855f7); opacity:{if nextPiece == 6 then 1 else (if nextPiece == 4 then 1 else 0)}" } []
]
-- Stats (Layer 2 physics -> Layer 5 UI)
text "STATS" { variant: "subtitle" }
text "Score: {score}"
text "Lines: {lines}"
text "Level: {level}"
text "Speed: {dropInterval}" { variant: "muted" }
text "Ticks: {gravityTick}" { variant: "muted" }
-- Signal debug panel (live values)
text "SIGNALS" { variant: "subtitle" }
text "piece:{piece} px:{px} py:{py}" { variant: "muted" }
text "rot:{rotation} blk:{blocked}" { variant: "muted" }
text "lock:{lockTick} grav:{gravityTick}" { variant: "muted" }
text "score:{score} lines:{lines}" { variant: "muted" }
text "level:{level} speed:{dropInterval}" { variant: "muted" }
text "g13:{g13}" { variant: "muted" }
text "g14:{g14}" { variant: "muted" }
text "g15:{g15}" { variant: "muted" }
text "g16:{g16}" { variant: "muted" }
text "g17:{g17}" { variant: "muted" }
text "g18:{g18}" { variant: "muted" }
text "g19:{g19}" { variant: "muted" }
]
]
-- Controls (Layer 3 input -> signal writes)
row [
button (if paused then "Resume" else "Pause") {
click: paused = if paused then 0 else 1,
variant: "primary"
}
button "Left" { click: px = if px > 0 then px - 1 else px, variant: "secondary" }
button "Right" { click: px = if px < 7 then px + 1 else px, variant: "secondary" }
button "Drop" { click: py = if blocked then py else 18, variant: "secondary" }
button "Rotate" { click: rotation = (rotation + 1) % 4, variant: "secondary" }
button "Reset" {
click: score = 0; lines = 0; level = 1; gameOver = 0; paused = 0; piece = 6; nextPiece = 1; px = 3; py = 0; rotation = 0; g16 = [0,0,0,0,0,0,0,0,0,0]; g17 = [0,0,0,0,0,0,0,0,0,0]; g18 = [0,0,0,0,0,0,0,0,0,0]; g19 = [0,0,0,0,0,0,0,0,0,0],
variant: "destructive"
}
]
when gameOver > 0 -> text "GAME OVER - Score: {score}, Lines: {lines}" { variant: "title" }
text "Arrow keys: Move/Rotate | Space: Hard drop | P: Pause" { variant: "muted" }
text "Streaming: 6 signal layers composited" { variant: "muted" }
]