use crate::command::Command; use crate::consts::*; use std::collections::BTreeSet; use std::ops::Bound::{Excluded, Included}; use std::rc::Rc; #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub enum GameStatus { Continue, PlayerOneWon, PlayerTwoWon, Draw, // Until I add score I guess } // TODO: Maintain sorted vecs for these BTrees? Do the range counts // with binary searches to find indices only. #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct GameState { pub status: GameStatus, pub players: [Player; 2], pub muds: Rc>, pub oil_spills: Rc>, pub powerup_oils: Rc>, pub powerup_boosts: Rc>, } #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct Player { pub position: Position, pub speed: u16, pub boost_remaining: u8, pub oils: u16, pub boosts: u16, } #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct Position { pub y: u8, pub x: u16, } #[derive(Debug, Clone, Default)] pub struct GameStateUpdateEvents { pub players: [GameStatePlayerUpdateEvents; 2], } #[derive(Debug, Clone, Default)] pub struct GameStatePlayerUpdateEvents { pub boosts_used: u16, pub boosts_collected: u16, pub boosts_maintained: u8, pub mud_hit: u16, pub oil_collected: u16, pub distance_travelled: u16, } impl GameState { pub fn update(&mut self, commands: [Command; 2], events: &mut GameStateUpdateEvents) { if self.status != GameStatus::Continue { return; } let next_positions = [ self.do_command(0, &commands[0], &mut events.players[0]), self.do_command(1, &commands[1], &mut events.players[1]), ]; let next_positions = self.update_player_collisions(next_positions, events); self.update_player_travel(next_positions, events); self.status = if self.players[0].finished() && self.players[1].finished() { if self.players[0].speed > self.players[1].speed { GameStatus::PlayerOneWon } else if self.players[0].speed < self.players[1].speed { GameStatus::PlayerTwoWon } else { GameStatus::Draw } } else if self.players[0].finished() { GameStatus::PlayerOneWon } else if self.players[1].finished() { GameStatus::PlayerTwoWon } else { GameStatus::Continue }; } pub fn reset_players_to_start(&mut self) { self.players[0].position = Position { x: 1, y: 1 }; self.players[1].position = Position { x: 1, y: 4 }; for player in &mut self.players { player.speed = 5; player.boost_remaining = 0; player.oils = 0; player.boosts = 0; } } fn do_command( &mut self, player_index: usize, command: &Command, events: &mut GameStatePlayerUpdateEvents, ) -> Position { use Command::*; self.players[player_index].tick_boost(); let mut next_y = self.players[player_index].position.y; match command { Nothing => {} Accelerate => self.players[player_index].accelerate(), Decelerate => self.players[player_index].decelerate(), TurnLeft => next_y = next_y.saturating_sub(1).max(MIN_Y), TurnRight => next_y = next_y.saturating_add(1).min(MAX_Y), UseBoost => { events.boosts_used += 1; self.players[player_index].boost(); } UseOil => { debug_assert!(self.players[player_index].oils > 0); self.players[player_index].oils = self.players[player_index].oils.saturating_sub(1); let player_position = self.players[player_index].position; let mut oil_spills = (*self.oil_spills).clone(); oil_spills.insert(Position { x: player_position.x.saturating_sub(1), y: player_position.y, }); self.oil_spills = Rc::new(oil_spills); } } let turning = match command { TurnLeft | TurnRight => true, _ => false, }; let next_x = self.players[player_index].next_position_x(turning); Position { x: next_x, y: next_y, } } fn update_player_collisions( &mut self, next_positions: [Position; 2], _events: &mut GameStateUpdateEvents, ) -> [Position; 2] { let same_lanes_before = self.players[0].position.y == self.players[1].position.y; let same_lanes_after = next_positions[0].y == next_positions[1].y; let first_passing_second = self.players[0].position.x < self.players[1].position.x && next_positions[0].x >= next_positions[1].x; let second_passing_first = self.players[1].position.x < self.players[0].position.x && next_positions[1].x >= next_positions[0].x; let same_x_after = next_positions[0].x == next_positions[1].x; if same_lanes_before && same_lanes_after && first_passing_second { [ Position { y: next_positions[0].y, x: next_positions[1].x.saturating_sub(1), }, next_positions[1], ] } else if same_lanes_before && same_lanes_after && second_passing_first { [ next_positions[0], Position { y: next_positions[1].y, x: next_positions[0].x.saturating_sub(1), }, ] } else if same_lanes_after && same_x_after { [ Position { y: self.players[0].position.y, x: self.players[0].next_position_x(true), }, Position { y: self.players[1].position.y, x: self.players[1].next_position_x(true), }, ] } else { next_positions } } fn update_player_travel( &mut self, next_positions: [Position; 2], events: &mut GameStateUpdateEvents, ) { for (i, (player, next_position)) in self .players .iter_mut() .zip(next_positions.iter()) .enumerate() { player.move_along( *next_position, &self.muds, &self.oil_spills, &self.powerup_oils, &self.powerup_boosts, &mut events.players[i], ); } } pub fn valid_moves(&self, player_index: usize) -> Vec { let player = &self.players[player_index]; let mut result = Vec::with_capacity(7); result.push(Command::Nothing); result.push(Command::Accelerate); if player.speed > SPEED_0 { result.push(Command::Decelerate); } if player.position.y > MIN_Y { result.push(Command::TurnLeft); } if player.position.y < MAX_Y - 1 { result.push(Command::TurnRight); } if player.boosts > 0 { result.push(Command::UseBoost); } if player.oils > 0 { result.push(Command::UseOil); } result } pub fn good_moves(&self, player_index: usize) -> Vec { let player = &self.players[player_index]; let mut result = Vec::with_capacity(5); result.push(Command::Accelerate); if player.position.y > MIN_Y { result.push(Command::TurnLeft); } if player.position.y < MAX_Y - 1 { result.push(Command::TurnRight); } if player.boosts > 0 { result.push(Command::UseBoost); } if player.oils > 0 { result.push(Command::UseOil); } result } } impl Player { fn accelerate(&mut self) { self.speed = match self.speed { i if i < SPEED_1 => SPEED_1, i if i < SPEED_2 => SPEED_2, i if i < SPEED_3 => SPEED_3, SPEED_BOOST => SPEED_BOOST, _ => SPEED_4, }; } fn decelerate(&mut self) { self.speed = match self.speed { i if i <= SPEED_1 => SPEED_0, i if i <= SPEED_2 => SPEED_1, i if i <= SPEED_3 => SPEED_2, i if i <= SPEED_4 => SPEED_3, _ => SPEED_4, }; self.boost_remaining = 0; } fn decelerate_from_obstacle(&mut self) { self.speed = match self.speed { i if i <= SPEED_2 => SPEED_1, i if i <= SPEED_3 => SPEED_2, i if i <= SPEED_4 => SPEED_3, _ => SPEED_4, }; self.boost_remaining = 0; } fn boost(&mut self) { debug_assert!(self.boosts > 0); self.speed = SPEED_BOOST; self.boost_remaining = BOOST_DURATION; self.boosts = self.boosts.saturating_sub(1); } fn tick_boost(&mut self) { self.boost_remaining = self.boost_remaining.saturating_sub(1); if self.boost_remaining == 0 && self.speed == SPEED_BOOST { self.speed = SPEED_4; } } fn next_position_x(&mut self, turning: bool) -> u16 { if turning { self.position.x.saturating_add(self.speed.saturating_sub(1)) } else { self.position.x.saturating_add(self.speed) } } fn move_along( &mut self, next_position: Position, muds: &BTreeSet, oil_spills: &BTreeSet, powerup_oils: &BTreeSet, powerup_boosts: &BTreeSet, events: &mut GameStatePlayerUpdateEvents, ) { let range = ( Included(Position { y: next_position.y, x: self.position.x.saturating_add(1), }), Excluded(Position { y: next_position.y, x: next_position.x.saturating_add(1), }), ); let mud_hit = muds .range(range) .count() .saturating_add(oil_spills.range(range).count()); for _ in 0..mud_hit { self.decelerate_from_obstacle(); } let oil_collected = powerup_oils.range(range).count() as u16; self.oils = self.oils.saturating_add(oil_collected); let boosts_collected = powerup_boosts.range(range).count() as u16; self.boosts = self.boosts.saturating_add(boosts_collected); events.mud_hit = events.mud_hit.saturating_add(mud_hit as u16); events.boosts_collected = events.boosts_collected.saturating_add(boosts_collected); events.oil_collected = events.oil_collected.saturating_add(oil_collected); events.distance_travelled = events .distance_travelled .saturating_add(next_position.x.saturating_sub(self.position.x)); if self.speed == SPEED_BOOST { events.boosts_maintained = events.boosts_maintained.saturating_add(1); } self.position = next_position; } fn finished(&self) -> bool { self.position.x >= WIDTH } }