dreamstack/examples/game-breakout.ds
enzotar e8bbfcbcb7 perf: merge same-interval timers + breakout game + beats viewer
Performance:
- All every N statements with same interval now merge into one setInterval
- Pong: 24 timers → 1, Breakout: 38 timers → 1 (single DS.flush per frame)

New examples:
- game-breakout.ds: brick-breaker with 5×10 colored bricks, keyboard, audio
- beats-viewer.ds: step sequencer spectator via relay

Fixes:
- _playTone/_playNoise early-exit when freq/duration <= 0
- Breakout score race: score+bounce checks before brick destruction
- Score sound effects in pong (220Hz/440Hz sawtooth)

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

158 lines
11 KiB
Text
Raw 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
--
-- 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 = 350
let bvx = 3
let bvy = -3
let score = 0
let lives = 3
let paused = 0
-- 5 rows × 10 columns = 50 bricks (1 = alive, 0 = destroyed)
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 - 30 else paddleX) else (if ev.key == "ArrowRight" then (if paddleX < 520 then paddleX + 30 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 3 else bvx
every 33 -> bvx = if ballX > 590 then -3 else bvx
every 33 -> bvy = if ballY < 4 then 3 else bvy
-- Ball lost (bottom) — lose a life and reset
every 33 -> lives = if ballY > 410 then lives - 1 else lives
every 33 -> ballX = if ballY > 410 then 300 else ballX
every 33 -> ballY = if ballY > 410 then 350 else ballY
every 33 -> bvx = if ballY > 410 then 3 else bvx
every 33 -> bvy = if ballY > 410 then -3 else bvy
-- Paddle hit
every 33 -> bvy = if ballY > 365 then (if ballX > paddleX then (if ballX < paddleX + 80 then -3 else bvy) else bvy) else bvy
-- ── Brick collision: row 0 (y=30..48) ──
-- Score + bounce FIRST (before brick is cleared)
every 33 -> score = if bvy < 0 then (if ballY < 48 then (if ballY > 28 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 < 48 then (if ballY > 28 then (if r0[floor(ballX / 60)] then 3 else bvy) else bvy) else bvy) else bvy
-- Then destroy the brick
every 33 -> r0[0] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 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]) else r0[0]
every 33 -> r0[1] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 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]) else r0[1]
every 33 -> r0[2] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 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]) else r0[2]
every 33 -> r0[3] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 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]) else r0[3]
every 33 -> r0[4] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 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]) else r0[4]
every 33 -> r0[5] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 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]) else r0[5]
every 33 -> r0[6] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 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]) else r0[6]
every 33 -> r0[7] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 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]) else r0[7]
every 33 -> r0[8] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 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]) else r0[8]
every 33 -> r0[9] = if bvy < 0 then (if ballY < 48 then (if ballY > 28 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]) else r0[9]
-- ── Brick collision: row 1 (y=50..68) ──
-- Score + bounce FIRST
every 33 -> score = if bvy < 0 then (if ballY < 68 then (if ballY > 48 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 < 68 then (if ballY > 48 then (if r1[floor(ballX / 60)] then 3 else bvy) else bvy) else bvy) else bvy
-- Then destroy the brick
every 33 -> r1[0] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 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]) else r1[0]
every 33 -> r1[1] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 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]) else r1[1]
every 33 -> r1[2] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 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]) else r1[2]
every 33 -> r1[3] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 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]) else r1[3]
every 33 -> r1[4] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 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]) else r1[4]
every 33 -> r1[5] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 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]) else r1[5]
every 33 -> r1[6] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 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]) else r1[6]
every 33 -> r1[7] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 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]) else r1[7]
every 33 -> r1[8] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 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]) else r1[8]
every 33 -> r1[9] = if bvy < 0 then (if ballY < 68 then (if ballY > 48 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]) else r1[9]
-- ── Sound effects ──
every 33 -> play_tone(if ballY > 365 then (if ballX > paddleX then (if ballX < paddleX + 80 then 440 else 0) else 0) else 0, 60)
every 33 -> play_tone(if ballY < 68 then (if ballY > 28 then 880 else 0) else 0, 40)
every 33 -> play_tone(if ballY > 410 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 0 bricks (red) — positioned with inline style only
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:32px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#f87171,#ef4444); opacity:{if r0[i] then 1 else 0}; transition:opacity 0.2s" } []
-- Row 1 bricks (orange)
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:52px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#fb923c,#f97316); opacity:{if r1[i] then 1 else 0}; transition:opacity 0.2s" } []
-- Row 2 bricks (yellow)
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:72px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#fde68a,#facc15); opacity:{if r2[i] then 1 else 0}; transition:opacity 0.2s" } []
-- Row 3 bricks (green)
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:92px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#4ade80,#22c55e); opacity:{if r3[i] then 1 else 0}; transition:opacity 0.2s" } []
-- Row 4 bricks (blue)
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:112px; left:{i * 60 + 2}px; background:linear-gradient(180deg,#60a5fa,#3b82f6); opacity:{if r4[i] then 1 else 0}; transition:opacity 0.2s" } []
-- Paddle (purple glow)
column { style: "position:absolute; width:80px; height:12px; top:380px; 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 - 30 else paddleX,
variant: "secondary"
}
button "➡️ Right" {
click: paddleX = if paddleX < 520 then paddleX + 30 else paddleX,
variant: "secondary"
}
button "🔄 Reset" {
click: score = 0; lives = 3; paused = 0; bvx = 3; bvy = -3; ballX = 300; ballY = 350; 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" }
]