From f7f73632308ac6e41f9de92f42f5653770b1aafe Mon Sep 17 00:00:00 2001 From: enzotar Date: Thu, 26 Feb 2026 20:46:53 -0800 Subject: [PATCH] feat: snake game streaming via relay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit game-snake.html — Full canvas Snake game: - 20x20 grid with gradient snake (eyes, body fade) - Keyboard (WASD/arrows) + button controls - Wall wrapping, self-collision detection - Speed increases on food eat (200ms → 80ms min) - Game over screen with restart - Streams every frame via DreamStack relay (0x31 SignalDiff) - Periodic auto-sync (0x30 every 50 frames) - Graceful fallback when relay unavailable game-viewer.ds — DreamStack receiver: - Connects to ws://localhost:9100/stream/snake - Shows live score, length, speed, position - PLAYING/GAME OVER status badge game-snake.ds — DreamStack source (simplified) game-reaction.ds — Reaction game (bonus) --- examples/game-reaction.ds | 81 ++++++++++ examples/game-snake.ds | 82 ++++++++++ examples/game-snake.html | 314 ++++++++++++++++++++++++++++++++++++++ examples/game-viewer.ds | 36 +++++ 4 files changed, 513 insertions(+) create mode 100644 examples/game-reaction.ds create mode 100644 examples/game-snake.ds create mode 100644 examples/game-snake.html create mode 100644 examples/game-viewer.ds diff --git a/examples/game-reaction.ds b/examples/game-reaction.ds new file mode 100644 index 0000000..6f7f51f --- /dev/null +++ b/examples/game-reaction.ds @@ -0,0 +1,81 @@ +-- DreamStack Reaction Game (Streamed) +-- Whack-a-mole style: click the target before it moves! +-- State is streamed so viewers can watch live. +-- +-- Run with: +-- Tab 1: cargo run -p ds-stream (relay) +-- Tab 2: dreamstack stream examples/game-reaction.ds (source/player) +-- Tab 3: open game-viewer build in browser (viewer) + +import { Card } from "../registry/components/card" +import { Badge } from "../registry/components/badge" + +-- Game state +let score = 0 +let misses = 0 +let target = 5 +let round = 0 + +-- Timer: move target every 2 seconds +every 2000 -> round += 1 + +-- Derived: target position follows round +let pos = round % 9 + 1 + +-- Stream game state +stream reaction_game on "ws://localhost:9100/peer/game" { + mode: signal, + output: score, misses, round, pos +} + +view game = column [ + text "🎯 Reaction Game" { variant: "title" } + text "Click the lit cell before it moves!" { variant: "subtitle" } + + -- Score bar + row [ + Badge { label: "Score: {score}", variant: "success" } + Badge { label: "Misses: {misses}", variant: "error" } + Badge { label: "Round: {round}", variant: "info" } + ] + + -- 3x3 Grid + Card { title: "Game Board" } [ + row [ + match pos + 1 -> button "🎯" { click: score += 1, variant: "primary" } + _ -> button "·" { click: misses += 1, variant: "secondary" } + match pos + 2 -> button "🎯" { click: score += 1, variant: "primary" } + _ -> button "·" { click: misses += 1, variant: "secondary" } + match pos + 3 -> button "🎯" { click: score += 1, variant: "primary" } + _ -> button "·" { click: misses += 1, variant: "secondary" } + ] + row [ + match pos + 4 -> button "🎯" { click: score += 1, variant: "primary" } + _ -> button "·" { click: misses += 1, variant: "secondary" } + match pos + 5 -> button "🎯" { click: score += 1, variant: "primary" } + _ -> button "·" { click: misses += 1, variant: "secondary" } + match pos + 6 -> button "🎯" { click: score += 1, variant: "primary" } + _ -> button "·" { click: misses += 1, variant: "secondary" } + ] + row [ + match pos + 7 -> button "🎯" { click: score += 1, variant: "primary" } + _ -> button "·" { click: misses += 1, variant: "secondary" } + match pos + 8 -> button "🎯" { click: score += 1, variant: "primary" } + _ -> button "·" { click: misses += 1, variant: "secondary" } + match pos + 9 -> button "🎯" { click: score += 1, variant: "primary" } + _ -> button "·" { click: misses += 1, variant: "secondary" } + ] + ] + + -- Reset + button "🔄 Reset Game" { click: score = 0; misses = 0, variant: "ghost" } +] diff --git a/examples/game-snake.ds b/examples/game-snake.ds new file mode 100644 index 0000000..620f0bd --- /dev/null +++ b/examples/game-snake.ds @@ -0,0 +1,82 @@ +-- DreamStack Snake Game (Streamed) +-- Move the snake with arrow buttons to eat the food! +-- Each button press moves one step. +-- +-- Run with: +-- Tab 1: cargo run -p ds-stream (relay) +-- Tab 2: dreamstack stream examples/game-snake.ds (source/player) +-- Tab 3: open the viewer HTML (viewer) + +import { Card } from "../registry/components/card" +import { Badge } from "../registry/components/badge" + +-- Snake head position (0-indexed on a conceptual 8x8 grid) +let headX = 4 +let headY = 4 +let score = 0 +let moves = 0 + +-- Food position (changes when eaten — player sets manually for now) +let foodX = 2 +let foodY = 2 + +-- Timer for excitement +let ticks = 0 +every 1000 -> ticks += 1 + +-- Stream the game +stream snake on "ws://localhost:9100/peer/game" { + mode: signal, + output: headX, headY, score, moves, foodX, foodY, ticks +} + +view snake_game = column [ + text "🐍 Snake Game" { variant: "title" } + text "Move with arrows • Eat the 🍎" { variant: "subtitle" } + + -- Score bar + row [ + Badge { label: "Score: {score}", variant: "success" } + Badge { label: "Moves: {moves}", variant: "info" } + Badge { label: "Time: {ticks}s", variant: "warning" } + Badge { label: "🐍 ({headX},{headY})", variant: "default" } + Badge { label: "🍎 ({foodX},{foodY})", variant: "error" } + ] + + -- Game board: 8 rows rendered with match on headY + Card { title: "Board" } [ + -- Row 0 + row [ + when headY == 0 -> when headX == 0 -> text "🟩" + when headY == 0 -> when headX == 1 -> text "🟩" + when headY == 0 -> when headX == 2 -> text "🟩" + when foodY == 0 -> when foodX == 0 -> text "🍎" + text "Row 0: Snake={headY == 0}" + ] + ] + + -- Directional controls + Card { title: "Controls" } [ + row [ + text " " + button "⬆️" { click: headY -= 1; moves += 1, variant: "primary" } + text " " + ] + row [ + button "⬅️" { click: headX -= 1; moves += 1, variant: "primary" } + button "⏹️" { variant: "secondary" } + button "➡️" { click: headX += 1; moves += 1, variant: "primary" } + ] + row [ + text " " + button "⬇️" { click: headY += 1; moves += 1, variant: "primary" } + text " " + ] + ] + + -- Quick food placement + row [ + button "🍎 Move Food" { click: foodX = ticks % 7; foodY = (ticks + 3) % 7, variant: "ghost" } + button "🔄 Reset" { click: headX = 4; headY = 4; score = 0; moves = 0, variant: "destructive" } + ] +] diff --git a/examples/game-snake.html b/examples/game-snake.html new file mode 100644 index 0000000..f8e4aa0 --- /dev/null +++ b/examples/game-snake.html @@ -0,0 +1,314 @@ + + + + +🐍 DreamStack Snake — Streamed + + + + +

🐍 DreamStack Snake

+

Arrow keys or buttons to move · Streamed live via relay

+ +
+ Score: 0 + Speed: 200ms + Length: 3 + ⏳ Connecting… +
+ +
+ +
+ +
+
+
+ + + +
+
+ +
+ +

Connecting to relay…

+ + + + diff --git a/examples/game-viewer.ds b/examples/game-viewer.ds new file mode 100644 index 0000000..50c59e8 --- /dev/null +++ b/examples/game-viewer.ds @@ -0,0 +1,36 @@ +-- DreamStack Snake Viewer (Receiver) +-- Watches the snake game live via streaming relay. +-- +-- Run with: +-- Tab 1: cargo run -p ds-stream (relay on :9100) +-- Tab 2: open examples/game-snake.html (player) +-- Tab 3: open this build in browser (viewer) + +import { Card } from "../registry/components/card" +import { Badge } from "../registry/components/badge" + +-- Connect to the snake game stream +let game = stream from "ws://localhost:9100/stream/snake" + +view viewer = column [ + text "👁️ Snake Spectator" { variant: "title" } + text "Watching the snake game live via relay" { variant: "subtitle" } + + row [ + Badge { label: "LIVE 🔴", variant: "error" } + Badge { label: "Score: {game.score}", variant: "success" } + Badge { label: "Length: {game.length}", variant: "info" } + Badge { label: "Speed: {game.speed}ms", variant: "warning" } + ] + + Card { title: "Game State" } [ + text "🐍 Head: ({game.headX}, {game.headY})" { variant: "title" } + text "🍎 Food: ({game.foodX}, {game.foodY})" + text "Direction: ({game.dirX}, {game.dirY})" + + when game.alive -> + Badge { label: "PLAYING", variant: "success" } + else -> + Badge { label: "GAME OVER", variant: "error" } + ] +]