Created
May 2, 2023 16:41
-
-
Save justinledwards/dae3b9e6dea6c3fd4e1bd659d3b082fb to your computer and use it in GitHub Desktop.
Rust Snake GUI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "snake" | |
version = "0.1.0" | |
edition = "2021" | |
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | |
[dependencies] | |
piston_window = "0.128.0" | |
rand = "0.8.5" | |
tinyfiledialogs = "3.9.1" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::env; | |
extern crate piston_window; | |
extern crate rand; | |
extern crate tinyfiledialogs; | |
use piston_window::*; | |
use rand::{ Rng }; | |
use std::collections::VecDeque; | |
use tinyfiledialogs::{ message_box_ok, MessageBoxIcon }; | |
fn main() { | |
let args: Vec<String> = env::args().collect(); | |
let default_speed = 60000.0; | |
let speed = if args.len() > 1 { | |
args[1].parse::<f64>().unwrap_or(default_speed) | |
} else { | |
default_speed | |
}; | |
let mut accumulated_time = 0.0; | |
let (w, h) = (20, 20); | |
let mut window: PistonWindow = WindowSettings::new("Snake", [w * 20, h * 20]) | |
.exit_on_esc(true) | |
.build() | |
.unwrap(); | |
let mut game = Game::new(w, h, speed); | |
while let Some(e) = window.next() { | |
if let Some(Button::Keyboard(key)) = e.press_args() { | |
game.key_pressed(key); | |
} | |
window.draw_2d(&e, |c, g, _device| { | |
clear([1.0; 4], g); | |
game.draw(&c, g); | |
}); | |
if let Some(u) = e.update_args() { | |
accumulated_time += u.dt; | |
let update_interval = 1.0 / (speed / (20.0 * 60.0)); | |
if accumulated_time >= update_interval { | |
game.update(&mut window); | |
accumulated_time -= update_interval; | |
} | |
} | |
} | |
} | |
struct Game { | |
snake: Snake, | |
food: Food, | |
w: u32, | |
h: u32, | |
key_buffer: VecDeque<Direction>, | |
speed: f64, | |
game_over: bool, | |
} | |
impl Game { | |
fn new(w: u32, h: u32, speed: f64) -> Game { | |
Game { | |
snake: Snake::new(), | |
food: Food::new(w, h), | |
w, | |
h, | |
key_buffer: VecDeque::new(), | |
speed, | |
game_over: false, | |
} | |
} | |
fn key_pressed(&mut self, key: Key) { | |
let d = match key { | |
Key::Up => Direction::Up, | |
Key::Down => Direction::Down, | |
Key::Left => Direction::Left, | |
Key::Right => Direction::Right, | |
_ => { | |
return; | |
} | |
}; | |
self.key_buffer.push_back(d); | |
} | |
fn update(&mut self, window: &mut PistonWindow) { | |
if self.game_over { | |
return; | |
} | |
if let Some(dir) = self.key_buffer.pop_front() { | |
self.snake.turn(dir); | |
} | |
self.snake.update(); | |
if self.snake.collides_with_food(&self.food) { | |
self.snake.grow(); | |
self.food = Food::new(self.w, self.h); | |
} | |
if self.snake.collides_with_self() { | |
// Handle game over | |
self.game_over = true; | |
let score = self.snake.body.len() - 1; | |
let message = format!("Game over! Score: {} Speed: {:.2}", score, self.speed); | |
println!("{}", message); | |
message_box_ok("Game Over", &message, MessageBoxIcon::Error); | |
// Reset game | |
self.snake = Snake::new(); | |
self.food = Food::new(self.w, self.h); | |
self.key_buffer.clear(); | |
self.game_over = false; | |
} | |
let score = self.snake.body.len() - 1; | |
let title = format!("Snake - Score: {} - Speed: {:.2}", score, self.speed); | |
window.set_title(title); | |
} | |
fn draw(&self, c: &Context, g: &mut G2d) { | |
self.snake.draw(c, g); | |
self.food.draw(c, g); | |
} | |
} | |
struct Food { | |
x: f64, | |
y: f64, | |
} | |
impl Food { | |
fn new(w: u32, h: u32) -> Food { | |
let mut rng = rand::thread_rng(); | |
Food { | |
x: (rng.gen_range(0..w) as f64) * 20.0, | |
y: (rng.gen_range(0..h) as f64) * 20.0, | |
} | |
} | |
fn draw(&self, c: &Context, g: &mut G2d) { | |
rectangle([1.0, 0.0, 0.0, 1.0], [self.x, self.y, 20.0, 20.0], c.transform, g); | |
} | |
} | |
#[derive(PartialEq, Copy, Clone)] | |
enum Direction { | |
Up, | |
Down, | |
Left, | |
Right, | |
} | |
struct Snake { | |
body: Vec<(f64, f64)>, | |
dir: Direction, | |
next_dir: Direction, | |
} | |
impl Snake { | |
fn new() -> Snake { | |
Snake { | |
body: vec![(0.0, 0.0)], | |
dir: Direction::Right, | |
next_dir: Direction::Right, | |
} | |
} | |
fn turn(&mut self, d: Direction) { | |
let opposite = match self.dir { | |
Direction::Up => Direction::Down, | |
Direction::Down => Direction::Up, | |
Direction::Left => Direction::Right, | |
Direction::Right => Direction::Left, | |
}; | |
if d != opposite { | |
self.next_dir = d; | |
} | |
} | |
fn update(&mut self) { | |
self.dir = self.next_dir; | |
let (dx, dy) = match self.next_dir { | |
Direction::Up => (0.0, -20.0), | |
Direction::Down => (0.0, 20.0), | |
Direction::Left => (-20.0, 0.0), | |
Direction::Right => (20.0, 0.0), | |
}; | |
let new_head = ( | |
(self.body[0].0 + dx + 400.0) % 400.0, | |
(self.body[0].1 + dy + 400.0) % 400.0, | |
); | |
self.body.insert(0, new_head); | |
self.body.pop(); | |
} | |
fn grow(&mut self) { | |
self.body.push((0.0, 0.0)); | |
} | |
fn collides_with_food(&self, food: &Food) -> bool { | |
self.body[0].0 == food.x && self.body[0].1 == food.y | |
} | |
fn collides_with_self(&self) -> bool { | |
let head = self.body.first().unwrap(); | |
self.body | |
.iter() | |
.skip(1) | |
.any(|&segment| segment == *head) | |
} | |
fn draw(&self, c: &Context, g: &mut G2d) { | |
let body_len = self.body.len() as f32; | |
for (i, &(x, y)) in self.body.iter().enumerate() { | |
let green = 0.2 + 0.8 * ((i as f32) / body_len); | |
let color = if i == 0 { | |
[0.0, 1.0, 0.0, 1.0] // Light green for the head | |
} else { | |
[0.0, green, 0.0, 1.0] // Gradually darker green for the body | |
}; | |
rectangle(color, [x, y, 20.0, 20.0], c.transform, g); | |
if i == 0 { | |
// Draw eyes for the snake head | |
let eye_color = [1.0, 1.0, 0.0, 1.0]; | |
let pupil_color = [0.0, 0.0, 0.0, 1.0]; | |
let (eye_x_offset, eye_y_offset) = match self.dir { | |
Direction::Up => (4.0, 4.0), | |
Direction::Down => (4.0, 12.0), | |
Direction::Left => (4.0, 4.0), | |
Direction::Right => (12.0, 4.0), | |
}; | |
ellipse(eye_color, [x + eye_x_offset, y + eye_y_offset, 4.0, 4.0], c.transform, g); | |
ellipse( | |
eye_color, | |
[x + 20.0 - eye_x_offset - 4.0, y + eye_y_offset, 4.0, 4.0], | |
c.transform, | |
g | |
); | |
ellipse( | |
pupil_color, | |
[x + eye_x_offset + 1.0, y + eye_y_offset + 1.0, 2.0, 2.0], | |
c.transform, | |
g | |
); | |
ellipse( | |
pupil_color, | |
[x + 20.0 - eye_x_offset - 3.0, y + eye_y_offset + 1.0, 2.0, 2.0], | |
c.transform, | |
g | |
); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment