dreamstack/examples/game-breakout.ds
enzotar 4ac584c81e fix: reactive component props + breakout improvements
Compiler fixes:
- Component props with signal-dependent expressions now wrapped as
  reactive getters (() => expr) at call site
- Component declarations handle fn-typed props via
  { get value() { return props.x(); } } for live reactivity
- Container style: prop wrapped in DS.effect when expr contains .value
- Timer merging: all same-interval 'every' statements grouped into
  single setInterval with one DS.flush()

Breakout game improvements:
- Classic row order: blue top (far), red bottom (near paddle)
- All 5 rows have full collision detection (was only 2)
- Faster ball (4px/frame) and paddle (40px/keypress)
- Score/Lives badges now update in real-time

All 48 examples compile, 136 tests pass
2026-02-27 10:53:27 -08:00

207 lines
18 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- DreamStack Breakout
-- Classic brick-breaker game with keyboard controls, sound effects, and streaming
-- Paddle, ball, and bricks are CSS-positioned elements inside a stack container
--
-- Layout: Blue (top/far) → Green → Yellow → Orange → Red (bottom/near paddle)
-- Ball hits red first, then works up — classic Breakout style
--
-- Run:
-- Tab 1: cargo run -p ds-stream (relay)
-- Tab 2: dreamstack dev examples/game-breakout.ds (player)
import { Badge } from "../registry/components/badge"
-- ── State ──
let paddleX = 250
let ballX = 300
let ballY = 320
let bvx = 4
let bvy = -4
let score = 0
let lives = 3
let paused = 0
-- 5 rows × 10 columns = 50 bricks (1 = alive, 0 = destroyed)
-- r0 = red (bottom, closest to paddle) y=200..216
-- r1 = orange y=180..196
-- r2 = yellow y=160..176
-- r3 = green y=140..156
-- r4 = blue (top, farthest) y=120..136
let r0 = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
let r1 = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
let r2 = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
let r3 = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
let r4 = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
-- ── Keyboard controls ──
on keydown(ev) -> paddleX = if ev.key == "ArrowLeft" then (if paddleX > 0 then paddleX - 40 else paddleX) else (if ev.key == "ArrowRight" then (if paddleX < 520 then paddleX + 40 else paddleX) else paddleX)
on keydown(ev) -> paused = if ev.key == " " then (if paused then 0 else 1) else paused
-- ── Game loop at ~30fps ──
every 33 -> ballX = if paused then ballX else (if lives > 0 then ballX + bvx else ballX)
every 33 -> ballY = if paused then ballY else (if lives > 0 then ballY + bvy else ballY)
-- Wall bounces (left/right/ceiling)
every 33 -> bvx = if ballX < 4 then 4 else bvx
every 33 -> bvx = if ballX > 590 then -4 else bvx
every 33 -> bvy = if ballY < 4 then 4 else bvy
-- Ball lost (bottom) — lose a life and reset
every 33 -> lives = if ballY > 395 then lives - 1 else lives
every 33 -> ballX = if ballY > 395 then 300 else ballX
every 33 -> ballY = if ballY > 395 then 320 else ballY
every 33 -> bvx = if ballY > 395 then 4 else bvx
every 33 -> bvy = if ballY > 395 then -4 else bvy
-- Paddle hit (paddle at y=360)
every 33 -> bvy = if ballY > 348 then (if ballX > paddleX then (if ballX < paddleX + 80 then -4 else bvy) else bvy) else bvy
-- ──────────────────────────────────────────────────────
-- Brick collision for all 5 rows
-- Each row: score+bounce FIRST, then destroy per-brick
-- ──────────────────────────────────────────────────────
-- ── Row 0: RED (bottom, y=200..216) — hit first by ball ──
every 33 -> score = if bvy < 0 then (if ballY < 218 then (if ballY > 198 then (if r0[floor(ballX / 60)] then score + 10 else score) else score) else score) else score
every 33 -> bvy = if bvy < 0 then (if ballY < 218 then (if ballY > 198 then (if r0[floor(ballX / 60)] then 4 else bvy) else bvy) else bvy) else bvy
every 33 -> r0[0] = if ballY < 218 then (if ballY > 198 then (if ballX > 0 then (if ballX < 60 then (if r0[0] then 0 else r0[0]) else r0[0]) else r0[0]) else r0[0]) else r0[0]
every 33 -> r0[1] = if ballY < 218 then (if ballY > 198 then (if ballX > 60 then (if ballX < 120 then (if r0[1] then 0 else r0[1]) else r0[1]) else r0[1]) else r0[1]) else r0[1]
every 33 -> r0[2] = if ballY < 218 then (if ballY > 198 then (if ballX > 120 then (if ballX < 180 then (if r0[2] then 0 else r0[2]) else r0[2]) else r0[2]) else r0[2]) else r0[2]
every 33 -> r0[3] = if ballY < 218 then (if ballY > 198 then (if ballX > 180 then (if ballX < 240 then (if r0[3] then 0 else r0[3]) else r0[3]) else r0[3]) else r0[3]) else r0[3]
every 33 -> r0[4] = if ballY < 218 then (if ballY > 198 then (if ballX > 240 then (if ballX < 300 then (if r0[4] then 0 else r0[4]) else r0[4]) else r0[4]) else r0[4]) else r0[4]
every 33 -> r0[5] = if ballY < 218 then (if ballY > 198 then (if ballX > 300 then (if ballX < 360 then (if r0[5] then 0 else r0[5]) else r0[5]) else r0[5]) else r0[5]) else r0[5]
every 33 -> r0[6] = if ballY < 218 then (if ballY > 198 then (if ballX > 360 then (if ballX < 420 then (if r0[6] then 0 else r0[6]) else r0[6]) else r0[6]) else r0[6]) else r0[6]
every 33 -> r0[7] = if ballY < 218 then (if ballY > 198 then (if ballX > 420 then (if ballX < 480 then (if r0[7] then 0 else r0[7]) else r0[7]) else r0[7]) else r0[7]) else r0[7]
every 33 -> r0[8] = if ballY < 218 then (if ballY > 198 then (if ballX > 480 then (if ballX < 540 then (if r0[8] then 0 else r0[8]) else r0[8]) else r0[8]) else r0[8]) else r0[8]
every 33 -> r0[9] = if ballY < 218 then (if ballY > 198 then (if ballX > 540 then (if ballX < 600 then (if r0[9] then 0 else r0[9]) else r0[9]) else r0[9]) else r0[9]) else r0[9]
-- ── Row 1: ORANGE (y=180..196) ──
every 33 -> score = if bvy < 0 then (if ballY < 198 then (if ballY > 178 then (if r1[floor(ballX / 60)] then score + 10 else score) else score) else score) else score
every 33 -> bvy = if bvy < 0 then (if ballY < 198 then (if ballY > 178 then (if r1[floor(ballX / 60)] then 4 else bvy) else bvy) else bvy) else bvy
every 33 -> r1[0] = if ballY < 198 then (if ballY > 178 then (if ballX > 0 then (if ballX < 60 then (if r1[0] then 0 else r1[0]) else r1[0]) else r1[0]) else r1[0]) else r1[0]
every 33 -> r1[1] = if ballY < 198 then (if ballY > 178 then (if ballX > 60 then (if ballX < 120 then (if r1[1] then 0 else r1[1]) else r1[1]) else r1[1]) else r1[1]) else r1[1]
every 33 -> r1[2] = if ballY < 198 then (if ballY > 178 then (if ballX > 120 then (if ballX < 180 then (if r1[2] then 0 else r1[2]) else r1[2]) else r1[2]) else r1[2]) else r1[2]
every 33 -> r1[3] = if ballY < 198 then (if ballY > 178 then (if ballX > 180 then (if ballX < 240 then (if r1[3] then 0 else r1[3]) else r1[3]) else r1[3]) else r1[3]) else r1[3]
every 33 -> r1[4] = if ballY < 198 then (if ballY > 178 then (if ballX > 240 then (if ballX < 300 then (if r1[4] then 0 else r1[4]) else r1[4]) else r1[4]) else r1[4]) else r1[4]
every 33 -> r1[5] = if ballY < 198 then (if ballY > 178 then (if ballX > 300 then (if ballX < 360 then (if r1[5] then 0 else r1[5]) else r1[5]) else r1[5]) else r1[5]) else r1[5]
every 33 -> r1[6] = if ballY < 198 then (if ballY > 178 then (if ballX > 360 then (if ballX < 420 then (if r1[6] then 0 else r1[6]) else r1[6]) else r1[6]) else r1[6]) else r1[6]
every 33 -> r1[7] = if ballY < 198 then (if ballY > 178 then (if ballX > 420 then (if ballX < 480 then (if r1[7] then 0 else r1[7]) else r1[7]) else r1[7]) else r1[7]) else r1[7]
every 33 -> r1[8] = if ballY < 198 then (if ballY > 178 then (if ballX > 480 then (if ballX < 540 then (if r1[8] then 0 else r1[8]) else r1[8]) else r1[8]) else r1[8]) else r1[8]
every 33 -> r1[9] = if ballY < 198 then (if ballY > 178 then (if ballX > 540 then (if ballX < 600 then (if r1[9] then 0 else r1[9]) else r1[9]) else r1[9]) else r1[9]) else r1[9]
-- ── Row 2: YELLOW (y=160..176) ──
every 33 -> score = if bvy < 0 then (if ballY < 178 then (if ballY > 158 then (if r2[floor(ballX / 60)] then score + 10 else score) else score) else score) else score
every 33 -> bvy = if bvy < 0 then (if ballY < 178 then (if ballY > 158 then (if r2[floor(ballX / 60)] then 4 else bvy) else bvy) else bvy) else bvy
every 33 -> r2[0] = if ballY < 178 then (if ballY > 158 then (if ballX > 0 then (if ballX < 60 then (if r2[0] then 0 else r2[0]) else r2[0]) else r2[0]) else r2[0]) else r2[0]
every 33 -> r2[1] = if ballY < 178 then (if ballY > 158 then (if ballX > 60 then (if ballX < 120 then (if r2[1] then 0 else r2[1]) else r2[1]) else r2[1]) else r2[1]) else r2[1]
every 33 -> r2[2] = if ballY < 178 then (if ballY > 158 then (if ballX > 120 then (if ballX < 180 then (if r2[2] then 0 else r2[2]) else r2[2]) else r2[2]) else r2[2]) else r2[2]
every 33 -> r2[3] = if ballY < 178 then (if ballY > 158 then (if ballX > 180 then (if ballX < 240 then (if r2[3] then 0 else r2[3]) else r2[3]) else r2[3]) else r2[3]) else r2[3]
every 33 -> r2[4] = if ballY < 178 then (if ballY > 158 then (if ballX > 240 then (if ballX < 300 then (if r2[4] then 0 else r2[4]) else r2[4]) else r2[4]) else r2[4]) else r2[4]
every 33 -> r2[5] = if ballY < 178 then (if ballY > 158 then (if ballX > 300 then (if ballX < 360 then (if r2[5] then 0 else r2[5]) else r2[5]) else r2[5]) else r2[5]) else r2[5]
every 33 -> r2[6] = if ballY < 178 then (if ballY > 158 then (if ballX > 360 then (if ballX < 420 then (if r2[6] then 0 else r2[6]) else r2[6]) else r2[6]) else r2[6]) else r2[6]
every 33 -> r2[7] = if ballY < 178 then (if ballY > 158 then (if ballX > 420 then (if ballX < 480 then (if r2[7] then 0 else r2[7]) else r2[7]) else r2[7]) else r2[7]) else r2[7]
every 33 -> r2[8] = if ballY < 178 then (if ballY > 158 then (if ballX > 480 then (if ballX < 540 then (if r2[8] then 0 else r2[8]) else r2[8]) else r2[8]) else r2[8]) else r2[8]
every 33 -> r2[9] = if ballY < 178 then (if ballY > 158 then (if ballX > 540 then (if ballX < 600 then (if r2[9] then 0 else r2[9]) else r2[9]) else r2[9]) else r2[9]) else r2[9]
-- ── Row 3: GREEN (y=140..156) ──
every 33 -> score = if bvy < 0 then (if ballY < 158 then (if ballY > 138 then (if r3[floor(ballX / 60)] then score + 10 else score) else score) else score) else score
every 33 -> bvy = if bvy < 0 then (if ballY < 158 then (if ballY > 138 then (if r3[floor(ballX / 60)] then 4 else bvy) else bvy) else bvy) else bvy
every 33 -> r3[0] = if ballY < 158 then (if ballY > 138 then (if ballX > 0 then (if ballX < 60 then (if r3[0] then 0 else r3[0]) else r3[0]) else r3[0]) else r3[0]) else r3[0]
every 33 -> r3[1] = if ballY < 158 then (if ballY > 138 then (if ballX > 60 then (if ballX < 120 then (if r3[1] then 0 else r3[1]) else r3[1]) else r3[1]) else r3[1]) else r3[1]
every 33 -> r3[2] = if ballY < 158 then (if ballY > 138 then (if ballX > 120 then (if ballX < 180 then (if r3[2] then 0 else r3[2]) else r3[2]) else r3[2]) else r3[2]) else r3[2]
every 33 -> r3[3] = if ballY < 158 then (if ballY > 138 then (if ballX > 180 then (if ballX < 240 then (if r3[3] then 0 else r3[3]) else r3[3]) else r3[3]) else r3[3]) else r3[3]
every 33 -> r3[4] = if ballY < 158 then (if ballY > 138 then (if ballX > 240 then (if ballX < 300 then (if r3[4] then 0 else r3[4]) else r3[4]) else r3[4]) else r3[4]) else r3[4]
every 33 -> r3[5] = if ballY < 158 then (if ballY > 138 then (if ballX > 300 then (if ballX < 360 then (if r3[5] then 0 else r3[5]) else r3[5]) else r3[5]) else r3[5]) else r3[5]
every 33 -> r3[6] = if ballY < 158 then (if ballY > 138 then (if ballX > 360 then (if ballX < 420 then (if r3[6] then 0 else r3[6]) else r3[6]) else r3[6]) else r3[6]) else r3[6]
every 33 -> r3[7] = if ballY < 158 then (if ballY > 138 then (if ballX > 420 then (if ballX < 480 then (if r3[7] then 0 else r3[7]) else r3[7]) else r3[7]) else r3[7]) else r3[7]
every 33 -> r3[8] = if ballY < 158 then (if ballY > 138 then (if ballX > 480 then (if ballX < 540 then (if r3[8] then 0 else r3[8]) else r3[8]) else r3[8]) else r3[8]) else r3[8]
every 33 -> r3[9] = if ballY < 158 then (if ballY > 138 then (if ballX > 540 then (if ballX < 600 then (if r3[9] then 0 else r3[9]) else r3[9]) else r3[9]) else r3[9]) else r3[9]
-- ── Row 4: BLUE (top, y=120..136) — hit last by ball ──
every 33 -> score = if bvy < 0 then (if ballY < 138 then (if ballY > 118 then (if r4[floor(ballX / 60)] then score + 10 else score) else score) else score) else score
every 33 -> bvy = if bvy < 0 then (if ballY < 138 then (if ballY > 118 then (if r4[floor(ballX / 60)] then 4 else bvy) else bvy) else bvy) else bvy
every 33 -> r4[0] = if ballY < 138 then (if ballY > 118 then (if ballX > 0 then (if ballX < 60 then (if r4[0] then 0 else r4[0]) else r4[0]) else r4[0]) else r4[0]) else r4[0]
every 33 -> r4[1] = if ballY < 138 then (if ballY > 118 then (if ballX > 60 then (if ballX < 120 then (if r4[1] then 0 else r4[1]) else r4[1]) else r4[1]) else r4[1]) else r4[1]
every 33 -> r4[2] = if ballY < 138 then (if ballY > 118 then (if ballX > 120 then (if ballX < 180 then (if r4[2] then 0 else r4[2]) else r4[2]) else r4[2]) else r4[2]) else r4[2]
every 33 -> r4[3] = if ballY < 138 then (if ballY > 118 then (if ballX > 180 then (if ballX < 240 then (if r4[3] then 0 else r4[3]) else r4[3]) else r4[3]) else r4[3]) else r4[3]
every 33 -> r4[4] = if ballY < 138 then (if ballY > 118 then (if ballX > 240 then (if ballX < 300 then (if r4[4] then 0 else r4[4]) else r4[4]) else r4[4]) else r4[4]) else r4[4]
every 33 -> r4[5] = if ballY < 138 then (if ballY > 118 then (if ballX > 300 then (if ballX < 360 then (if r4[5] then 0 else r4[5]) else r4[5]) else r4[5]) else r4[5]) else r4[5]
every 33 -> r4[6] = if ballY < 138 then (if ballY > 118 then (if ballX > 360 then (if ballX < 420 then (if r4[6] then 0 else r4[6]) else r4[6]) else r4[6]) else r4[6]) else r4[6]
every 33 -> r4[7] = if ballY < 138 then (if ballY > 118 then (if ballX > 420 then (if ballX < 480 then (if r4[7] then 0 else r4[7]) else r4[7]) else r4[7]) else r4[7]) else r4[7]
every 33 -> r4[8] = if ballY < 138 then (if ballY > 118 then (if ballX > 480 then (if ballX < 540 then (if r4[8] then 0 else r4[8]) else r4[8]) else r4[8]) else r4[8]) else r4[8]
every 33 -> r4[9] = if ballY < 138 then (if ballY > 118 then (if ballX > 540 then (if ballX < 600 then (if r4[9] then 0 else r4[9]) else r4[9]) else r4[9]) else r4[9]) else r4[9]
-- ── Sound effects ──
every 33 -> play_tone(if ballY > 348 then (if ballX > paddleX then (if ballX < paddleX + 80 then 440 else 0) else 0) else 0, 60)
every 33 -> play_tone(if ballY < 218 then (if ballY > 118 then 880 else 0) else 0, 40)
every 33 -> play_tone(if ballY > 395 then 110 else 0, 300, "sawtooth")
-- Stream for viewers
stream breakout on "ws://localhost:9100/peer/breakout" {
mode: signal,
output: paddleX, ballX, ballY, score, lives, paused, r0, r1, r2, r3, r4
}
view breakout_game = column [
text "🧱 DreamStack Breakout" { variant: "title" }
row [
Badge { label: "Score: {score}", variant: "success" }
Badge { label: "Lives: {lives}", variant: (if lives > 1 then "info" else "error") }
]
-- ── The Court ──
stack { style: "position:relative; width:600px; height:420px; background:linear-gradient(180deg,#0c0c1d,#1a1a3e); border:2px solid #334155; border-radius:16px; margin:0.5rem auto; overflow:hidden; box-shadow:0 0 40px rgba(168,85,247,0.15)" } [
-- Row 4: Blue bricks (TOP — farthest from paddle, hit last)
for i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ->
column { style: "position:absolute; width:56px; height:16px; border-radius:4px; top:120px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#60a5fa,#3b82f6); opacity:{if r4[i] then 1 else 0}; transition:opacity 0.2s" } []
-- Row 3: Green bricks
for i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ->
column { style: "position:absolute; width:56px; height:16px; border-radius:4px; top:140px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#4ade80,#22c55e); opacity:{if r3[i] then 1 else 0}; transition:opacity 0.2s" } []
-- Row 2: Yellow bricks
for i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ->
column { style: "position:absolute; width:56px; height:16px; border-radius:4px; top:160px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#fde68a,#facc15); opacity:{if r2[i] then 1 else 0}; transition:opacity 0.2s" } []
-- Row 1: Orange bricks
for i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ->
column { style: "position:absolute; width:56px; height:16px; border-radius:4px; top:180px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#fb923c,#f97316); opacity:{if r1[i] then 1 else 0}; transition:opacity 0.2s" } []
-- Row 0: Red bricks (BOTTOM — closest to paddle, hit first)
for i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ->
column { style: "position:absolute; width:56px; height:16px; border-radius:4px; top:200px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#f87171,#ef4444); opacity:{if r0[i] then 1 else 0}; transition:opacity 0.2s" } []
-- Paddle (purple glow)
column { style: "position:absolute; width:80px; height:12px; top:360px; background:linear-gradient(180deg,#c084fc,#a855f7); border-radius:6px; box-shadow:0 0 12px rgba(168,85,247,0.5); transition:left 0.05s", left: paddleX } []
-- Ball (white glow)
column { style: "position:absolute; width:12px; height:12px; background:radial-gradient(circle,#fff,#e2e8f0); border-radius:50%; box-shadow:0 0 12px rgba(255,255,255,0.6)", top: ballY, left: ballX } []
]
-- Controls
row [
button (if paused then "▶ Resume" else "⏸ Pause") {
click: paused = if paused then 0 else 1,
variant: "primary"
}
button "⬅️ Left" {
click: paddleX = if paddleX > 0 then paddleX - 40 else paddleX,
variant: "secondary"
}
button "➡️ Right" {
click: paddleX = if paddleX < 520 then paddleX + 40 else paddleX,
variant: "secondary"
}
button "🔄 Reset" {
click: score = 0; lives = 3; paused = 0; bvx = 4; bvy = -4; ballX = 300; ballY = 320; paddleX = 250; r0[0] = 1; r0[1] = 1; r0[2] = 1; r0[3] = 1; r0[4] = 1; r0[5] = 1; r0[6] = 1; r0[7] = 1; r0[8] = 1; r0[9] = 1; r1[0] = 1; r1[1] = 1; r1[2] = 1; r1[3] = 1; r1[4] = 1; r1[5] = 1; r1[6] = 1; r1[7] = 1; r1[8] = 1; r1[9] = 1; r2[0] = 1; r2[1] = 1; r2[2] = 1; r2[3] = 1; r2[4] = 1; r2[5] = 1; r2[6] = 1; r2[7] = 1; r2[8] = 1; r2[9] = 1; r3[0] = 1; r3[1] = 1; r3[2] = 1; r3[3] = 1; r3[4] = 1; r3[5] = 1; r3[6] = 1; r3[7] = 1; r3[8] = 1; r3[9] = 1; r4[0] = 1; r4[1] = 1; r4[2] = 1; r4[3] = 1; r4[4] = 1; r4[5] = 1; r4[6] = 1; r4[7] = 1; r4[8] = 1; r4[9] = 1,
variant: "destructive"
}
]
when lives < 1 -> text "💀 GAME OVER — Final Score: {score}" { variant: "title" }
text "⬅➡ Arrow keys to move • Space to pause" { variant: "muted" }
text "🔴 Streaming via relay — spectators see real-time game state" { variant: "muted" }
]