summaryrefslogtreecommitdiff
path: root/2020-overdrive/src/state.rs
diff options
context:
space:
mode:
Diffstat (limited to '2020-overdrive/src/state.rs')
-rw-r--r--2020-overdrive/src/state.rs355
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
+ }
+}