Skip to main content

sparrow/
demo.rs

1//! `sparrow demo` — A self-contained demo that codes a snake game.
2//!
3//! Shows live progress with module labels ("[Planner] Analyzing...",
4//! "[Coder] Writing game.rs...", "[Verifier] Checking...") using colored
5//! terminal output. Takes ~30 seconds and is designed to be shared on
6//! social media to showcase Sparrow's swarm pipeline.
7
8use std::io::{self, Write};
9use std::time::Duration;
10
11/// Run the self-contained demo.
12///
13/// Simulates Sparrow's Planner → Coder → Verifier pipeline by generating
14/// a simple snake game in Rust, compiling it, and optionally running it.
15pub async fn run_demo(_skills: Option<&dyn crate::capabilities::SkillLibrary>) -> anyhow::Result<()> {
16    use crossterm::style::{Color, SetForegroundColor, ResetColor};
17    use crossterm::ExecutableCommand;
18
19    let mut stdout = io::stdout();
20
21    // ── Header ────────────────────────────────────────────────────────
22    println!();
23    stdout.execute(SetForegroundColor(Color::Cyan))?;
24    println!("══════════════════════════════════════════════════");
25    println!("  🐦 SPARROW DEMO — Let's code a Snake game !");
26    println!("══════════════════════════════════════════════════");
27    stdout.execute(ResetColor)?;
28    println!();
29
30    // ── Phase 1: Planner ──────────────────────────────────────────────
31    phase_header(&mut stdout, "Planner", "Analyse de la demande...", Color::Yellow)?;
32    tokio::time::sleep(Duration::from_millis(500)).await;
33
34    let plan = vec![
35        "1. Créer un fichier game.rs avec la boucle de jeu",
36        "2. Implémenter le serpent (position, direction, croissance)",
37        "3. Génération de la pomme aléatoire",
38        "4. Gestion des entrées clavier (flèches directionnelles)",
39        "5. Détection de collision (murs + auto-collision)",
40        "6. Affichage terminal avec crossterm",
41    ];
42
43    for step in &plan {
44        print!("  ");
45        stdout.execute(SetForegroundColor(Color::DarkYellow))?;
46        print!("→ ");
47        stdout.execute(ResetColor)?;
48        println!("{step}");
49        io::stdout().flush().ok();
50        tokio::time::sleep(Duration::from_millis(400)).await;
51    }
52    println!();
53
54    // ── Phase 2: Coder ────────────────────────────────────────────────
55    phase_header(&mut stdout, "Coder", "Écriture de game.rs...", Color::Green)?;
56    tokio::time::sleep(Duration::from_millis(300)).await;
57
58    let game_code = generate_snake_game_code();
59
60    // Show code being "written" with typing effect (truncated for speed)
61    let preview_lines: Vec<&str> = game_code.lines().take(8).collect();
62    for line in &preview_lines {
63        stdout.execute(SetForegroundColor(Color::DarkGreen))?;
64        println!("  │ {line}");
65        io::stdout().flush().ok();
66        tokio::time::sleep(Duration::from_millis(150)).await;
67    }
68    stdout.execute(SetForegroundColor(Color::DarkGreen))?;
69    println!("  │ ... ({} lignes au total)", game_code.lines().count());
70    stdout.execute(ResetColor)?;
71
72    // Write the actual file
73    let demo_dir = std::env::temp_dir().join("sparrow_demo");
74    std::fs::create_dir_all(&demo_dir)?;
75    let game_path = demo_dir.join("game.rs");
76    std::fs::write(&game_path, &game_code)?;
77
78    println!();
79    stdout.execute(SetForegroundColor(Color::Green))?;
80    println!("  ✓ Fichier écrit → {}", game_path.display());
81    stdout.execute(ResetColor)?;
82    println!();
83
84    // ── Phase 3: Verifier ─────────────────────────────────────────────
85    phase_header(&mut stdout, "Verifier", "Vérification du code...", Color::Magenta)?;
86    tokio::time::sleep(Duration::from_millis(300)).await;
87
88    // Try to compile the game
89    let compile_result = compile_snake_game(&demo_dir, &game_path);
90    match &compile_result {
91        Ok(()) => {
92            stdout.execute(SetForegroundColor(Color::Green))?;
93            println!("  ✓ Compilation réussie !");
94            stdout.execute(ResetColor)?;
95        }
96        Err(err) => {
97            stdout.execute(SetForegroundColor(Color::Red))?;
98            println!("  ✗ Compilation échouée : {err}");
99            stdout.execute(ResetColor)?;
100            println!("  → Le code source reste disponible dans {}", demo_dir.display());
101        }
102    }
103
104    println!();
105
106    // ── Summary ───────────────────────────────────────────────────────
107    stdout.execute(SetForegroundColor(Color::Cyan))?;
108    println!("══════════════════════════════════════════════════");
109    println!("  🎉 Démo terminée !");
110    println!("══════════════════════════════════════════════════");
111    stdout.execute(ResetColor)?;
112    println!();
113    println!("  Planner  : {} étapes planifiées", plan.len());
114    println!("  Coder    : {} lignes de code générées", game_code.lines().count());
115    println!(
116        "  Verifier : {}",
117        if compile_result.is_ok() {
118            "compilation OK ✓"
119        } else {
120            "compilation échouée ✗"
121        }
122    );
123    println!();
124    println!("  Code source : {}", game_path.display());
125    println!();
126
127    // ── Offer to run ──────────────────────────────────────────────────
128    if compile_result.is_ok() {
129        println!("🐍 Le jeu est prêt ! Veux-tu y jouer maintenant ?");
130        print!("Lancer le snake game ? [O/n] ");
131        io::stdout().flush().ok();
132
133        let mut answer = String::new();
134        io::stdin().read_line(&mut answer)?;
135
136        if !matches!(answer.trim().to_lowercase().as_str(), "n" | "no" | "non") {
137            run_compiled_game(&demo_dir)?;
138        }
139    }
140
141    println!("\n✨ Merci d'avoir testé Sparrow !");
142    println!("   → Partage : sparrow share");
143    println!("   → Docs    : https://github.com/ucav/Sparrow\n");
144
145    Ok(())
146}
147
148// ─── Phase header helper ─────────────────────────────────────────────────────
149
150fn phase_header(
151    stdout: &mut io::Stdout,
152    label: &str,
153    subtitle: &str,
154    color: crossterm::style::Color,
155) -> io::Result<()> {
156    use crossterm::style::{Attribute, SetAttribute, SetForegroundColor, ResetColor};
157    use crossterm::ExecutableCommand;
158
159    stdout.execute(SetForegroundColor(color))?;
160    stdout.execute(SetAttribute(Attribute::Bold))?;
161    print!("[{label}]");
162    stdout.execute(ResetColor)?;
163    stdout.execute(SetAttribute(Attribute::Reset))?;
164    println!(" {subtitle}");
165    Ok(())
166}
167
168// ─── Snake game code generator ────────────────────────────────────────────────
169
170/// Generate a complete, compilable snake game in Rust using crossterm.
171fn generate_snake_game_code() -> String {
172    r#"//! Snake Game — Généré par Sparrow Demo
173//!
174//! Un snake game minimaliste dans le terminal.
175//! Contrôles : ← ↑ ↓ → (flèches directionnelles), q pour quitter.
176
177use std::collections::VecDeque;
178use std::io::{stdout, Write};
179use std::time::{Duration, Instant};
180
181use crossterm::cursor::{Hide, Show};
182use crossterm::event::{self, Event, KeyCode};
183use crossterm::style::{Color, Print, SetForegroundColor, ResetColor};
184use crossterm::terminal::{self, Clear, ClearType};
185use crossterm::{ExecutableCommand, QueueableCommand};
186use rand::Rng;
187
188const WIDTH: u16 = 40;
189const HEIGHT: u16 = 20;
190const TICK_MS: u64 = 100;
191
192#[derive(Clone, Copy, PartialEq, Eq)]
193enum Direction {
194    Up,
195    Down,
196    Left,
197    Right,
198}
199
200impl Direction {
201    fn opposite(self) -> Self {
202        match self {
203            Direction::Up => Direction::Down,
204            Direction::Down => Direction::Up,
205            Direction::Left => Direction::Right,
206            Direction::Right => Direction::Left,
207        }
208    }
209}
210
211struct Game {
212    snake: VecDeque<(u16, u16)>,
213    direction: Direction,
214    food: (u16, u16),
215    score: u32,
216    game_over: bool,
217}
218
219impl Game {
220    fn new() -> Self {
221        let mut rng = rand::thread_rng();
222        let start_x = WIDTH / 2;
223        let start_y = HEIGHT / 2;
224        let mut snake = VecDeque::new();
225        snake.push_back((start_x, start_y));
226        snake.push_back((start_x - 1, start_y));
227        snake.push_back((start_x - 2, start_y));
228
229        let mut game = Self {
230            snake,
231            direction: Direction::Right,
232            food: (rng.gen_range(1..WIDTH - 1), rng.gen_range(1..HEIGHT - 1)),
233            score: 0,
234            game_over: false,
235        };
236        game.spawn_food();
237        game
238    }
239
240    fn spawn_food(&mut self) {
241        let mut rng = rand::thread_rng();
242        loop {
243            let candidate = (rng.gen_range(1..WIDTH - 1), rng.gen_range(1..HEIGHT - 1));
244            if !self.snake.contains(&candidate) {
245                self.food = candidate;
246                break;
247            }
248        }
249    }
250
251    fn tick(&mut self) {
252        if self.game_over {
253            return;
254        }
255
256        let head = *self.snake.front().unwrap();
257        let new_head = match self.direction {
258            Direction::Up => (head.0, head.1.wrapping_sub(1)),
259            Direction::Down => (head.0, head.1 + 1),
260            Direction::Left => (head.0.wrapping_sub(1), head.1),
261            Direction::Right => (head.0 + 1, head.1),
262        };
263
264        // Wall collision
265        if new_head.0 == 0 || new_head.0 >= WIDTH - 1 || new_head.1 == 0 || new_head.1 >= HEIGHT - 1
266        {
267            self.game_over = true;
268            return;
269        }
270
271        // Self collision
272        if self.snake.contains(&new_head) {
273            self.game_over = true;
274            return;
275        }
276
277        self.snake.push_front(new_head);
278
279        if new_head == self.food {
280            self.score += 1;
281            self.spawn_food();
282        } else {
283            self.snake.pop_back();
284        }
285    }
286
287    fn set_direction(&mut self, dir: Direction) {
288        if dir != self.direction.opposite() {
289            self.direction = dir;
290        }
291    }
292
293    fn draw(&self, stdout: &mut std::io::Stdout) {
294        stdout.queue(Clear(ClearType::All)).unwrap();
295
296        // Draw top wall
297        stdout.queue(crossterm::cursor::MoveTo(0, 0)).unwrap();
298        stdout
299            .queue(SetForegroundColor(Color::DarkBlue))
300            .unwrap();
301        for _ in 0..WIDTH {
302            stdout.queue(Print("█")).unwrap();
303        }
304
305        // Draw body
306        for y in 1..HEIGHT {
307            stdout.queue(crossterm::cursor::MoveTo(0, y)).unwrap();
308            stdout
309                .queue(SetForegroundColor(Color::DarkBlue))
310                .unwrap();
311            stdout.queue(Print("█")).unwrap(); // left wall
312
313            for x in 1..WIDTH - 1 {
314                if self.snake.contains(&(x, y)) {
315                    let is_head = self.snake.front() == Some(&(x, y));
316                    stdout
317                        .queue(SetForegroundColor(if is_head {
318                            Color::Green
319                        } else {
320                            Color::DarkGreen
321                        }))
322                        .unwrap();
323                    stdout.queue(Print(if is_head { "●" } else { "○" })).unwrap();
324                } else if (x, y) == self.food {
325                    stdout.queue(SetForegroundColor(Color::Red)).unwrap();
326                    stdout.queue(Print("🍎")).unwrap();
327                } else {
328                    stdout.queue(Print(" ")).unwrap();
329                }
330            }
331
332            stdout
333                .queue(SetForegroundColor(Color::DarkBlue))
334                .unwrap();
335            stdout.queue(Print("█")).unwrap(); // right wall
336        }
337
338        // Draw bottom wall
339        stdout.queue(crossterm::cursor::MoveTo(0, HEIGHT)).unwrap();
340        stdout
341            .queue(SetForegroundColor(Color::DarkBlue))
342            .unwrap();
343        for _ in 0..WIDTH {
344            stdout.queue(Print("█")).unwrap();
345        }
346
347        // Score
348        stdout
349            .queue(crossterm::cursor::MoveTo(0, HEIGHT + 1))
350            .unwrap();
351        stdout.queue(ResetColor).unwrap();
352        stdout
353            .queue(Print(format!(
354                "Score: {}  |  Flèches: diriger  |  q: quitter",
355                self.score
356            )))
357            .unwrap();
358
359        stdout.flush().unwrap();
360    }
361}
362
363fn main() -> anyhow::Result<()> {
364    let mut stdout = stdout();
365    terminal::enable_raw_mode()?;
366    stdout.execute(Hide)?;
367    stdout.execute(Clear(ClearType::All))?;
368
369    let mut game = Game::new();
370    let mut last_tick = Instant::now();
371
372    loop {
373        // Handle input
374        while event::poll(Duration::from_millis(0))? {
375            if let Event::Key(key_event) = event::read()? {
376                match key_event.code {
377                    KeyCode::Up => game.set_direction(Direction::Up),
378                    KeyCode::Down => game.set_direction(Direction::Down),
379                    KeyCode::Left => game.set_direction(Direction::Left),
380                    KeyCode::Right => game.set_direction(Direction::Right),
381                    KeyCode::Char('q') | KeyCode::Char('Q') => {
382                        terminal::disable_raw_mode()?;
383                        stdout.execute(Show)?;
384                        println!("\n👋 Score final : {}\n", game.score);
385                        return Ok(());
386                    }
387                    _ => {}
388                }
389            }
390        }
391
392        // Game tick
393        if last_tick.elapsed() >= Duration::from_millis(TICK_MS) {
394            game.tick();
395            last_tick = Instant::now();
396            game.draw(&mut stdout);
397
398            if game.game_over {
399                stdout
400                    .queue(crossterm::cursor::MoveTo(WIDTH / 2 - 5, HEIGHT / 2))
401                    .unwrap();
402                stdout.queue(SetForegroundColor(Color::Red)).unwrap();
403                stdout.queue(Print("GAME OVER!")).unwrap();
404                stdout.queue(ResetColor).unwrap();
405                stdout.flush().unwrap();
406                std::thread::sleep(Duration::from_secs(2));
407
408                terminal::disable_raw_mode()?;
409                stdout.execute(Show)?;
410                println!("\n💀 Game Over ! Score final : {}\n", game.score);
411                return Ok(());
412            }
413        }
414
415        std::thread::sleep(Duration::from_millis(1));
416    }
417}
418"#
419    .to_string()
420}
421
422// ─── Compilation ─────────────────────────────────────────────────────────────
423
424/// Try to compile the generated snake game.
425fn compile_snake_game(demo_dir: &std::path::Path, game_path: &std::path::Path) -> anyhow::Result<()> {
426    // Check if rustc is available
427    let rustc_check = std::process::Command::new("rustc")
428        .arg("--version")
429        .stdout(std::process::Stdio::null())
430        .stderr(std::process::Stdio::null())
431        .status();
432
433    if rustc_check.map_or(true, |s| !s.success()) {
434        anyhow::bail!("rustc n'est pas installé. Installe Rust : https://rustup.rs");
435    }
436
437    // Create a minimal Cargo.toml for the demo (crossterm + rand deps)
438    let cargo_toml = format!(
439        r#"[package]
440name = "sparrow-snake"
441version = "0.1.0"
442edition = "2021"
443
444[dependencies]
445crossterm = "0.28"
446rand = "0.8"
447anyhow = "1"
448"#
449    );
450    std::fs::write(demo_dir.join("Cargo.toml"), cargo_toml)?;
451
452    // Rename game.rs to main.rs in a src/ directory
453    let src_dir = demo_dir.join("src");
454    std::fs::create_dir_all(&src_dir)?;
455    std::fs::copy(game_path, src_dir.join("main.rs"))?;
456
457    // Run cargo build
458    let output = std::process::Command::new("cargo")
459        .args(["build", "--release"])
460        .current_dir(demo_dir)
461        .stdout(std::process::Stdio::piped())
462        .stderr(std::process::Stdio::piped())
463        .output()?;
464
465    if !output.status.success() {
466        let stderr = String::from_utf8_lossy(&output.stderr);
467        anyhow::bail!("Compilation échouée : {stderr}");
468    }
469
470    Ok(())
471}
472
473/// Run the compiled snake game.
474fn run_compiled_game(demo_dir: &std::path::Path) -> anyhow::Result<()> {
475    let binary = demo_dir.join("target/release/sparrow-snake");
476
477    if !binary.exists() {
478        anyhow::bail!("Binaire introuvable : {}", binary.display());
479    }
480
481    println!("\n🐍 Lancement du jeu... (q pour quitter)\n");
482
483    let status = std::process::Command::new(&binary)
484        .current_dir(demo_dir)
485        .status()?;
486
487    if !status.success() {
488        anyhow::bail!("Le jeu a terminé avec une erreur.");
489    }
490
491    Ok(())
492}