1use std::io::{self, Write};
9use std::time::Duration;
10
11pub 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 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_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_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 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 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_header(&mut stdout, "Verifier", "Vérification du code...", Color::Magenta)?;
86 tokio::time::sleep(Duration::from_millis(300)).await;
87
88 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 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 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
148fn 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
168fn 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
422fn compile_snake_game(demo_dir: &std::path::Path, game_path: &std::path::Path) -> anyhow::Result<()> {
426 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 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 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 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
473fn 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}