use alloc::string::String; use alloc::{format, vec, vec::Vec, boxed::Box}; use async_trait::async_trait; use crate::std::io::{Color, Display, KeyStroke, Stdin}; use crate::std::time; use crate::std::application::{Application, Error}; use crate::std::render::{ColorCode, ColouredChar, Dimensions, Frame, RenderError, Window}; use crate::std::random::Random; use crate::system::std::render; use super::super::super::lib::geometry::{Position, Direction}; #[derive(PartialEq)] enum Gamemode { SinglePlayer, WithAI(u8, u8, u8), // number of ai, ai length, number of poi's Uninitialised, } #[derive(Clone, Debug, PartialEq)] enum Status { Scored, Lost, Exited, None, } pub struct Game { snakes: Vec, pois: Vec, score: u8, gamemode: Gamemode, } #[async_trait] impl Application for Game { fn new() -> Self { Self { snakes: Vec::new(), pois: Vec::new(), score: 0, gamemode: Gamemode::Uninitialised, } } async fn run(&mut self, window: Option, args: Vec) -> Result<(), Error> { let _settings = [0, 0, 0]; // ai_count, snake_len, poi_count if args.len() == 0 { self.gamemode = Gamemode::SinglePlayer; } else { match args[0].as_str() { "easy" => { self.gamemode = Gamemode::SinglePlayer; }, "normal" => { self.gamemode = Gamemode::WithAI(5, 5, 5); }, "impossible" => { self.gamemode = Gamemode::WithAI(10, 15, 5); } "chaos" => { self.gamemode = Gamemode::WithAI(20, 15, 20); } _ => { self.gamemode = Gamemode::SinglePlayer; }, } } // sets up game based on difficulty self.prepare(); // switch OS to application mode let _d = Display::borrow(); // render the initial state of the screen. self.render().map_err(|_| Error::ApplicationError(String::from("failed to render game screen")))?; // run the game self.gameloop().await?; Ok(()) } } impl Game { fn prepare(&mut self) { self.snakes.push(Snake::player(0, 3)); if let Gamemode::WithAI(ai_count, snake_len, poi_count) = self.gamemode { for i in 0..ai_count { self.snakes.push(Snake::ai(i as usize + 1, snake_len)); } (0..poi_count).for_each(|_| self.new_poi()); } else { self.new_poi() } } fn respawn_snakes(&mut self) { if let Gamemode::WithAI(_ai_count, snake_len, _poi) = self.gamemode { self.snakes.push(Snake::ai(self.snakes.len() + 1, snake_len)); } } async fn gameloop(&mut self) -> Result<(), Error> { // main gameloop let mut _all_points: Vec; 'gameloop: loop { time::wait(0.1); let mut _points: Vec; let length = self.snakes.len(); for i in 0..length { let points: Vec = self.snakes.clone().into_iter().map(|s| s.tail).flatten().collect(); let res = self.snakes[i].next(&points, &self.pois); match res { Status::Lost => { if self.snakes[i].ai_controlled { self.snakes.remove(i); self.respawn_snakes(); } else { self.render_end_screen().map_err(|_| Error::ApplicationError(String::from("failed to render end screen")))?; // loop triggers when game is lost loop { match Stdin::keystroke().await { KeyStroke::Char('x') => break 'gameloop, _ => continue, } } } }, Status::Exited => { break 'gameloop }, Status::Scored => { if !self.snakes[i].ai_controlled { self.score += 1; } self.replace_poi(&self.snakes[i].clone().head); // passes a reference to the location of the current snake's head }, Status::None => {}, } } self.snakes.retain(|s| s.alive); self.render().map_err(|_| Error::ApplicationError(String::from("failed to render game screen")))?; }; Ok(()) } fn new_poi(&mut self) { self.pois.push(Position { x: Random::int(3, 76) as i64, y: Random::int(3, 21) as i64 }); } fn replace_poi(&mut self, poi: &Position) { self.pois.remove(self.pois.iter().position(|p| p == poi).unwrap()); self.new_poi(); } fn render(&mut self) -> Result<(), RenderError> { let mut frame = Frame::new(render::Position::new(0, 0), Dimensions::new(80, 25))?; let mut curr_colour = ColorCode::new(Color::LightBlue, Color::Black); for s in self.snakes.clone() { curr_colour = match s.ai_controlled { true => ColorCode::new(Color::Cyan, Color::Black), false => ColorCode::new(Color::LightGreen, Color::Black), }; for point in s.tail.iter() { frame[24 - point.y as usize][point.x as usize] = ColouredChar::coloured('▓', curr_colour); } } self.pois.iter().for_each(|poi| { frame[24 - poi.y as usize][poi.x as usize] = ColouredChar::coloured('o', ColorCode::new(Color::Red, Color::Black)); }); let literal = format!("snake go brr score: {}", self.score); let msg = Game::centre_text(80, literal); msg.chars().enumerate().for_each(|(i, c)| { if c != ' ' { frame[1][i] = ColouredChar::coloured(c, ColorCode::new(Color::LightGreen, Color::Black)) } }); frame.write_to_screen()?; Ok(()) } fn centre_text(dims: usize, text: String) -> String { // centres text in a string of whitespace of a given length let max_pad = dims / 2; let mut msg = String::new(); msg.push_str(" ".repeat(max_pad - round_up(text.len() as f64 / 2.0)).as_str()); msg.push_str(text.as_str()); msg.push_str(" ".repeat(max_pad - round_down(text.len() as f64 / 2.0 + 0.51)).as_str()); msg } fn render_end_screen(&mut self) -> Result<(), RenderError> { let mut frame = Frame::new(render::Position::new(0, 0), Dimensions::new(80, 25))?; frame[10] = Game::centre_text(80, String::from("u lost")).chars().map(|c| ColouredChar::coloured(c, ColorCode::new(Color::Red, Color::Black))).collect(); frame[12] = Game::centre_text(80, String::from(format!("ur score was {}", self.score))).chars().map(|c| ColouredChar::coloured(c, ColorCode::new(Color::LightGreen, Color::Black))).collect(); frame[14] = Game::centre_text(80, String::from("L bozo")).chars().map(|c| ColouredChar::coloured(c, ColorCode::new(Color::Red, Color::Black))).collect(); frame.write_to_screen()?; Ok(()) } } #[derive(Debug, Clone)] struct Snake { ai_controlled: bool, head: Position, tail: Vec, dir: Direction, alive: bool, } impl Snake { fn ai(id: usize, len: u8) -> Self { Self { ai_controlled: true, head: Position { x: 2 + 2*id as i64 * 2, y: 9 }, tail: (1..=len as i64).map(|p| Position { x: 2 + 2*id as i64, y: 5 + p}).collect(), dir: Direction::Degrees0, alive: true, } } fn player(id: usize, len: u8) -> Self { Self { ai_controlled: false, head: Position { x: 2 + 2*id as i64, y: 9 }, tail: (1..=len as i64).map(|p| Position { x: 2 + 2*id as i64, y: 5 + p}).collect(), dir: Direction::Degrees0, alive: true, } } fn next(&mut self, tails: &Vec, points_of_interest: &Vec) -> Status { // returns (lose_condition, scored) // uses pathing algorithm if ai else keyboard input if human if self.ai_controlled { self.dir = PathFinder::decide(&self.head, tails, points_of_interest); } else { if let Some(KeyStroke::Char(c)) = Stdin::try_keystroke() { self.dir = match c { 'w' => Direction::Degrees0, 'a' => Direction::Degrees270, 's' => Direction::Degrees180, 'd' => Direction::Degrees90, 'x' => return Status::Exited, _ => self.dir.clone(), }; } } if self.dir != Direction::None { self.tail.push(self.head.clone()); } match self.dir { Direction::Degrees0 => self.head.y += 1, Direction::Degrees180 => self.head.y -= 1, Direction::Degrees270 => self.head.x -= 1, Direction::Degrees90 => self.head.x += 1, Direction::None => {}, } if self.lose_condition(tails) { self.alive = false; self.tail.remove(0); return Status::Lost; } if points_of_interest.contains(&self.head) { return Status::Scored; } else { if self.dir != Direction::None { self.tail.remove(0); } } Status::None } fn lose_condition(&mut self, tails: &Vec) -> bool { // where tails includes the tail of every other snake let p = self.head.clone(); let snake_overlaps = tails.contains(&self.head); // checks if any part of the snake overlaps itself let out_of_bounds = p.x < 0 || p.y < 0 || p.x > 79 || p.y > 24; // checks if the snake goes out of bounds snake_overlaps || out_of_bounds } } struct PathFinder {} impl PathFinder { fn decide(head: &Position, tails: &Vec, pois: &Vec) -> Direction { let nearest_poi = head.nearest(pois); let rel_pos = head.get_offset(&nearest_poi); // check actions don't lose them the game let mut possible_moves = Vec::new(); let mut h: Position; h = Position { x: head.x + 1, y: head.y }; if !(PathFinder::check_bounds(&h) || PathFinder::check_collision(&h, &tails)) { possible_moves.push(Direction::Degrees90); } h = Position { x: head.x - 1, y: head.y }; if !(PathFinder::check_bounds(&h) || PathFinder::check_collision(&h, &tails)) { possible_moves.push(Direction::Degrees270); } h = Position { x: head.x, y: head.y + 1 }; if !(PathFinder::check_bounds(&h) || PathFinder::check_collision(&h, &tails)) { possible_moves.push(Direction::Degrees0); } h = Position { x: head.x, y: head.y - 1 }; if !(PathFinder::check_bounds(&h) || PathFinder::check_collision(&h, &tails)) { possible_moves.push(Direction::Degrees180); } if possible_moves.is_empty() { // panic!("no possible moves"); // use for debugging if a snake cannot find a move for some reason return Direction::Degrees90; } else { let optimal = PathFinder::optimal_move(head, &rel_pos, &possible_moves); // serial_println!("{:?} {:?} {:?} {:?}", nearest_poi, rel_pos, head, optimal); return optimal; } } fn optimal_move(_head: &Position, rel_pos: &Position, moves: &Vec) -> Direction { let mut optimal_moves = vec![Direction::None; 4]; let x_offset: usize; let y_offset: usize; if rel_pos.x.abs() > rel_pos.y.abs() { y_offset = 1; x_offset = 0; } else { x_offset = 1; y_offset = 0; } if rel_pos.x < 0 { optimal_moves[x_offset] = Direction::Degrees270; optimal_moves[x_offset + 2] = Direction::Degrees90; } else { optimal_moves[x_offset] = Direction::Degrees90; optimal_moves[x_offset + 2] = Direction::Degrees270; } if rel_pos.y < 0 { optimal_moves[y_offset] = Direction::Degrees180; optimal_moves[y_offset + 2] = Direction::Degrees0; } else { optimal_moves[y_offset] = Direction::Degrees0; optimal_moves[y_offset + 2] = Direction::Degrees180; } //println!("moves: {:?}, optimal_moves: {:?}, rel_pos: {:?}", moves, optimal_moves, rel_pos); for m in optimal_moves { if moves.contains(&m) { return m; } }; // this should never be used, the above statement should always return a value. panic!("No optimal move found (this should not happen)"); } fn check_bounds(head: &Position) -> bool { head.x < 0 || head.y < 0 || head.x > 79 || head.y > 24 } fn check_collision(head: &Position, tails: &Vec) -> bool { tails.contains(&head) } } fn round_up(n: f64) -> usize { (n + 0.99) as usize } fn round_down(n: f64) -> usize { n as usize }