-- DreamStack Pong -- DOM-based pong game with reactive signals, keyboard controls, and streaming -- Paddles and ball are CSS-positioned elements inside a stack container -- -- Run: -- Tab 1: cargo run -p ds-stream (relay) -- Tab 2: dreamstack dev examples/game-pong.ds (player) import { Badge } from "../registry/components/badge" -- ── State ── let p1y = 160 let p2y = 160 let ballX = 290 let ballY = 190 let bvx = 3 let bvy = 2 let score1 = 0 let score2 = 0 let rally = 0 let paused = 0 let ticks = 0 -- ── Keyboard controls ── on keydown(ev) -> p1y = if ev.key == "ArrowUp" then (if p1y > 0 then p1y - 25 else p1y) else (if ev.key == "ArrowDown" then (if p1y < 320 then p1y + 25 else p1y) else p1y) on keydown(ev) -> paused = if ev.key == " " then (if paused then 0 else 1) else paused -- ── Game loop at ~30fps ── every 33 -> ticks = ticks + 1 -- Ball movement (only when not paused) every 33 -> ballX = if paused then ballX else ballX + bvx every 33 -> ballY = if paused then ballY else ballY + bvy -- Wall bounces (top/bottom) every 33 -> bvy = if ballY < 6 then 2 else bvy every 33 -> bvy = if ballY > 382 then -2 else bvy -- P1 paddle hit (left) — bounce + sound every 33 -> bvx = if ballX < 26 then (if ballY > p1y then (if ballY < p1y + 80 then 4 else bvx) else bvx) else bvx every 33 -> rally = if ballX < 26 then (if ballY > p1y then (if ballY < p1y + 80 then rally + 1 else rally) else rally) else rally -- P2/AI paddle hit (right) — bounce + sound every 33 -> bvx = if ballX > 566 then (if ballY > p2y then (if ballY < p2y + 80 then -4 else bvx) else bvx) else bvx every 33 -> rally = if ballX > 566 then (if ballY > p2y then (if ballY < p2y + 80 then rally + 1 else rally) else rally) else rally -- AI paddle tracking (moves toward ball) every 33 -> p2y = if paused then p2y else (if bvx > 0 then (if ballY > p2y + 44 then p2y + 2 else (if ballY < p2y + 36 then p2y - 2 else p2y)) else p2y) -- Scoring: ball exits left -> AI scores, auto-reset + score sound every 33 -> score2 = if ballX < -8 then score2 + 1 else score2 every 33 -> ballX = if ballX < -8 then 290 else ballX every 33 -> ballY = if ballX < -8 then 190 else ballY every 33 -> bvx = if ballX < -8 then 3 else bvx every 33 -> bvy = if ballX < -8 then 2 else bvy -- Scoring: ball exits right -> P1 scores, auto-reset every 33 -> score1 = if ballX > 604 then score1 + 1 else score1 every 33 -> ballX = if ballX > 604 then 290 else ballX every 33 -> ballY = if ballX > 604 then 190 else ballY every 33 -> bvx = if ballX > 604 then -3 else bvx every 33 -> bvy = if ballX > 604 then -2 else bvy -- Sound effects: paddle hit every 33 -> play_tone(if ballX < 26 then (if ballY > p1y then (if ballY < p1y + 80 then 880 else 0) else 0) else 0, 60) every 33 -> play_tone(if ballX > 566 then (if ballY > p2y then (if ballY < p2y + 80 then 660 else 0) else 0) else 0, 60) -- Stream for viewers stream pong on "ws://localhost:9100/peer/pong" { mode: signal, output: p1y, p2y, ballX, ballY, score1, score2, rally, paused } view pong_game = column [ text "🏓 DreamStack Pong" { variant: "title" } -- Score bar row [ Badge { label: "P1: {score1}", variant: "info" } Badge { label: "Rally: {rally}", variant: "warning" } Badge { label: "AI: {score2}", variant: "error" } ] -- ── The Court ── stack { style: "position:relative; width:600px; height:400px; background:linear-gradient(180deg,#0f172a,#1e293b); border:2px solid #334155; border-radius:16px; margin:0.5rem auto; overflow:hidden; box-shadow:0 0 40px rgba(99,102,241,0.15)" } [ -- Center line column { style: "position:absolute; left:298px; top:0; width:4px; height:400px; background:repeating-linear-gradient(to bottom, #475569 0px, #475569 12px, transparent 12px, transparent 24px)" } [] -- Center circle column { style: "position:absolute; left:260px; top:160px; width:80px; height:80px; border:2px solid #475569; border-radius:50%" } [] -- P1 paddle (blue, left) column { style: "position:absolute; left:8px; width:12px; height:80px; background:linear-gradient(180deg,#818cf8,#6366f1); border-radius:6px; box-shadow:0 0 12px rgba(129,140,248,0.5); transition:top 0.05s", top: p1y } [] -- P2/AI paddle (red, right) column { style: "position:absolute; left:580px; width:12px; height:80px; background:linear-gradient(180deg,#f87171,#ef4444); border-radius:6px; box-shadow:0 0 12px rgba(248,113,113,0.5); transition:top 0.05s", top: p2y } [] -- Ball (yellow glow) column { style: "position:absolute; width:14px; height:14px; background:radial-gradient(circle,#fde68a,#f59e0b); border-radius:50%; box-shadow:0 0 16px #fbbf24,0 0 4px #fbbf24", top: ballY, left: ballX } [] -- Score overlay on court row { style: "position:absolute; top:12px; left:0; width:100%; justify-content:center; gap:140px; pointer-events:none" } [ text "{score1}" { style: "font-size:56px; color:rgba(129,140,248,0.2); font-weight:900; font-family:monospace" } text "{score2}" { style: "font-size:56px; color:rgba(248,113,113,0.2); font-weight:900; font-family:monospace" } ] ] -- Controls row [ button (if paused then "▶ Resume" else "⏸ Pause") { click: paused = if paused then 0 else 1, variant: "primary" } button "⬆️ Up" { click: p1y = if p1y > 0 then p1y - 25 else p1y, variant: "secondary" } button "⬇️ Down" { click: p1y = if p1y < 320 then p1y + 25 else p1y, variant: "secondary" } button "🔄 Reset" { click: score1 = 0; score2 = 0; rally = 0; paused = 0; bvx = 3; bvy = 2; ballX = 290; ballY = 190; p1y = 160; p2y = 160, variant: "destructive" } ] text "⬆⬇ Arrow keys to move • Space to pause • Ball auto-serves" { variant: "muted" } text "🔴 Streaming via relay — spectators see real-time game state" { variant: "muted" } ]