dreamstack/examples/game-breakout.ds

208 lines
18 KiB
Text
Raw Normal View History

-- 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" }
]