Files
Zxq5-OS/src/user/bin/games/snake.rs
T
2025-02-18 01:39:26 +00:00

414 lines
13 KiB
Rust

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<Snake>,
pois: Vec<Position>,
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<Window>, args: Vec<String>) -> 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<Position>;
'gameloop: loop {
time::wait(0.1);
let mut _points: Vec<Position>;
let length = self.snakes.len();
for i in 0..length {
let points: Vec<Position> = 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<Position>,
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<Position>, points_of_interest: &Vec<Position>) -> 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<Position>) -> 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<Position>, pois: &Vec<Position>) -> 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>) -> 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<Position>) -> bool {
tails.contains(&head)
}
}
fn round_up(n: f64) -> usize {
(n + 0.99) as usize
}
fn round_down(n: f64) -> usize {
n as usize
}