-- 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 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 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 — Gravity, lock, line clear -- ================================================================ -- Gravity tick every 33 -> gravityTick = if paused then gravityTick else (if gameOver then gravityTick else gravityTick + 1) -- Auto-drop every 33 -> py = if paused then py else (if gameOver 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 every 33 -> lockTick = if py > 17 then lockTick + 1 else 0 -- Freeze piece into grid rows on lock (simplified: T, I, O, J, L, S, Z) -- Row 18 cells every 33 -> g18[px] = if lockTick == 3 then (if py > 17 then piece else g18[px]) else g18[px] every 33 -> g18[px + 1] = if lockTick == 3 then (if py > 17 then piece else g18[px + 1]) else g18[px + 1] every 33 -> g18[px + 2] = if lockTick == 3 then (if py > 17 then (if piece == 4 then g18[px + 2] else piece) else g18[px + 2]) else g18[px + 2] -- Row 17 cells (T-piece has cell at (px+1, py+1)) every 33 -> g17[px + 1] = if lockTick == 3 then (if py > 16 then (if piece == 6 then 6 else (if piece == 4 then 4 else g17[px + 1])) else g17[px + 1]) else g17[px + 1] -- Row 17 freeze for py==17 every 33 -> g17[px] = if py == 17 then (if lockTick == 3 then piece else g17[px]) else g17[px] every 33 -> g17[px + 2] = if py == 17 then (if lockTick == 3 then (if piece == 4 then g17[px + 2] else piece) else g17[px + 2]) else g17[px + 2] -- Row 16 freeze for py==16 every 33 -> g16[px] = if py == 16 then (if lockTick == 3 then piece else g16[px]) else g16[px] every 33 -> g16[px + 1] = if py == 16 then (if lockTick == 3 then piece else g16[px + 1]) else g16[px + 1] every 33 -> g16[px + 2] = if py == 16 then (if lockTick == 3 then (if piece == 4 then g16[px + 2] else piece) else g16[px + 2]) else g16[px + 2] -- 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 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] -- 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 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 16 (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: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 layer key text "SIGNALS" { variant: "subtitle" } text "Data: 20 grid rows" { variant: "muted" } text "Physics: gravity" { variant: "muted" } text "Input: keyboard" { variant: "muted" } text "Derived: lock, clear" { variant: "muted" } text "Sound: 3 tones" { variant: "muted" } text "Stream: 30+ signals" { 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 = 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" } ]