Implement High Score System with Persistence in Rust
Mục tiêu: Add a high score system to a Rust quiz game with file persistence using Serde for JSON serialization.
Adding High Score System to Rust Quiz Game
Now that we have the basic quiz game functionality in place, let’s enhance it by adding a high score system. This will involve saving the scores to a file so they persist between game sessions. We’ll create a function to save high scores and later implement functions to load and display them.
Step: Add a function to save high scores to a file
To save high scores, we’ll need to:
- Create a data structure to represent high scores
- Implement a function to save scores to a file
- Handle potential file operations errors
Let’s start by creating a new module for high score functionality.
// src/high_scores/mod.rs
use serde::{Serialize, Deserialize};
use std::fs;
use std::path::Path;
#[derive(Serialize, Deserialize, Debug)]
pub struct HighScore {
pub name: String,
pub score: u32,
}
pub fn save_high_scores(scores: Vec<HighScore>, file_path: &str) -> Result<(), std::io::Error> {
// Serialize the scores to JSON
let json = serde_json::to_string_pretty(&scores)?;
// Write to file
fs::write(Path::new(file_path), json.as_bytes())?;
Ok(())
}
pub fn load_high_scores(file_path: &str) -> Result<Vec<HighScore>, std::io::Error> {
if let Ok(contents) = fs::read_to_string(Path::new(file_path)) {
let scores: Vec<HighScore> = serde_json::from_str(&contents)?;
return Ok(scores);
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"High scores file not found",
))
}
pub fn display_high_scores(scores: &Vec<HighScore>) {
println!("High Scores:");
for (index, score) in scores.iter().enumerate() {
println!("{}: {} - {}", index + 1, score.name, score.score);
}
}
Updating main.rs
We’ll need to modify our main.rs to use this new module:
// main.rs
mod high_scores;
fn main() {
// ... existing code ...
// After calculating final score
let high_scores = high_scores::load_high_scores("high_scores.json").unwrap_or_default();
// Add current score if it's high enough
let mut updated_scores = high_scores;
updated_scores.push(HighScore {
name: user_name.clone(),
score: final_score,
});
updated_scores.sort_by(|a, b| b.score.cmp(&a.score));
updated_scores.truncate(10); // Keep top 10 scores
// Save the updated high scores
high_scores::save_high_scores(updated_scores, "high_scores.json").unwrap();
// Display high scores
high_scores::display_high_scores(&updated_scores);
// ... rest of your code ...
}
Dependencies
Add these dependencies to your Cargo.toml:
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Explanation
- Data Structure: We created a
HighScorestruct that holds a player’s name and their score. This struct implementsSerializeandDeserializetraits for easy JSON conversion. - File Operations:
save_high_scores: Takes a vector ofHighScoreand a file path, serializes the scores to JSON, and writes them to the file.load_high_scores: Reads from the file, deserializes the JSON back into a vector ofHighScore, and returns it.- Display Function: A simple function to print out the high scores in a formatted way.
- Integration: In
main.rs, after the quiz completes, we: - Load existing high scores
- Add the current player’s score
- Sort the scores
- Keep only the top 10 scores
- Save the updated list back to file
- Display the updated high scores
Testing
To test this functionality:
- Run the game multiple times with different names and scores
- Check that the high scores file (
high_scores.json) is being created and updated - Verify that only the top 10 scores are kept
- Check the formatting of the displayed high scores
Next Steps
Now that we can save high scores, the next tasks will be:
- Implementing the function to load high scores
- Adding sorting logic to display scores in order
- Testing the high score system thoroughly
Further Reading
Implementing High Score Loading and Display in Rust Quiz Game
Mục tiêu: Load and display high scores from a JSON file in a Rust quiz game.
Implementing High Score Loading and Display in Rust Quiz Game
In this task, we’ll implement the functionality to load and display high scores for our Rust Quiz Game. This will involve reading from a file, parsing the data, and displaying it in a user-friendly format.
Understanding the Requirements
Before diving into the code, let’s outline what we need to achieve:
- Load High Scores: We need to read high scores from a file. The file format could be JSON or plain text. For this example, we’ll use JSON for better structure.
- Display High Scores: Once loaded, we need to display these scores in a formatted way. This could be a sorted list showing the top scores.
Step-by-Step Implementation
1. Define the Data Structure
First, we need to define a data structure to hold high score information. Let’s create a struct called HighScore:
#[derive(Debug, Serialize, Deserialize)]
struct HighScore {
name: String,
score: u32,
}
#[derive(Debug, Serialize, Deserialize)]
struct HighScores {
scores: Vec<HighScore>,
file_path: String,
}
HighScorestruct holds a player’s name and their scoreHighScoresstruct holds a collection of scores and the file path where they are stored
2. Implement Loading High Scores
We’ll implement a method to load high scores from a file:
use std::fs::File;
use std::io::Read;
use std::path::Path;
impl HighScores {
fn load(&mut self) -> Result<(), Box<dyn std::error::Error>> {
// Try to open the file
let mut file = File::open(&self.file_path)?;
// Read the contents of the file
let mut contents = String::new();
file.read_to_string(&mut contents)?;
// Parse the JSON
let scores: Vec<HighScore> = serde_json::from_str(&contents)?;
// Update our scores vector
self.scores = scores;
Ok(())
}
}
- This method opens the file, reads its contents, and parses the JSON into a vector of
HighScoreobjects - If the file doesn’t exist, we’ll create it with default values in the next step
3. Implement Displaying High Scores
Next, we’ll implement a method to display the high scores:
impl HighScores {
fn display(&self) {
println!("High Scores:");
println!("-----------");
if self.scores.is_empty() {
println!("No scores yet!");
return;
}
// Sort the scores in descending order
let mut sorted_scores = self.scores.clone();
sorted_scores.sort_by(|a, b| b.score.cmp(&a.score));
for (index, score) in sorted_scores.iter().enumerate() {
println!("{}: {} - {}", index + 1, score.name, score.score);
}
println!("-----------");
}
}
- This method prints out the high scores in a formatted way
- Scores are sorted in descending order before being displayed
4. Error Handling
The methods return Result types to handle potential errors gracefully. This is important for file operations where things can go wrong (file not found, permission issues, etc.).
5. Testing the Implementation
Let’s create a test function to verify our implementation:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_high_scores() {
let mut high_scores = HighScores {
scores: vec![HighScore {
name: "Player1".to_string(),
score: 100,
}],
file_path: "test_scores.json".to_string(),
};
// Save initial scores
high_scores.save().unwrap();
// Load scores
high_scores.load().unwrap();
// Display scores
high_scores.display();
}
}
- This test creates a
HighScoresinstance, saves it, loads it back, and displays it - You should see the scores printed to the console when running the test
Next Steps
After implementing this task, you should:
- Integrate the high score system with the main game loop
- Implement the saving functionality
- Add sorting logic for better score display
- Test the system thoroughly with different scenarios
Further Reading
To learn more about the concepts used here, you can explore these topics:
These resources will help you understand the underlying concepts and improve your implementation.
Implementing Sorting Logic for High Scores in Rust
Mục tiêu: A step-by-step guide to implementing sorting logic for high scores in Rust, including struct definition, sorting functions, and display functionality.
Implementing Sorting Logic for High Scores in Rust
Now that we’ve implemented functions to save and load high scores, the next step is to add sorting logic to display the top scores. This will allow players to see the highest achievements in an organized manner.
Step-by-Step Approach
- Define a Struct for High Scores We’ll create a struct to represent high scores. This struct will include the player’s name and their score.
- Implement Sorting Function We’ll create a function that takes a vector of
HighScoreinstances and returns a new vector sorted in descending order based on scores. - Display Sorted Scores We’ll implement a function to display the sorted high scores in a user-friendly format.
- Integrate with Existing Code We’ll show how to integrate this sorting logic with the existing high score loading and saving functions.
Code Implementation
// Define a struct to represent high scores
#[derive(Debug, PartialEq, Eq)]
struct HighScore {
name: String,
score: u32,
}
// Implement sorting logic for high scores
fn sort_high_scores(scores: Vec<HighScore>) -> Vec<HighScore> {
// Sort scores in descending order
let mut sorted_scores = scores;
sorted_scores.sort_by(|a, b| b.score.cmp(&a.score));
sorted_scores
}
// Display high scores in a formatted way
fn display_high_scores(scores: Vec<HighScore>) {
println!("\n--- High Scores ---");
println!("Rank | Name | Score");
println!("----------------------------");
for (rank, score) in scores.iter().enumerate() {
println!(
"{0} | {1:<10} | {2}",
rank + 1,
score.name,
score.score
);
}
println!("----------------------------\n");
}
Explanation
- HighScore Struct: This struct holds the player’s name and their score. The
#[derive(Debug, PartialEq, Eq)]attribute allows us to compare and display instances of this struct. - Sorting Function: The
sort_high_scoresfunction takes a vector ofHighScoreand sorts it in place usingsort_by. The sorting is done in descending order based on the score. - Display Function: The
display_high_scoresfunction prints the high scores in a tabular format. It includes ranks, names, and scores, making it easy to read and understand.
Integration with Existing Code
Assuming you have functions load_high_scores() and save_high_scores(), you can integrate the sorting logic as follows:
fn main() {
// Load existing high scores
let mut high_scores = load_high_scores();
// Sort the high scores
let sorted_scores = sort_high_scores(high_scores);
// Display the sorted high scores
display_high_scores(sorted_scores);
}
Best Practices
- Error Handling: Make sure to handle potential errors when loading and saving files.
- Testing: Test the sorting logic with different datasets to ensure correctness.
- Code Organization: Keep these functions in a separate module (e.g.,
utils/mod.rs) for better code organization.
What to Read Next
To deepen your understanding of Rust’s sorting capabilities:
By following this approach, you’ll have a robust high score system that displays scores in a clear and organized manner.
Testing the High Score System in Rust Quiz Game
Mục tiêu: Ensuring the high score system in a Rust quiz game functions correctly, including saving, loading, and displaying scores, as well as handling edge cases.
Testing the High Score System in Rust Quiz Game
Testing is a critical part of software development, and for the high score system in our Rust Quiz Game, we need to ensure that:
- Scores are saved to and loaded from the file correctly
- High scores are properly sorted and displayed
- The system handles multiple playthroughs without data corruption
- Edge cases like empty files or invalid data are managed
Let’s break down how to test this system thoroughly.
1. Setting Up for Testing
Before we start testing, let’s make sure we have the following components in place:
// high_scores.rs
use std::fs::{File, OpenOptions};
use std::io::{Read, Write};
use std::path::Path;
pub struct HighScores {
file_path: String,
}
impl HighScores {
pub fn new(file_path: &str) -> HighScores {
HighScores {
file_path: file_path.to_string(),
}
}
pub fn save_score(&self, score: i32) -> std::io::Result<()> {
let mut file = OpenOptions::new()
.write(true)
.create(true)
.append(true)
.open(&self.file_path)?;
file.write_all(format!("{}\n", score).as_bytes())?;
Ok(())
}
pub fn load_scores(&self) -> std::io::Result<Vec<i32>> {
if !Path::new(&self.file_path).exists() {
return Ok(Vec::new());
}
let mut file = File::open(&self.file_path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let scores: Vec<i32> = contents
.lines()
.map(|line| line.trim().parse::<i32>().unwrap_or(0))
.collect();
Ok(scores)
}
pub fn display_scores(&self) -> String {
let scores = self.load_scores().unwrap_or_default();
let sorted_scores = scores.iter().cloned().filter(|s| *s > 0).sorted().rev().collect::<Vec<_>>();
let mut display = String::new();
display.push_str("High Scores:\n");
for (index, score) in sorted_scores.iter().enumerate() {
display.push_str(&format!("{}: {}\n", index + 1, score));
}
display
}
}
This implementation includes:
- Saving scores to a file
- Loading scores from a file
- Displaying sorted scores in descending order
- Basic error handling
2. Unit Testing
Let’s write some unit tests to verify individual components:
#[cfg(test)]
mod tests {
use super::*;
use std::fs::remove_file;
#[test]
fn test_save_and_load_score() {
let tmp_file = "test_scores.txt";
let high_scores = HighScores::new(tmp_file);
// Save some test scores
high_scores.save_score(100).unwrap();
high_scores.save_score(50).unwrap();
// Load and verify scores
let loaded_scores = high_scores.load_scores().unwrap();
assert_eq!(loaded_scores, vec![100, 50]);
// Clean up
remove_file(tmp_file).unwrap();
}
#[test]
fn test_display_scores() {
let tmp_file = "test_scores.txt";
let high_scores = HighScores::new(tmp_file);
// Save test scores
high_scores.save_score(150).unwrap();
high_scores.save_score(75).unwrap();
// Display scores
let display = high_scores.display_scores();
println!("{}", display);
assert!(display.contains("1: 150"));
assert!(display.contains("2: 75"));
remove_file(tmp_file).unwrap();
}
#[test]
fn test_empty_file_handling() {
let tmp_file = "empty_test.txt";
let high_scores = HighScores::new(tmp_file);
// Try to load from non-existent file
let loaded_scores = high_scores.load_scores().unwrap();
assert_eq!(loaded_scores, Vec::new());
// Save a score
high_scores.save_score(200).unwrap();
// Load again
let loaded_after_save = high_scores.load_scores().unwrap();
assert_eq!(loaded_after_save, vec![200]);
remove_file(tmp_file).unwrap();
}
}
These tests cover:
- Basic save/load functionality
- Display formatting
- Handling of empty/non-existent files
3. Integration Testing
Integration testing involves testing the high score system as part of the whole application. Here’s how you can test it:
- Play through the game once and get a score
- Save the score
- Play again with a different score
- Verify that both scores appear in the high scores list
- Repeat multiple times with different scores
You can simulate this in code:
fn main() {
let mut scores = vec![];
// Simulate multiple playthroughs
scores.push(150);
scores.push(120);
scores.push(200);
scores.push(90);
let high_scores = HighScores::new("high_scores.txt");
// Save all scores
for score in scores {
high_scores.save_score(score).unwrap();
}
// Display the high scores
println!("{}", high_scores.display_scores());
}
4. Testing Edge Cases
- Empty File: Verify that if the high scores file is empty, the system handles it gracefully without crashing.
- Invalid Data: If somehow non-numeric data gets into the file, ensure the system ignores it or handles it appropriately.
- Large Number of Scores: Test with a large number of scores to ensure performance remains acceptable.
- Zero or Negative Scores: Verify that only positive scores are saved and displayed.
5. User Interaction Testing
After implementing the high score system, test it with real user interaction:
- Play the quiz game
- Enter different answers to get various scores
- After each game, check if the high scores are updated correctly
- Verify that the display shows the correct scores in the right order
6. Automated Testing
You can create a test script that runs multiple instances of the game and saves random scores to verify the system’s robustness:
use rand::Rng;
use std::thread;
use std::time::Duration;
fn test_robustness() {
let mut rng = rand::thread_rng();
for _ in 0..10 {
let score = rng.gen_range(1..=200);
let high_scores = HighScores::new("high_scores.txt");
high_scores.save_score(score).unwrap();
// Small delay between saves
thread::sleep(Duration::from_millis(100));
}
let high_scores = HighScores::new("high_scores.txt");
println!("Final high scores:\n{}", high_scores.display_scores());
}
7. Cleaning Up
After testing, make sure to clean up any test files you created:
fn cleanup() {
let _ = remove_file("high_scores.txt");
let _ = remove_file("test_scores.txt");
let _ = remove_file("empty_test.txt");
}
Conclusion
Testing the high score system involves:
- Unit testing individual components
- Integration testing within the whole application
- Testing edge cases and boundary conditions
- Automated testing for robustness
- User interaction testing
- Proper cleanup of test files
By following this structured approach, you can ensure that your high score system works reliably and as expected.