Win Condition

We can now count laps, but the lap counter goes up indefinitely. At some point teh game should end and you should see a more detailed scoreboard - things like best lap time and average lap time.

Let's put a new state in the GameState enum:

#![allow(unused)]
fn main() {
enum GameState {
    Menu,
    Playing,
    ScoreScreen,
}
}

And a function to the gameplay struct to check if the game is completed:

#![allow(unused)]
fn main() {
    /// Returns True when the game is complete.
    /// The game is considered complete when everyone has
    /// done enough laps
    pub fn game_complete(&self) -> bool {
        for score in self.scores.iter() {
            if score.laps.len() < NUM_LAPS_TO_WIN {
                return false
            }
        }
        return true
    }
}

And in the App struct's play_game function, we can now initiate the state change:

#![allow(unused)]
fn main() {
    pub fn play_game(&mut self, dt: f64) {
        self.gameplay.update(dt, &self.key_map);
        let ship_entity_refs = self.gameplay.ship_entities.iter().collect();
        let trail_entity_refs = self.gameplay.trails.iter().collect();

        self.renderer.render(
            &self.gameplay.camera.get_camera_matrix(),
            ship_entity_refs,
            trail_entity_refs,
            self.gameplay.get_text_entities(),
        );

        // If the game is finished, show the score screen
        if self.gameplay.game_complete() {
            self.game_state = GameState::ScoreScreen;
        }
    }
}

And we can write another function that runs in the ScoreScreen state:

#![allow(unused)]
fn main() {
pub fn show_scores(&mut self, dt: f64) {
        self.gameplay.update(dt * 0.1, &self.key_map);
        let ship_entity_refs = self.gameplay.ship_entities.iter().collect();
        let trail_entity_refs = self.gameplay.trails.iter().collect();

        self.renderer.render(
            &self.gameplay.camera.get_camera_matrix(),
            ship_entity_refs,
            trail_entity_refs,
            self.score_screen.get_text_entities(),
        );

        // If the game is finished, show the score screen
        if self.key_map.start_game == KeyState::JustReleased {
            self.game_state = GameState::Menu;
            self.reset();
        }
    }
}

You may notice something strange there. I'm still calling self.gameplay.update and I'm still rendering the ship entities and trails. That's because I think it would be cool for the game to continue slow-motion in the background.

I have, of course, created a ScoreScreen struct that contains the text entities. There's not much particularly complex in it except for the code that populates the score screen text:

#![allow(unused)]
fn main() {
impl ScoreScreen {

    <<< snip >>>

    pub fn populate_scores(&mut self, ships: &Vec<Ship>, scores: &Vec<Score>) {
        self.scores.clear();

        let mut ship_and_score_refs: Vec<(&Ship, &Score)> =
            ships.iter().zip(scores.iter()).collect();
        ship_and_score_refs.sort_by(|a, b| a.1.cmp(b.1));

        self.scores.append_string("   Avg   Best", &[0.5, 0.5, 0.5]);

        for (ship, score) in ship_and_score_refs {
            let color = [ship.color.0, ship.color.1, ship.color.2];
            
            let best_lap = score.get_best_lap();
            let average_lap = score.get_average_lap();
            
            self.scores.append_string("~ ", &color);
            self.scores.append_string(&format_time(average_lap), &color);
            self.scores.append_string(" ", &color);
            self.scores.append_string(&format_time(best_lap), &color);
        }
    }

}

fn format_time(time: Option<f64>) -> String {
    if let Some(sec) = time {
        let seconds = sec as u32;
        let millis = (sec.fract() * 100.0).floor() as u32;
        format!("{:02}:{:02}", seconds, millis)
    } else {
        "--:--".to_string()
    }
}
}

Not to much complex there, just fetch the average and best lap times for each player. Oops, better implement those:

#![allow(unused)]
fn main() {
impl Score {

    <<< snip >>>
    /// Returns a vector of the times for each lap
    pub fn get_lap_timings(&self) -> Vec<f64> {
        let mut lap_times = vec![];
        let mut lap_start_time = 0.0;
        for lap_end_time in &self.laps {
            lap_times.push(lap_end_time - lap_start_time);
            lap_start_time = *lap_end_time;
        }
        // First "lap" is the time it takes to get across
        // the start line
        lap_times.drain(0..1);
        lap_times
    }

    /// Returns the average lap time
    pub fn get_average_lap(&self) -> Option<f64> {
        let lap_timings = self.get_lap_timings();

        if lap_timings.len() > 0 {
            let mut total_time = 0.0;
            for lap_time in &lap_timings {
                total_time += lap_time
            }
            Some(total_time / (lap_timings.len() as f64))
        } else {
            None
        }
        
    }

    pub fn get_best_lap(&self) -> Option<f64> {
        let mut lap_timings = self.get_lap_timings();
        
        // Lap timings should never be NAN
        lap_timings.sort_by(|a, b| a.partial_cmp(b).unwrap());
        lap_timings.first().cloned()
    }
}
}

Remember how the scores are stored as the times that the player crosses the line? This means that we need the function get_lap_timings() to get the duration for each lap.

And that's about it really for the score screen.