From 7e23dc9d9c9fa9f9f654b89a9573b6e08b76ad9e Mon Sep 17 00:00:00 2001 From: Franz Dietrich Date: Fri, 17 Oct 2025 19:17:22 +0200 Subject: [PATCH] add two threading examples --- turtle-lib/examples/game_logic_demo.rs | 106 +++++++ turtle-lib/examples/hangman_threaded.rs | 370 ++++++++++++++++++++++++ 2 files changed, 476 insertions(+) create mode 100644 turtle-lib/examples/game_logic_demo.rs create mode 100644 turtle-lib/examples/hangman_threaded.rs diff --git a/turtle-lib/examples/game_logic_demo.rs b/turtle-lib/examples/game_logic_demo.rs new file mode 100644 index 0000000..7cf71b0 --- /dev/null +++ b/turtle-lib/examples/game_logic_demo.rs @@ -0,0 +1,106 @@ +//! Example: Game Logic in Separate Thread +//! +//! This example demonstrates how to run game logic in a separate thread +//! while keeping the render loop responsive on the main thread. +//! +//! The main thread handles rendering and animation, while game logic +//! threads can perform blocking operations (like fetching data) and +//! send turtle commands via channels. + +use std::thread; +use std::time::Duration; +use turtle_lib::*; + +#[macroquad::main("Game Logic Threading")] +async fn main() { + let mut app = TurtleApp::new(); + + // Create two turtles and get their command senders + let turtle1_tx = app.create_turtle_channel(100); + let turtle2_tx = app.create_turtle_channel(100); + + // Spawn first game logic thread + let _thread1 = thread::spawn({ + let tx = turtle1_tx.clone(); + move || { + // Simulate some blocking work (e.g., network request, calculation) + println!("Thread 1: Starting work..."); + thread::sleep(Duration::from_millis(500)); + + // Now send turtle commands + let mut plan = create_turtle_plan(); + plan.set_pen_color(BLUE) + .forward(100.0) + .right(90.0) + .forward(100.0) + .right(90.0) + .forward(100.0) + .right(90.0) + .forward(100.0); + + tx.send(plan.build()) + .expect("Failed to send commands for turtle 1"); + println!("Thread 1: Commands sent!"); + + // Send more commands in a loop + for i in 0..10 { + thread::sleep(Duration::from_millis(300)); + let mut step = create_turtle_plan(); + step.right(36.0).forward(50.0); + let _ = tx.try_send(step.build()); + println!("Thread 1: Step {} sent", i + 1); + } + } + }); + + // Spawn second game logic thread + let _thread2 = thread::spawn({ + let tx = turtle2_tx.clone(); + move || { + // Different timing than thread1 + println!("Thread 2: Starting work..."); + thread::sleep(Duration::from_millis(1000)); + + // Draw a circle with turtle2 + let mut plan = create_turtle_plan(); + plan.set_pen_color(RED).circle_left(75.0, 360.0, 72); + + tx.send(plan.build()) + .expect("Failed to send commands for turtle 2"); + println!("Thread 2: Circle sent!"); + } + }); + + // Main render loop + let mut frame_count = 0; + 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 background + macroquad::prelude::clear_background(WHITE); + + // Process incoming commands from game logic threads + // This drains all pending commands from turtle channels + app.process_commands(); + + // Update animation state (tweening, etc.) + app.update(); + + // Render the turtles + app.render(); + + frame_count += 1; + if frame_count % 60 == 0 { + println!("Rendered {} frames", frame_count); + } + + macroquad::prelude::next_frame().await; + } + + println!("Finished!"); +} diff --git a/turtle-lib/examples/hangman_threaded.rs b/turtle-lib/examples/hangman_threaded.rs new file mode 100644 index 0000000..374825d --- /dev/null +++ b/turtle-lib/examples/hangman_threaded.rs @@ -0,0 +1,370 @@ +//! 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); + + // Channel for game logic to communicate with render thread + let (tx, rx) = mpsc::channel(); + + // Spawn game logic thread + let game_thread = thread::spawn({ + let hangman = hangman_tx.clone(); + let lines = lines_tx.clone(); + let smiley = smiley_tx.clone(); + let tx = tx.clone(); + + move || { + run_game_logic(hangman, lines, smiley, tx); + } + }); + + // Main render loop + let mut frame = 0; + loop { + // Check for quit + if macroquad::prelude::is_key_pressed(macroquad::prelude::KeyCode::Escape) + || macroquad::prelude::is_key_pressed(macroquad::prelude::KeyCode::Q) + { + break; + } + + // Process incoming commands from game thread + while let Ok(msg) = rx.try_recv() { + match msg { + GameMessage::GameOver { won, word } => { + if won { + println!("🎉 You Won! The word was: {}", word); + } else { + println!("💀 You Lost! The word was: {}", word); + } + break; + } + } + } + + // Clear and render + macroquad::prelude::clear_background(WHITE); + app.process_commands(); + app.update(); + app.render(); + + frame += 1; + if frame % 60 == 0 { + println!("Rendered {} frames", frame / 60); + } + + 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, + tx: mpsc::Sender, +) { + let secret = choose_word(); + println!("Starting hangman game..."); + println!("Secret word has {} letters", secret.len()); + + // Setup: Position hangman turtle and draw base (hill + mast) + { + let mut plan = create_turtle_plan(); + setup_hangman(&mut plan); + draw_hill(&mut plan); + hangman_tx.send(plan.build()).ok(); + } + + // Give render thread time to process + std::thread::sleep(std::time::Duration::from_millis(100)); + + let mut all_guesses = String::new(); + let mut wrong_guesses = 0; + const MAX_WRONG: usize = 8; // 8 body parts after base + + // Main game loop + loop { + // Draw current state of lines + draw_lines_state(&lines_tx, &secret, &all_guesses); + + // Check if won + if secret.chars().all(|c| all_guesses.contains(c)) { + draw_smiley(&smiley_tx, true); + tx.send(GameMessage::GameOver { + won: true, + word: secret.to_string(), + }) + .ok(); + break; + } + + // Check if lost + if wrong_guesses >= MAX_WRONG { + draw_smiley(&smiley_tx, false); + tx.send(GameMessage::GameOver { + won: false, + word: secret.to_string(), + }) + .ok(); + break; + } + + // Ask for guess + let guess = ask_for_letter(); + let guess_lower = guess.to_lowercase(); + + // Check if already guessed + if all_guesses.contains(&guess_lower) { + println!("You already guessed '{}'", guess_lower); + continue; + } + + all_guesses.push_str(&guess_lower); + + if secret.contains(&guess_lower) { + println!("✓ Correct! '{}' is in the word", guess_lower); + } 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.circle_left(50.0, 180.0, 36) + .left(180.0) + .circle_right(50.0, 90.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.hide() + .set_speed(1001) // Instant mode + .set_pen_color(BLACK) + .set_pen_width(2.0) + .pen_up() + .go_to(vec2(-100.0, 100.0)) // Top of screen + .pen_down() + .right(90.0); + + // Print word state in console + print!("Word: "); + for letter in secret.chars() { + if all_guesses.contains(letter) { + print!("{} ", letter); + plan.forward(20.0); + } else { + print!("_ "); + plan.forward(20.0); + } + } + 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() + .forward(2.5) + .right(90.0) + .set_pen_color(GREEN) + .pen_down() + .circle_left(7.5, 360.0, 24) + .set_pen_color(BLACK) + .left(90.0) + .backward(2.5) + .pen_up(); + } else { + // Draw black underscore + plan.forward(5.0); + } + plan.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(); +}