From 075c4a20fe21f21f21a2e03e1e80279e716cb68d Mon Sep 17 00:00:00 2001 From: enzotar Date: Fri, 27 Feb 2026 12:13:22 -0800 Subject: [PATCH] =?UTF-8?q?feat:=20tetris=20=E2=80=94=20signal=20compositi?= =?UTF-8?q?on=20showcase=20with=206=20reactive=20layers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit game-tetris.ds demonstrates DreamStack signal composition: - Layer 1: Data signals (20 grid rows, piece state, next piece) - Layer 2: Physics signals (gravity tick, drop speed, level scaling) - Layer 3: Input signals (keyboard events -> movement commands) - Layer 4: Derived signals (lock detection, line clear, game over) - Layer 5: Sound signals (lock, clear, game over tones) - Layer 6: Stream signals (30+ signals broadcast for spectators) Board: 10x20 grid, 7 piece types, ghost piece indicator Controls: Arrow keys, space (hard drop), P (pause) Side panel: next piece preview, stats, signal layer key Compiles to 76KB HTML+JS --- examples/game-tetris.ds | 269 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 examples/game-tetris.ds diff --git a/examples/game-tetris.ds b/examples/game-tetris.ds new file mode 100644 index 0000000..3935d97 --- /dev/null +++ b/examples/game-tetris.ds @@ -0,0 +1,269 @@ +-- 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" } +]