diff options
Diffstat (limited to '2020-overdrive/src/state.rs')
-rw-r--r-- | 2020-overdrive/src/state.rs | 355 |
1 files changed, 355 insertions, 0 deletions
diff --git a/2020-overdrive/src/state.rs b/2020-overdrive/src/state.rs new file mode 100644 index 0000000..acbce80 --- /dev/null +++ b/2020-overdrive/src/state.rs @@ -0,0 +1,355 @@ +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<BTreeSet<Position>>, + pub oil_spills: Rc<BTreeSet<Position>>, + pub powerup_oils: Rc<BTreeSet<Position>>, + pub powerup_boosts: Rc<BTreeSet<Position>>, +} + +#[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<Command> { + 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<Command> { + 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<Position>, + oil_spills: &BTreeSet<Position>, + powerup_oils: &BTreeSet<Position>, + powerup_boosts: &BTreeSet<Position>, + 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 + } +} |