//! Hangman Game with Threading //! //! A classic hangman game where game logic runs in a separate thread //! while the render loop stays responsive. The user can make guesses //! while turtle animations play smoothly. //! //! Run with: `cargo run --package turtle-lib --example hangman_threaded` use std::io::{self, Write}; use std::sync::mpsc; use std::thread; use turtle_lib::*; // Word list for the game const WORDS: &[&str] = &[ "turtle", "graphics", "threading", "rust", "animation", "crossbeam", "channel", "synchronization", "parallel", "concurrent", ]; #[macroquad::main("Hangman")] async fn main() { let mut app = TurtleApp::new(); // Create three turtles: hangman, lines, and smiley let hangman_tx = app.create_turtle_channel(100); let lines_tx = app.create_turtle_channel(100); let smiley_tx = app.create_turtle_channel(100); // Spawn game logic thread let game_thread = thread::spawn({ let hangman = hangman_tx.clone(); let lines = lines_tx.clone(); let smiley = smiley_tx.clone(); move || { run_game_logic(hangman, lines, smiley); } }); // Main render loop loop { // Check for quit if macroquad::prelude::is_key_pressed(macroquad::prelude::KeyCode::Escape) || macroquad::prelude::is_key_pressed(macroquad::prelude::KeyCode::Q) { break; } // Clear and render macroquad::prelude::clear_background(WHITE); app.process_commands(); app.update(); app.render(); macroquad::prelude::next_frame().await; } // Wait for game thread to finish game_thread.join().ok(); println!("Game ended. Goodbye!"); } enum GameMessage { GameOver { won: bool, word: String }, } fn run_game_logic( hangman_tx: TurtleCommandSender, lines_tx: TurtleCommandSender, smiley_tx: TurtleCommandSender, ) { let secret = choose_word(); println!("Starting hangman game..."); println!("Secret word has {} letters", secret.len()); // Setup: Position hangman turtle and draw base (hill) { let mut plan = create_turtle_plan(); setup_hangman(&mut plan); draw_hill(&mut plan); hangman_tx.send(plan.build()).ok(); } let mut all_guesses = String::new(); let mut wrong_guesses = 0; const MAX_WRONG: usize = 8; // 8 body parts after base // Draw current state of lines draw_lines_state(&lines_tx, &secret, &all_guesses); // Main game loop loop { // Check if won if secret.chars().all(|c| all_guesses.contains(c)) { draw_smiley(&smiley_tx, true); break; } // Check if lost if wrong_guesses >= MAX_WRONG { draw_smiley(&smiley_tx, false); break; } // Ask for guess let guess = ask_for_letter(); let guess_lower = guess.to_lowercase(); all_guesses.push_str(&guess_lower); if secret.contains(&guess_lower) { println!("✓ Correct! '{}' is in the word", guess_lower); // Draw current state of lines draw_lines_state(&lines_tx, &secret, &all_guesses); } else { println!("✗ Wrong! '{}' is NOT in the word", guess_lower); wrong_guesses += 1; // Draw next hangman step draw_hangman_step(&hangman_tx, wrong_guesses); println!("Wrong guesses: {}/{}", wrong_guesses, MAX_WRONG); } } } fn choose_word() -> &'static str { WORDS[(std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_secs() as usize) % WORDS.len()] } fn ask_for_letter() -> String { print!("Guess a letter: "); io::stdout().flush().ok(); let mut guess = String::new(); io::stdin().read_line(&mut guess).ok(); guess.trim().to_string() } fn setup_hangman(plan: &mut TurtlePlan) { plan.hide() .set_speed(1001) // Instant mode .set_pen_width(3.0) // Thicker lines for visibility .set_pen_color(BLACK) .pen_up() .go_to(vec2(-100.0, -100.0)) // More centered position .pen_down(); } fn draw_hangman_step(tx: &TurtleCommandSender, step: usize) { let mut plan = create_turtle_plan(); plan.set_speed(1001); // Instant mode match step { 1 => draw_mast(&mut plan), 2 => draw_bar(&mut plan), 3 => draw_support(&mut plan), 4 => draw_rope(&mut plan), 5 => draw_head(&mut plan), 6 => draw_arms(&mut plan), 7 => draw_body(&mut plan), 8 => draw_legs(&mut plan), _ => {} } tx.send(plan.build()).ok(); } // Hangman drawing functions (scaled down for visibility) fn draw_hill(plan: &mut TurtlePlan) { plan.left(135.0) .circle_left(100.0, 90.0, 36) .circle_left(100.0, -45.0, 36) .right(90.0); } fn draw_mast(plan: &mut TurtlePlan) { plan.forward(150.0); } fn draw_bar(plan: &mut TurtlePlan) { plan.right(90.0).forward(75.0); } fn draw_support(plan: &mut TurtlePlan) { plan.backward(50.0) .right(135.0) .forward(35.355) .backward(35.355) .left(135.0) .forward(50.0); } fn draw_rope(plan: &mut TurtlePlan) { plan.set_pen_width(2.0).right(90.0).forward(35.0); } fn draw_head(plan: &mut TurtlePlan) { plan.left(90.0).circle_right(15.0, 540.0, 72); } fn draw_arms(plan: &mut TurtlePlan) { plan.left(60.0) .forward(50.0) .backward(50.0) .left(60.0) .forward(50.0) .backward(50.0) .right(30.0); } fn draw_body(plan: &mut TurtlePlan) { plan.forward(50.0); } fn draw_legs(plan: &mut TurtlePlan) { plan.right(20.0) .forward(60.0) .backward(60.0) .left(40.0) .forward(60.0) .backward(60.0) .right(20.0); } fn draw_lines_state(tx: &TurtleCommandSender, secret: &str, all_guesses: &str) { let mut plan = create_turtle_plan(); plan.reset() //.hide() .set_pen_color(BLACK) .set_pen_width(2.0) .pen_up() .go_to(vec2(-100.0, 100.0)) .pen_down(); // Print word state in console print!("Word: "); for letter in secret.chars() { if all_guesses.contains(letter) { print!("{} ", letter); } else { print!("_ "); } } println!(); // Draw underscores/circles for each letter for letter in secret.chars() { if all_guesses.contains(letter) { // Draw green circle for revealed letter plan.pen_up() .right(90.0) .forward(2.5) .set_pen_color(GREEN) .pen_down() .circle_left(7.5, 360.0, 24) .pen_up() .backward(2.5) .left(90.0) .set_pen_color(BLACK) .pen_down(); } else { // Draw black underscore plan.forward(15.0); } plan.pen_up().forward(15.0).pen_down(); } tx.send(plan.build()).ok(); } fn draw_smiley(tx: &TurtleCommandSender, won: bool) { let mut plan = create_turtle_plan(); plan.hide() .set_speed(1001) // Instant mode .pen_up() .go_to(vec2(100.0, 0.0)) // Right side of screen .pen_down() .set_pen_color(if won { GREEN } else { RED }); // Face plan.circle_left(50.0, 360.0, 72); // Left eye plan.pen_up() .forward(27.5) .right(90.0) .forward(20.0) .pen_down() .circle_left(3.0, 360.0, 24); // Right eye plan.pen_up() .forward(42.5) .pen_down() .circle_left(3.0, 360.0, 24); // Mouth plan.pen_up() .backward(42.5) .left(90.0) .backward(40.0) .right(90.0) .pen_down(); if won { // Smile plan.right(45.0).circle_left(32.5, 90.0, 36); } else { // Frown plan.left(45.0).circle_right(32.5, 90.0, 36); } tx.send(plan.build()).ok(); }