Compare commits
2 Commits
7e23dc9d9c
...
14b93f657b
| Author | SHA1 | Date | |
|---|---|---|---|
| 14b93f657b | |||
| 16430f3958 |
256
.github/copilot-instructions.md
vendored
256
.github/copilot-instructions.md
vendored
@ -2,59 +2,73 @@
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
Rust workspace with turtle graphics implementations. **Primary focus: `turtle-lib`** - lightweight library using Macroquad + Lyon for GPU-accelerated rendering.
|
Rust workspace with turtle graphics implementations. **Primary focus: `turtle-lib`** - lightweight library using Macroquad + Lyon for GPU-accelerated rendering with multi-turtle support and optional threading.
|
||||||
|
|
||||||
### Workspace Structure
|
### Workspace Structure
|
||||||
```
|
```
|
||||||
turtlers/
|
turtlers/
|
||||||
├── turtle-lib/ # MAIN LIBRARY - Macroquad + Lyon (focus here)
|
├── turtle-lib/ # MAIN LIBRARY - Macroquad + Lyon (focus here)
|
||||||
└── turtle-lib-macros/ # Proc macro for turtle_main
|
├── turtle-lib-macros/ # Proc macro for turtle_main
|
||||||
|
└── examples/ # 15+ examples including threading patterns
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture (`turtle-lib`)
|
## Architecture (`turtle-lib`)
|
||||||
|
|
||||||
### Core Design Pattern: Command Queue + Tweening
|
### Core Design Pattern: Persistent Controllers + Command Queues
|
||||||
- **Builder API** (`TurtlePlan`) accumulates commands
|
- **Builder API** (`TurtlePlan`) accumulates commands into immutable `CommandQueue`
|
||||||
- **Command Queue** stores execution plan
|
- **TurtleApp** maintains persistent `Vec<TweenController>` (one per turtle with embedded turtle_id)
|
||||||
- **Tween Controller** interpolates between states for animation
|
- **TweenController** manages command execution and animation state
|
||||||
- **Lyon Tessellation** converts all primitives to GPU meshes
|
- **Lyon Tessellation** converts all primitives to GPU meshes
|
||||||
|
- **Multi-Turtle** support: Create multiple turtles with `add_turtle()` or threading channels
|
||||||
|
|
||||||
|
### Key Architectural Decision: Turtle ID Storage
|
||||||
|
**Critical**: After recent refactoring, `turtle_id` is now **stored in TweenController** (not derived from Vec index). This makes rendering robust when turtles/controllers are sparse or deleted.
|
||||||
|
|
||||||
### Key Files
|
### Key Files
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── lib.rs - Public API, TurtleApp (main loop), re-exports
|
├── lib.rs - TurtleApp, multi-turtle API, channel integration
|
||||||
├── builders.rs - Fluent API traits (forward/right/etc chain)
|
├── builders.rs - Fluent API traits (forward/right/circle/reset/etc)
|
||||||
├── commands.rs - TurtleCommand enum (Move/Turn/Circle/etc)
|
├── commands.rs - TurtleCommand enum (Move/Turn/Circle/Reset/etc)
|
||||||
├── execution.rs - Execute commands, update state
|
├── execution.rs - Command execution (immediate) + state updates
|
||||||
├── tweening.rs - Animation interpolation, speed control
|
├── tweening.rs - Animation + tween interpolation (CommandTween embeds turtle_id)
|
||||||
├── drawing.rs - Render Lyon meshes with Macroquad
|
├── drawing.rs - Lyon mesh rendering with Macroquad
|
||||||
|
├── state.rs - Turtle, TurtleParams, TurtleWorld (persistent state)
|
||||||
├── tessellation.rs - Lyon integration (polygons/strokes/fills/arcs)
|
├── tessellation.rs - Lyon integration (polygons/strokes/fills/arcs)
|
||||||
├── state.rs - TurtleState, TurtleWorld, FillState
|
├── circle_geometry.rs - Arc/circle math helpers
|
||||||
└── circle_geometry.rs - Arc/circle math
|
└── commands_channel.rs - Async channels for threading patterns
|
||||||
```
|
```
|
||||||
|
|
||||||
### Critical Concepts
|
### Critical Concepts
|
||||||
|
|
||||||
**1. Consolidated Commands** (reduces duplication):
|
**1. Consolidated Move Commands**:
|
||||||
- `Move(distance)` - negative = backward
|
- `Move(distance)` - negative = backward (no separate Backward)
|
||||||
- `Turn(angle)` - positive = right, negative = left (degrees)
|
- `Turn(angle)` - positive = right, negative = left (degrees)
|
||||||
- `Circle{radius, angle, steps, direction}` - unified left/right
|
- `Circle{radius, angle, steps, direction}` - unified left/right via CircleDirection
|
||||||
|
- `Reset` - clears drawings, animations, fill state; resets params to defaults
|
||||||
|
|
||||||
**2. Fill System** (multi-contour with holes):
|
**2. Fill System (Multi-Contour with Holes)**:
|
||||||
- `FillState` tracks `Vec<Vec<Vec2>>` (multiple contours)
|
- `FillState` tracks `Vec<Vec<Coordinate>>` (multiple closed contours)
|
||||||
- `pen_up()` closes current contour, `pen_down()` opens new
|
- `pen_up()` closes current contour, `pen_down()` opens new one
|
||||||
- Lyon's EvenOdd fill rule auto-detects holes
|
- Lyon's EvenOdd fill rule auto-detects holes (inner contours with opposite winding)
|
||||||
- Example: Donut = outer circle + inner circle (2 contours)
|
- Example: Donut = outer circle (pen_down) → pen_up → inner circle → end_fill
|
||||||
|
|
||||||
**3. Speed Modes**:
|
**3. Animation Modes**:
|
||||||
- `< 999`: Animated with tweening
|
- Speed `>= 999`: Instant mode (no tweening, executes immediately)
|
||||||
- `>= 999`: Instant execution
|
- Speed `< 999`: Animated mode (tweens with CubicInOut easing, ~duration based on distance/speed)
|
||||||
- Controlled via `SetSpeed` commands (dynamic switching)
|
- Dynamic switching via `SetSpeed` command mid-animation
|
||||||
|
|
||||||
**4. Lyon Tessellation Pipeline**:
|
**4. Multi-Turtle Architecture**:
|
||||||
All drawing → Lyon → GPU mesh → Macroquad rendering
|
- Each turtle owns a persistent `TweenController` with embedded `turtle_id`
|
||||||
- ~410 lines eliminated vs manual triangulation
|
- Rendering finds active tween by checking `controller.current_tween().turtle_id` (not Vec index)
|
||||||
- Functions: `tessellate_polygon/stroke/circle/arc/multi_contour`
|
- Supports concurrent animation of multiple turtles
|
||||||
|
- Example: Hangman uses `turtle_command_channel()` for blocking stdin on separate thread
|
||||||
|
|
||||||
|
**5. Threading Pattern** (for interactive apps like Hangman):
|
||||||
|
- `create_turtle_channel(buffer_size)` returns `TurtleCommandSender` (clonable, Send)
|
||||||
|
- Spawn game logic on thread, send `CommandQueue` batches via channel
|
||||||
|
- Main render loop calls `app.process_commands()` before `update()` to drain channels
|
||||||
|
- Example: `hangman_threaded.rs` spawns stdin reader, sends drawing commands when player guesses
|
||||||
|
|
||||||
## Developer Workflows
|
## Developer Workflows
|
||||||
|
|
||||||
@ -66,131 +80,149 @@ cargo test --package turtle-lib
|
|||||||
cargo clippy --package turtle-lib -- -Wclippy::pedantic \
|
cargo clippy --package turtle-lib -- -Wclippy::pedantic \
|
||||||
-Aclippy::cast_precision_loss -Aclippy::cast_sign_loss -Aclippy::cast_possible_truncation
|
-Aclippy::cast_precision_loss -Aclippy::cast_sign_loss -Aclippy::cast_possible_truncation
|
||||||
|
|
||||||
# Run examples (15+ examples available)
|
# Run examples
|
||||||
cargo run --package turtle-lib --example hello_turtle
|
cargo run --package turtle-lib --example hello_turtle
|
||||||
cargo run --package turtle-lib --example yinyang
|
cargo run --package turtle-lib --example yinyang
|
||||||
cargo run --package turtle-lib --example cheese_macro
|
cargo run --package turtle-lib --example hangman_threaded
|
||||||
```
|
|
||||||
|
|
||||||
### Macro Crate
|
|
||||||
```bash
|
|
||||||
cargo build --package turtle-lib-macros
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Code Quality Standards
|
### Code Quality Standards
|
||||||
- Clippy pedantic mode enabled
|
- Clippy pedantic enabled (graphics math casts allowed)
|
||||||
- Cast warnings allowed for graphics math
|
- Examples must build warning-free
|
||||||
- All examples must build warning-free
|
|
||||||
- Use `#[must_use]` on builder methods
|
- Use `#[must_use]` on builder methods
|
||||||
|
- Builder methods return `&mut Self` (never owned Self) for chaining
|
||||||
|
|
||||||
## Project-Specific Patterns
|
## Project-Specific Patterns
|
||||||
|
|
||||||
### 1. The `turtle_main` Macro (PREFERRED for examples)
|
### 1. Single Turtle (Default)
|
||||||
Simplest way to create turtle programs:
|
|
||||||
```rust
|
```rust
|
||||||
use turtle_lib::*;
|
#[turtle_main("Simple")]
|
||||||
|
|
||||||
#[turtle_main("Window Title")]
|
|
||||||
fn draw(turtle: &mut TurtlePlan) {
|
fn draw(turtle: &mut TurtlePlan) {
|
||||||
turtle.forward(100.0).right(90.0);
|
turtle.forward(100.0).right(90.0);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Generates: window setup + render loop + quit handling (ESC/Q)
|
### 2. Multi-Turtle Direct Setup
|
||||||
|
|
||||||
### 2. Import Convention
|
|
||||||
Only need: `use turtle_lib::*;`
|
|
||||||
- Re-exports: `vec2`, `RED/BLUE/GREEN/etc`, all turtle types
|
|
||||||
- No `use macroquad::prelude::*` needed (causes unused warnings)
|
|
||||||
|
|
||||||
### 3. Builder Chain Pattern
|
|
||||||
```rust
|
```rust
|
||||||
let mut t = create_turtle();
|
let mut app = TurtleApp::new();
|
||||||
t.forward(100).right(90)
|
let t0_id = app.add_turtle(); // Default setup
|
||||||
.set_pen_color(BLUE)
|
let t1_id = app.add_turtle();
|
||||||
.circle_left(50.0, 360.0, 36)
|
|
||||||
.begin_fill()
|
app.append_commands(t0_id, turtle1_plan.build());
|
||||||
.end_fill();
|
app.append_commands(t1_id, turtle2_plan.build());
|
||||||
let app = TurtleApp::new().with_commands(t.build());
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Multi-Contour Fill Example
|
### 3. Threading Pattern (Hangman Example)
|
||||||
```rust
|
```rust
|
||||||
turtle.begin_fill();
|
let mut app = TurtleApp::new();
|
||||||
turtle.circle_left(100.0, 360.0, 72); // Outer circle
|
let turtle_tx = app.create_turtle_channel(100);
|
||||||
turtle.pen_up(); // Closes contour
|
|
||||||
turtle.goto(vec2(0.0, -30.0));
|
|
||||||
turtle.pen_down(); // Opens new contour
|
|
||||||
turtle.circle_left(30.0, 360.0, 36); // Inner (becomes hole)
|
|
||||||
turtle.end_fill(); // EvenOdd rule creates donut
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Manual Setup (advanced control)
|
|
||||||
```rust
|
|
||||||
#[macroquad::main("Custom")]
|
|
||||||
async fn main() {
|
|
||||||
let mut turtle = create_turtle();
|
|
||||||
// ... drawing code ...
|
|
||||||
let mut app = TurtleApp::new().with_commands(turtle.build());
|
|
||||||
|
|
||||||
|
// Spawn game thread
|
||||||
|
let tx = turtle_tx.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
loop {
|
loop {
|
||||||
|
let letter = get_input(); // Blocks
|
||||||
|
let mut plan = create_turtle();
|
||||||
|
plan.forward(50.0);
|
||||||
|
tx.send(plan.build()).ok();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Main loop
|
||||||
|
loop {
|
||||||
clear_background(WHITE);
|
clear_background(WHITE);
|
||||||
|
app.process_commands(); // ← Drains channel
|
||||||
app.update();
|
app.update();
|
||||||
app.render();
|
app.render();
|
||||||
next_frame().await;
|
next_frame().await;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 4. Multi-Contour Fills (Donut)
|
||||||
|
```rust
|
||||||
|
turtle.set_fill_color(BLUE)
|
||||||
|
.begin_fill()
|
||||||
|
.circle_left(100.0, 360.0, 72); // Outer
|
||||||
|
|
||||||
|
turtle.pen_up()
|
||||||
|
.go_to(vec2(0.0, -30.0))
|
||||||
|
.pen_down()
|
||||||
|
.circle_left(30.0, 360.0, 36); // Inner (hole)
|
||||||
|
|
||||||
|
turtle.end_fill(); // EvenOdd creates hole
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Reset Turtle
|
||||||
|
```rust
|
||||||
|
turtle.forward(100.0)
|
||||||
|
.reset() // Clears drawings, resets to defaults
|
||||||
|
.forward(50.0); // Fresh start
|
||||||
|
```
|
||||||
|
|
||||||
## Common Tasks
|
## Common Tasks
|
||||||
|
|
||||||
### Adding New Turtle Command
|
### Adding New Turtle Command
|
||||||
1. Add variant to `TurtleCommand` enum in `commands.rs`
|
1. Add variant to `TurtleCommand` enum in `commands.rs`
|
||||||
2. Implement builder method in `builders.rs` (chain with `self`)
|
2. Implement builder method in `builders.rs` (always return `&mut Self`)
|
||||||
3. Add execution logic in `execution.rs`
|
3. Add execution in `execution.rs` (for immediate state changes) or `tweening.rs` (for animated state)
|
||||||
4. Update tessellation/rendering if needed
|
4. If animated, implement in `calculate_target_state()` in `tweening.rs`
|
||||||
|
5. Update drawing/tessellation if needed
|
||||||
|
|
||||||
### Adding Example
|
### Adding an Example
|
||||||
- Prefer `turtle_main` macro for simplicity
|
- Use `turtle_main` macro for simplicity
|
||||||
- Use only `use turtle_lib::*;`
|
- Import only `use turtle_lib::*;` (all exports included)
|
||||||
- Keep examples focused (one concept each)
|
- For threading: use `create_turtle_channel()` + `process_commands()`
|
||||||
- See `examples/hello_turtle.rs` for minimal template
|
- Place in `turtle-lib/examples/` and update README examples
|
||||||
|
|
||||||
### Debugging Lyon Issues
|
### Debugging Animation Issues
|
||||||
- Enable tracing: `RUST_LOG=turtle_lib=debug cargo run`
|
```bash
|
||||||
- Check `tessellation.rs` for Lyon API usage
|
RUST_LOG=turtle_lib=debug cargo run --example yinyang
|
||||||
- EvenOdd fill rule: holes must have opposite winding
|
```
|
||||||
|
- Check `tweening.rs` for state transitions
|
||||||
|
- Verify `command_creates_drawing()` includes your command type
|
||||||
|
- Circle direction: Left = counter-clockwise, Right = clockwise
|
||||||
|
|
||||||
|
## Critical Implementation Details
|
||||||
|
|
||||||
|
### TurtleCommand::Reset Behavior
|
||||||
|
- Clears `Turtle::commands` (all drawings)
|
||||||
|
- Clears `Turtle::filling` (ongoing fill operations)
|
||||||
|
- Resets `TurtleParams` to defaults (position 0,0, heading 0, pen down, etc.)
|
||||||
|
- Preserves `turtle_id` after reset
|
||||||
|
- Called via `execute_command()` in both instant and animated modes
|
||||||
|
|
||||||
|
### Turtle ID Robustness
|
||||||
|
- **Before**: `turtle_id` derived from Vec index (fragile if controllers deleted)
|
||||||
|
- **After**: `turtle_id` embedded in `TweenController` and `CommandTween`
|
||||||
|
- Rendering finds active tween via `find_map(|c| c.current_tween())` → uses `tween.turtle_id` directly
|
||||||
|
- Safe for sparse/dynamic turtle creation
|
||||||
|
|
||||||
|
### Lyon Tessellation
|
||||||
|
- All drawing → `tessellate_arc/stroke/circle/multi_contour` → `MeshData` → Macroquad `Mesh`
|
||||||
|
- Circle direction affects angle stepping: Left subtracts, Right adds
|
||||||
|
- Start angle for circles: `geom.start_angle_from_center.to_degrees()` (NOT rotation)
|
||||||
|
- EvenOdd fill rule: holes detected by contour winding (no explicit flag needed)
|
||||||
|
|
||||||
## Dependencies & Integration
|
## Dependencies & Integration
|
||||||
|
|
||||||
### Main Dependencies
|
### Main Dependencies
|
||||||
- `macroquad = "0.4"` - Window/rendering framework
|
- `macroquad = "0.4"` - Window/rendering framework
|
||||||
- `lyon = "1.0"` - Tessellation (fills, strokes, circles)
|
- `lyon = "1.0"` - Tessellation (fills, strokes, circles)
|
||||||
- `tween = "2.1.0"` - Animation easing
|
- `tween = "2.1.0"` - Animation easing (CubicInOut)
|
||||||
- `tracing = "0.1"` - Optional logging (zero overhead when unused)
|
- `tracing = "0.1"` - Optional logging (zero cost when unused)
|
||||||
|
- `crossbeam-channel` - Threading pattern support (if used)
|
||||||
### Proc Macro Crate
|
|
||||||
- Separate crate required by Rust (proc-macro = true)
|
|
||||||
- Uses `syn`, `quote`, `proc-macro2`
|
|
||||||
- Generates full macroquad app boilerplate
|
|
||||||
|
|
||||||
## What NOT to Do
|
## What NOT to Do
|
||||||
|
|
||||||
- Don't add `use macroquad::prelude::*` in examples when not required
|
- Don't derive `turtle_id` from Vec index for rendering (use embedded id)
|
||||||
- Don't manually triangulate - use Lyon functions
|
- Don't add `use macroquad::prelude::*` without explicit need (causes unused imports)
|
||||||
- Don't add commands for Forward/Backward separately (use Move)
|
- Don't manually triangulate—always use Lyon `tessellate_*` functions
|
||||||
- Don't create summary/comparison docs unless requested
|
- Don't separate Forward/Backward—use negative `Move` values
|
||||||
|
- Don't call `reset()` expecting to preserve drawing state—it clears everything
|
||||||
## Key Documentation Files
|
|
||||||
|
|
||||||
- `README.md` - Main API docs
|
|
||||||
- `turtle-lib/README.md` - Library-specific docs
|
|
||||||
- `turtle-lib-macros/README.md` - Macro docs
|
|
||||||
|
|
||||||
## Response Style
|
## Response Style
|
||||||
|
|
||||||
- Be concise, no extensive summaries
|
- Be concise, actionable, focused on code
|
||||||
- No emojis in technical responses
|
- Reference specific files/lines when helpful
|
||||||
- Focus on code solutions over explanations
|
- Use examples from `examples/` directory
|
||||||
- Use bullet points for lists
|
- No generic advice; focus on THIS project's patterns
|
||||||
- Reference specific files when helpful
|
|
||||||
|
|||||||
@ -34,23 +34,18 @@ async fn main() {
|
|||||||
let lines_tx = app.create_turtle_channel(100);
|
let lines_tx = app.create_turtle_channel(100);
|
||||||
let smiley_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
|
// Spawn game logic thread
|
||||||
let game_thread = thread::spawn({
|
let game_thread = thread::spawn({
|
||||||
let hangman = hangman_tx.clone();
|
let hangman = hangman_tx.clone();
|
||||||
let lines = lines_tx.clone();
|
let lines = lines_tx.clone();
|
||||||
let smiley = smiley_tx.clone();
|
let smiley = smiley_tx.clone();
|
||||||
let tx = tx.clone();
|
|
||||||
|
|
||||||
move || {
|
move || {
|
||||||
run_game_logic(hangman, lines, smiley, tx);
|
run_game_logic(hangman, lines, smiley);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Main render loop
|
// Main render loop
|
||||||
let mut frame = 0;
|
|
||||||
loop {
|
loop {
|
||||||
// Check for quit
|
// Check for quit
|
||||||
if macroquad::prelude::is_key_pressed(macroquad::prelude::KeyCode::Escape)
|
if macroquad::prelude::is_key_pressed(macroquad::prelude::KeyCode::Escape)
|
||||||
@ -59,31 +54,12 @@ async fn main() {
|
|||||||
break;
|
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
|
// Clear and render
|
||||||
macroquad::prelude::clear_background(WHITE);
|
macroquad::prelude::clear_background(WHITE);
|
||||||
app.process_commands();
|
app.process_commands();
|
||||||
app.update();
|
app.update();
|
||||||
app.render();
|
app.render();
|
||||||
|
|
||||||
frame += 1;
|
|
||||||
if frame % 60 == 0 {
|
|
||||||
println!("Rendered {} frames", frame / 60);
|
|
||||||
}
|
|
||||||
|
|
||||||
macroquad::prelude::next_frame().await;
|
macroquad::prelude::next_frame().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,13 +76,12 @@ fn run_game_logic(
|
|||||||
hangman_tx: TurtleCommandSender,
|
hangman_tx: TurtleCommandSender,
|
||||||
lines_tx: TurtleCommandSender,
|
lines_tx: TurtleCommandSender,
|
||||||
smiley_tx: TurtleCommandSender,
|
smiley_tx: TurtleCommandSender,
|
||||||
tx: mpsc::Sender<GameMessage>,
|
|
||||||
) {
|
) {
|
||||||
let secret = choose_word();
|
let secret = choose_word();
|
||||||
println!("Starting hangman game...");
|
println!("Starting hangman game...");
|
||||||
println!("Secret word has {} letters", secret.len());
|
println!("Secret word has {} letters", secret.len());
|
||||||
|
|
||||||
// Setup: Position hangman turtle and draw base (hill + mast)
|
// Setup: Position hangman turtle and draw base (hill)
|
||||||
{
|
{
|
||||||
let mut plan = create_turtle_plan();
|
let mut plan = create_turtle_plan();
|
||||||
setup_hangman(&mut plan);
|
setup_hangman(&mut plan);
|
||||||
@ -114,37 +89,23 @@ fn run_game_logic(
|
|||||||
hangman_tx.send(plan.build()).ok();
|
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 all_guesses = String::new();
|
||||||
let mut wrong_guesses = 0;
|
let mut wrong_guesses = 0;
|
||||||
const MAX_WRONG: usize = 8; // 8 body parts after base
|
const MAX_WRONG: usize = 8; // 8 body parts after base
|
||||||
|
|
||||||
// Main game loop
|
|
||||||
loop {
|
|
||||||
// Draw current state of lines
|
// Draw current state of lines
|
||||||
draw_lines_state(&lines_tx, &secret, &all_guesses);
|
draw_lines_state(&lines_tx, &secret, &all_guesses);
|
||||||
|
// Main game loop
|
||||||
|
loop {
|
||||||
// Check if won
|
// Check if won
|
||||||
if secret.chars().all(|c| all_guesses.contains(c)) {
|
if secret.chars().all(|c| all_guesses.contains(c)) {
|
||||||
draw_smiley(&smiley_tx, true);
|
draw_smiley(&smiley_tx, true);
|
||||||
tx.send(GameMessage::GameOver {
|
|
||||||
won: true,
|
|
||||||
word: secret.to_string(),
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if lost
|
// Check if lost
|
||||||
if wrong_guesses >= MAX_WRONG {
|
if wrong_guesses >= MAX_WRONG {
|
||||||
draw_smiley(&smiley_tx, false);
|
draw_smiley(&smiley_tx, false);
|
||||||
tx.send(GameMessage::GameOver {
|
|
||||||
won: false,
|
|
||||||
word: secret.to_string(),
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,16 +113,12 @@ fn run_game_logic(
|
|||||||
let guess = ask_for_letter();
|
let guess = ask_for_letter();
|
||||||
let guess_lower = guess.to_lowercase();
|
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);
|
all_guesses.push_str(&guess_lower);
|
||||||
|
|
||||||
if secret.contains(&guess_lower) {
|
if secret.contains(&guess_lower) {
|
||||||
println!("✓ Correct! '{}' is in the word", guess_lower);
|
println!("✓ Correct! '{}' is in the word", guess_lower);
|
||||||
|
// Draw current state of lines
|
||||||
|
draw_lines_state(&lines_tx, &secret, &all_guesses);
|
||||||
} else {
|
} else {
|
||||||
println!("✗ Wrong! '{}' is NOT in the word", guess_lower);
|
println!("✗ Wrong! '{}' is NOT in the word", guess_lower);
|
||||||
wrong_guesses += 1;
|
wrong_guesses += 1;
|
||||||
@ -221,9 +178,9 @@ fn draw_hangman_step(tx: &TurtleCommandSender, step: usize) {
|
|||||||
|
|
||||||
// Hangman drawing functions (scaled down for visibility)
|
// Hangman drawing functions (scaled down for visibility)
|
||||||
fn draw_hill(plan: &mut TurtlePlan) {
|
fn draw_hill(plan: &mut TurtlePlan) {
|
||||||
plan.circle_left(50.0, 180.0, 36)
|
plan.left(135.0)
|
||||||
.left(180.0)
|
.circle_left(100.0, 90.0, 36)
|
||||||
.circle_right(50.0, 90.0, 36)
|
.circle_left(100.0, -45.0, 36)
|
||||||
.right(90.0);
|
.right(90.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,24 +235,21 @@ fn draw_legs(plan: &mut TurtlePlan) {
|
|||||||
|
|
||||||
fn draw_lines_state(tx: &TurtleCommandSender, secret: &str, all_guesses: &str) {
|
fn draw_lines_state(tx: &TurtleCommandSender, secret: &str, all_guesses: &str) {
|
||||||
let mut plan = create_turtle_plan();
|
let mut plan = create_turtle_plan();
|
||||||
plan.hide()
|
plan.reset()
|
||||||
.set_speed(1001) // Instant mode
|
//.hide()
|
||||||
.set_pen_color(BLACK)
|
.set_pen_color(BLACK)
|
||||||
.set_pen_width(2.0)
|
.set_pen_width(2.0)
|
||||||
.pen_up()
|
.pen_up()
|
||||||
.go_to(vec2(-100.0, 100.0)) // Top of screen
|
.go_to(vec2(-100.0, 100.0))
|
||||||
.pen_down()
|
.pen_down();
|
||||||
.right(90.0);
|
|
||||||
|
|
||||||
// Print word state in console
|
// Print word state in console
|
||||||
print!("Word: ");
|
print!("Word: ");
|
||||||
for letter in secret.chars() {
|
for letter in secret.chars() {
|
||||||
if all_guesses.contains(letter) {
|
if all_guesses.contains(letter) {
|
||||||
print!("{} ", letter);
|
print!("{} ", letter);
|
||||||
plan.forward(20.0);
|
|
||||||
} else {
|
} else {
|
||||||
print!("_ ");
|
print!("_ ");
|
||||||
plan.forward(20.0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
@ -305,20 +259,21 @@ fn draw_lines_state(tx: &TurtleCommandSender, secret: &str, all_guesses: &str) {
|
|||||||
if all_guesses.contains(letter) {
|
if all_guesses.contains(letter) {
|
||||||
// Draw green circle for revealed letter
|
// Draw green circle for revealed letter
|
||||||
plan.pen_up()
|
plan.pen_up()
|
||||||
.forward(2.5)
|
|
||||||
.right(90.0)
|
.right(90.0)
|
||||||
|
.forward(2.5)
|
||||||
.set_pen_color(GREEN)
|
.set_pen_color(GREEN)
|
||||||
.pen_down()
|
.pen_down()
|
||||||
.circle_left(7.5, 360.0, 24)
|
.circle_left(7.5, 360.0, 24)
|
||||||
.set_pen_color(BLACK)
|
.pen_up()
|
||||||
.left(90.0)
|
|
||||||
.backward(2.5)
|
.backward(2.5)
|
||||||
.pen_up();
|
.left(90.0)
|
||||||
|
.set_pen_color(BLACK)
|
||||||
|
.pen_down();
|
||||||
} else {
|
} else {
|
||||||
// Draw black underscore
|
// Draw black underscore
|
||||||
plan.forward(5.0);
|
plan.forward(15.0);
|
||||||
}
|
}
|
||||||
plan.forward(15.0).pen_down();
|
plan.pen_up().forward(15.0).pen_down();
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.send(plan.build()).ok();
|
tx.send(plan.build()).ok();
|
||||||
|
|||||||
@ -635,6 +635,41 @@ impl TurtlePlan {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resets the turtle to its default state.
|
||||||
|
///
|
||||||
|
/// This clears all drawings, clears the animation queue, and resets all turtle parameters:
|
||||||
|
/// - Position: (0, 0)
|
||||||
|
/// - Heading: 0° (facing right)
|
||||||
|
/// - Pen: down
|
||||||
|
/// - Pen width: 2.0
|
||||||
|
/// - Pen color: black
|
||||||
|
/// - Fill color: none
|
||||||
|
/// - Visibility: visible
|
||||||
|
/// - Shape: arrow
|
||||||
|
/// - Speed: default
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # use turtle_lib::*;
|
||||||
|
/// #
|
||||||
|
/// #[turtle_main("Reset Example")]
|
||||||
|
/// fn draw(turtle: &mut TurtlePlan) {
|
||||||
|
/// // Draw something
|
||||||
|
/// turtle.forward(100.0);
|
||||||
|
///
|
||||||
|
/// // Reset everything back to default
|
||||||
|
/// turtle.reset();
|
||||||
|
///
|
||||||
|
/// // Start fresh
|
||||||
|
/// turtle.forward(50.0);
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn reset(&mut self) -> &mut Self {
|
||||||
|
self.queue.push(TurtleCommand::Reset);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Consumes the `TurtlePlan` and returns the command queue.
|
/// Consumes the `TurtlePlan` and returns the command queue.
|
||||||
///
|
///
|
||||||
/// Use this to finalize the turtle commands and pass them to `TurtleApp`.
|
/// Use this to finalize the turtle commands and pass them to `TurtleApp`.
|
||||||
|
|||||||
@ -42,6 +42,9 @@ pub enum TurtleCommand {
|
|||||||
// Fill operations
|
// Fill operations
|
||||||
BeginFill,
|
BeginFill,
|
||||||
EndFill,
|
EndFill,
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
Reset,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Queue of turtle commands with execution state
|
/// Queue of turtle commands with execution state
|
||||||
|
|||||||
@ -30,7 +30,6 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle)
|
|||||||
state.begin_fill(fill_color);
|
state.begin_fill(fill_color);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
TurtleCommand::EndFill => {
|
TurtleCommand::EndFill => {
|
||||||
if let Some(mut fill_state) = state.filling.take() {
|
if let Some(mut fill_state) = state.filling.take() {
|
||||||
if !fill_state.current_contour.is_empty() {
|
if !fill_state.current_contour.is_empty() {
|
||||||
@ -79,7 +78,6 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle)
|
|||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
TurtleCommand::PenUp => {
|
TurtleCommand::PenUp => {
|
||||||
state.params.pen_down = false;
|
state.params.pen_down = false;
|
||||||
if state.filling.is_some() {
|
if state.filling.is_some() {
|
||||||
@ -91,7 +89,6 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle)
|
|||||||
state.close_fill_contour();
|
state.close_fill_contour();
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
TurtleCommand::PenDown => {
|
TurtleCommand::PenDown => {
|
||||||
state.params.pen_down = true;
|
state.params.pen_down = true;
|
||||||
if state.filling.is_some() {
|
if state.filling.is_some() {
|
||||||
@ -106,7 +103,23 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle)
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => false, // Not a side-effect-only command
|
TurtleCommand::Reset => {
|
||||||
|
state.reset();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
TurtleCommand::Move(_)
|
||||||
|
| TurtleCommand::Turn(_)
|
||||||
|
| TurtleCommand::Circle { .. }
|
||||||
|
| TurtleCommand::Goto(_)
|
||||||
|
| TurtleCommand::SetColor(_)
|
||||||
|
| TurtleCommand::SetFillColor(_)
|
||||||
|
| TurtleCommand::SetPenWidth(_)
|
||||||
|
| TurtleCommand::SetSpeed(_)
|
||||||
|
| TurtleCommand::SetShape(_)
|
||||||
|
| TurtleCommand::SetHeading(_)
|
||||||
|
| TurtleCommand::ShowTurtle
|
||||||
|
| TurtleCommand::HideTurtle => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,6 +261,11 @@ pub fn execute_command(command: &TurtleCommand, state: &mut Turtle) {
|
|||||||
TurtleCommand::ShowTurtle => state.params.visible = true,
|
TurtleCommand::ShowTurtle => state.params.visible = true,
|
||||||
TurtleCommand::HideTurtle => state.params.visible = false,
|
TurtleCommand::HideTurtle => state.params.visible = false,
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
TurtleCommand::Reset => {
|
||||||
|
state.reset();
|
||||||
|
}
|
||||||
|
|
||||||
_ => {} // Already handled by execute_command_side_effects
|
_ => {} // Already handled by execute_command_side_effects
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -92,11 +92,18 @@ impl Turtle {
|
|||||||
Angle::radians(self.params.heading)
|
Angle::radians(self.params.heading)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reset turtle to default state (preserves turtle_id)
|
/// Reset turtle to default state (preserves turtle_id and queued commands)
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
let id = self.turtle_id;
|
// Clear all drawings
|
||||||
*self = Self::default();
|
self.commands.clear();
|
||||||
self.turtle_id = id;
|
|
||||||
|
// Clear fill state
|
||||||
|
self.filling = None;
|
||||||
|
|
||||||
|
// Reset parameters to defaults
|
||||||
|
self.params = TurtleParams::default();
|
||||||
|
|
||||||
|
// Keep turtle_id and tween_controller (preserves queued commands)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start recording fill vertices
|
/// Start recording fill vertices
|
||||||
|
|||||||
@ -436,6 +436,10 @@ impl TweenController {
|
|||||||
// Fill commands don't change turtle state for tweening purposes
|
// Fill commands don't change turtle state for tweening purposes
|
||||||
// They're handled directly in execution
|
// They're handled directly in execution
|
||||||
}
|
}
|
||||||
|
TurtleCommand::Reset => {
|
||||||
|
// Reset returns to default state
|
||||||
|
target = TurtleParams::default();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
target
|
target
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user