9.5 - Mini-Project: High-Score System
In this mini-project, we'll apply what we've learned about file I/O and data serialization to create a complete high-score system for a game. This system will allow players to save their scores, view the top scores, and persist this data between game sessions.
Project Overview
Our high-score system will include the following features:
- Recording player scores with names and timestamps
- Saving scores to a file using JSON serialization
- Loading and displaying the top scores
- Sorting scores by different criteria (score value, date)
- Filtering scores by difficulty level
- Backing up the high-score file
- A simple console interface for testing the system
Step 1: Define the Data Structure
First, let's define the classes we'll need to represent our high-score data:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
// Represents a single score entry
public class ScoreEntry
{
public string PlayerName { get; set; }
public int Score { get; set; }
public string Level { get; set; }
public string Difficulty { get; set; }
public DateTime Date { get; set; }
// Additional game-specific stats
public int EnemiesDefeated { get; set; }
public int TimePlayedSeconds { get; set; }
public bool CompletedLevel { get; set; }
// Display the score entry in a formatted way
public override string ToString()
{
return $"{PlayerName}: {Score} points - {Level} ({Difficulty}) - {Date:MM/dd/yyyy}";
}
// Detailed stats display
public string GetDetailedStats()
{
TimeSpan timePlayed = TimeSpan.FromSeconds(TimePlayedSeconds);
string timeString = timePlayed.Hours > 0
? $"{timePlayed.Hours}h {timePlayed.Minutes}m {timePlayed.Seconds}s"
: $"{timePlayed.Minutes}m {timePlayed.Seconds}s";
return $"Player: {PlayerName}\n" +
$"Score: {Score}\n" +
$"Level: {Level}\n" +
$"Difficulty: {Difficulty}\n" +
$"Date: {Date:MM/dd/yyyy HH:mm:ss}\n" +
$"Enemies Defeated: {EnemiesDefeated}\n" +
$"Time Played: {timeString}\n" +
$"Completed Level: {(CompletedLevel ? "Yes" : "No")}";
}
}
// Container for all high scores
public class HighScoreData
{
public List<ScoreEntry> Scores { get; set; } = new List<ScoreEntry>();
public DateTime LastUpdated { get; set; }
public string GameVersion { get; set; }
}
Step 2: Create the High-Score Manager
Now, let's create the main class that will manage our high scores:
public class HighScoreManager
{
private HighScoreData highScoreData;
private readonly string scoreFilePath;
private readonly JsonSerializerOptions jsonOptions;
// Maximum number of scores to keep
private readonly int maxScores;
public HighScoreManager(string filePath, int maxScoresToKeep = 100)
{
scoreFilePath = filePath;
maxScores = maxScoresToKeep;
// Configure JSON serialization options
jsonOptions = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
// Initialize or load existing data
LoadScores();
}
// Add a new score to the high-score list
public void AddScore(ScoreEntry newScore)
{
// Ensure the date is set
if (newScore.Date == default)
{
newScore.Date = DateTime.Now;
}
// Add the new score
highScoreData.Scores.Add(newScore);
// Update the last updated timestamp
highScoreData.LastUpdated = DateTime.Now;
// Sort scores by score value (descending)
highScoreData.Scores = highScoreData.Scores
.OrderByDescending(s => s.Score)
.ToList();
// Trim the list if it exceeds the maximum
if (highScoreData.Scores.Count > maxScores)
{
highScoreData.Scores = highScoreData.Scores
.Take(maxScores)
.ToList();
}
// Save the updated scores
SaveScores();
}
// Get all scores
public List<ScoreEntry> GetAllScores()
{
return highScoreData.Scores.ToList();
}
// Get top N scores
public List<ScoreEntry> GetTopScores(int count)
{
return highScoreData.Scores
.OrderByDescending(s => s.Score)
.Take(count)
.ToList();
}
// Get top scores for a specific difficulty
public List<ScoreEntry> GetTopScoresByDifficulty(string difficulty, int count)
{
return highScoreData.Scores
.Where(s => s.Difficulty.Equals(difficulty, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(s => s.Score)
.Take(count)
.ToList();
}
// Get top scores for a specific level
public List<ScoreEntry> GetTopScoresByLevel(string level, int count)
{
return highScoreData.Scores
.Where(s => s.Level.Equals(level, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(s => s.Score)
.Take(count)
.ToList();
}
// Get recent scores
public List<ScoreEntry> GetRecentScores(int count)
{
return highScoreData.Scores
.OrderByDescending(s => s.Date)
.Take(count)
.ToList();
}
// Get scores for a specific player
public List<ScoreEntry> GetPlayerScores(string playerName)
{
return highScoreData.Scores
.Where(s => s.PlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(s => s.Score)
.ToList();
}
// Check if a score qualifies for the top N
public bool IsHighScore(int score, int topN = 10)
{
if (highScoreData.Scores.Count < topN)
{
return true;
}
int lowestTopScore = highScoreData.Scores
.OrderByDescending(s => s.Score)
.Take(topN)
.Min(s => s.Score);
return score > lowestTopScore;
}
// Get a player's highest score
public ScoreEntry GetPlayerBestScore(string playerName)
{
return highScoreData.Scores
.Where(s => s.PlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(s => s.Score)
.FirstOrDefault();
}
// Get a player's rank based on their best score
public int GetPlayerRank(string playerName)
{
var orderedScores = highScoreData.Scores
.OrderByDescending(s => s.Score)
.Select(s => s.PlayerName)
.Distinct()
.ToList();
return orderedScores.FindIndex(name =>
name.Equals(playerName, StringComparison.OrdinalIgnoreCase)) + 1;
}
// Clear all scores
public void ClearAllScores()
{
// Create a backup before clearing
BackupScores();
highScoreData.Scores.Clear();
highScoreData.LastUpdated = DateTime.Now;
SaveScores();
}
// Load scores from file
private void LoadScores()
{
try
{
// Check if the file exists
if (File.Exists(scoreFilePath))
{
// Read the JSON data
string json = File.ReadAllText(scoreFilePath);
// Deserialize the data
highScoreData = JsonSerializer.Deserialize<HighScoreData>(json, jsonOptions);
Console.WriteLine($"Loaded {highScoreData.Scores.Count} scores from {scoreFilePath}");
}
else
{
// Create a new high-score data object
highScoreData = new HighScoreData
{
Scores = new List<ScoreEntry>(),
LastUpdated = DateTime.Now,
GameVersion = "1.0.0"
};
Console.WriteLine("No existing high-score file found. Created a new one.");
}
}
catch (Exception ex)
{
Console.WriteLine($"Error loading high scores: {ex.Message}");
// Create a new high-score data object as fallback
highScoreData = new HighScoreData
{
Scores = new List<ScoreEntry>(),
LastUpdated = DateTime.Now,
GameVersion = "1.0.0"
};
}
}
// Save scores to file
private void SaveScores()
{
try
{
// Ensure the directory exists
string directory = Path.GetDirectoryName(scoreFilePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// Serialize the data to JSON
string json = JsonSerializer.Serialize(highScoreData, jsonOptions);
// Write to the file
File.WriteAllText(scoreFilePath, json);
Console.WriteLine($"Saved {highScoreData.Scores.Count} scores to {scoreFilePath}");
}
catch (Exception ex)
{
Console.WriteLine($"Error saving high scores: {ex.Message}");
}
}
// Create a backup of the high-score file
public bool BackupScores()
{
try
{
if (!File.Exists(scoreFilePath))
{
return false;
}
string backupPath = $"{scoreFilePath}.{DateTime.Now:yyyyMMdd_HHmmss}.bak";
File.Copy(scoreFilePath, backupPath);
Console.WriteLine($"Created backup at {backupPath}");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Error creating backup: {ex.Message}");
return false;
}
}
// Get statistics about the high scores
public HighScoreStats GetStatistics()
{
if (highScoreData.Scores.Count == 0)
{
return new HighScoreStats
{
TotalScores = 0,
AverageScore = 0,
HighestScore = 0,
LowestScore = 0,
MostRecentDate = DateTime.MinValue,
OldestDate = DateTime.MinValue,
UniquePlayerCount = 0
};
}
return new HighScoreStats
{
TotalScores = highScoreData.Scores.Count,
AverageScore = (int)highScoreData.Scores.Average(s => s.Score),
HighestScore = highScoreData.Scores.Max(s => s.Score),
LowestScore = highScoreData.Scores.Min(s => s.Score),
MostRecentDate = highScoreData.Scores.Max(s => s.Date),
OldestDate = highScoreData.Scores.Min(s => s.Date),
UniquePlayerCount = highScoreData.Scores.Select(s => s.PlayerName).Distinct().Count()
};
}
}
// Class to hold statistics about the high scores
public class HighScoreStats
{
public int TotalScores { get; set; }
public int AverageScore { get; set; }
public int HighestScore { get; set; }
public int LowestScore { get; set; }
public DateTime MostRecentDate { get; set; }
public DateTime OldestDate { get; set; }
public int UniquePlayerCount { get; set; }
}
Step 3: Create a Simple Game Simulator
To test our high-score system, let's create a simple game simulator that generates random scores:
public class GameSimulator
{
private readonly Random random = new Random();
private readonly string[] playerNames = {
"SpaceAce", "NinjaWarrior", "PixelHero", "MegaGamer", "CyberKnight",
"QuestMaster", "DragonSlayer", "StarFighter", "LevelLord", "BossBuster"
};
private readonly string[] levels = {
"Forest Temple", "Lava Castle", "Ice Cavern", "Sky Fortress", "Desert Ruins"
};
private readonly string[] difficulties = {
"Easy", "Normal", "Hard", "Expert", "Legendary"
};
// Simulate a game and return a score entry
public ScoreEntry SimulateGame()
{
string playerName = playerNames[random.Next(playerNames.Length)];
string level = levels[random.Next(levels.Length)];
string difficulty = difficulties[random.Next(difficulties.Length)];
// Base score depends on difficulty
int baseScore = difficulty switch
{
"Easy" => random.Next(1000, 3000),
"Normal" => random.Next(2000, 5000),
"Hard" => random.Next(4000, 8000),
"Expert" => random.Next(7000, 12000),
"Legendary" => random.Next(10000, 20000),
_ => random.Next(1000, 5000)
};
// Random modifiers
int enemiesDefeated = random.Next(10, 100);
int timePlayedSeconds = random.Next(60, 1200); // 1-20 minutes
bool completedLevel = random.Next(100) < 70; // 70% chance of completion
// Adjust score based on performance
int finalScore = baseScore;
finalScore += enemiesDefeated * 50;
finalScore -= timePlayedSeconds / 2; // Faster completion = higher score
if (completedLevel) finalScore = (int)(finalScore * 1.5);
// Create the score entry
return new ScoreEntry
{
PlayerName = playerName,
Score = finalScore,
Level = level,
Difficulty = difficulty,
Date = DateTime.Now.AddDays(-random.Next(30)), // Random date within last 30 days
EnemiesDefeated = enemiesDefeated,
TimePlayedSeconds = timePlayedSeconds,
CompletedLevel = completedLevel
};
}
}
Step 4: Create a Console Interface
Now, let's create a simple console interface to interact with our high-score system:
public class HighScoreConsoleUI
{
private readonly HighScoreManager scoreManager;
private readonly GameSimulator gameSimulator;
public HighScoreConsoleUI(HighScoreManager manager, GameSimulator simulator)
{
scoreManager = manager;
gameSimulator = simulator;
}
// Run the main menu loop
public void Run()
{
bool exit = false;
while (!exit)
{
Console.Clear();
Console.WriteLine("===== GAME HIGH SCORE SYSTEM =====");
Console.WriteLine("1. View Top 10 Scores");
Console.WriteLine("2. View Scores by Difficulty");
Console.WriteLine("3. View Scores by Level");
Console.WriteLine("4. View Recent Scores");
Console.WriteLine("5. Search Player Scores");
Console.WriteLine("6. View High Score Statistics");
Console.WriteLine("7. Simulate a Game");
Console.WriteLine("8. Simulate Multiple Games");
Console.WriteLine("9. Backup High Scores");
Console.WriteLine("0. Exit");
Console.WriteLine("==================================");
Console.Write("Enter your choice: ");
string input = Console.ReadLine();
switch (input)
{
case "1":
ViewTopScores();
break;
case "2":
ViewScoresByDifficulty();
break;
case "3":
ViewScoresByLevel();
break;
case "4":
ViewRecentScores();
break;
case "5":
SearchPlayerScores();
break;
case "6":
ViewStatistics();
break;
case "7":
SimulateGame();
break;
case "8":
SimulateMultipleGames();
break;
case "9":
BackupScores();
break;
case "0":
exit = true;
break;
default:
Console.WriteLine("Invalid choice. Press any key to continue...");
Console.ReadKey();
break;
}
}
}
// Display the top scores
private void ViewTopScores()
{
Console.Clear();
Console.WriteLine("===== TOP 10 HIGH SCORES =====");
var scores = scoreManager.GetTopScores(10);
if (scores.Count == 0)
{
Console.WriteLine("No scores recorded yet.");
}
else
{
for (int i = 0; i < scores.Count; i++)
{
Console.WriteLine($"{i + 1}. {scores[i]}");
}
}
Console.WriteLine("\nPress any key to return to the menu...");
Console.ReadKey();
}
// Display scores by difficulty
private void ViewScoresByDifficulty()
{
Console.Clear();
Console.WriteLine("===== SCORES BY DIFFICULTY =====");
Console.WriteLine("Select difficulty:");
Console.WriteLine("1. Easy");
Console.WriteLine("2. Normal");
Console.WriteLine("3. Hard");
Console.WriteLine("4. Expert");
Console.WriteLine("5. Legendary");
Console.Write("Enter your choice: ");
string input = Console.ReadLine();
string difficulty = input switch
{
"1" => "Easy",
"2" => "Normal",
"3" => "Hard",
"4" => "Expert",
"5" => "Legendary",
_ => ""
};
if (string.IsNullOrEmpty(difficulty))
{
Console.WriteLine("Invalid choice. Press any key to continue...");
Console.ReadKey();
return;
}
Console.Clear();
Console.WriteLine($"===== TOP SCORES FOR {difficulty.ToUpper()} DIFFICULTY =====");
var scores = scoreManager.GetTopScoresByDifficulty(difficulty, 10);
if (scores.Count == 0)
{
Console.WriteLine($"No scores recorded for {difficulty} difficulty.");
}
else
{
for (int i = 0; i < scores.Count; i++)
{
Console.WriteLine($"{i + 1}. {scores[i]}");
}
}
Console.WriteLine("\nPress any key to return to the menu...");
Console.ReadKey();
}
// Display scores by level
private void ViewScoresByLevel()
{
Console.Clear();
Console.WriteLine("===== SCORES BY LEVEL =====");
Console.WriteLine("Select level:");
Console.WriteLine("1. Forest Temple");
Console.WriteLine("2. Lava Castle");
Console.WriteLine("3. Ice Cavern");
Console.WriteLine("4. Sky Fortress");
Console.WriteLine("5. Desert Ruins");
Console.Write("Enter your choice: ");
string input = Console.ReadLine();
string level = input switch
{
"1" => "Forest Temple",
"2" => "Lava Castle",
"3" => "Ice Cavern",
"4" => "Sky Fortress",
"5" => "Desert Ruins",
_ => ""
};
if (string.IsNullOrEmpty(level))
{
Console.WriteLine("Invalid choice. Press any key to continue...");
Console.ReadKey();
return;
}
Console.Clear();
Console.WriteLine($"===== TOP SCORES FOR {level.ToUpper()} =====");
var scores = scoreManager.GetTopScoresByLevel(level, 10);
if (scores.Count == 0)
{
Console.WriteLine($"No scores recorded for {level}.");
}
else
{
for (int i = 0; i < scores.Count; i++)
{
Console.WriteLine($"{i + 1}. {scores[i]}");
}
}
Console.WriteLine("\nPress any key to return to the menu...");
Console.ReadKey();
}
// Display recent scores
private void ViewRecentScores()
{
Console.Clear();
Console.WriteLine("===== RECENT SCORES =====");
var scores = scoreManager.GetRecentScores(10);
if (scores.Count == 0)
{
Console.WriteLine("No scores recorded yet.");
}
else
{
for (int i = 0; i < scores.Count; i++)
{
Console.WriteLine($"{i + 1}. {scores[i]}");
}
}
Console.WriteLine("\nPress any key to return to the menu...");
Console.ReadKey();
}
// Search for a player's scores
private void SearchPlayerScores()
{
Console.Clear();
Console.WriteLine("===== SEARCH PLAYER SCORES =====");
Console.Write("Enter player name: ");
string playerName = Console.ReadLine();
if (string.IsNullOrWhiteSpace(playerName))
{
Console.WriteLine("Invalid player name. Press any key to continue...");
Console.ReadKey();
return;
}
Console.Clear();
Console.WriteLine($"===== SCORES FOR {playerName.ToUpper()} =====");
var scores = scoreManager.GetPlayerScores(playerName);
if (scores.Count == 0)
{
Console.WriteLine($"No scores found for player {playerName}.");
}
else
{
int rank = scoreManager.GetPlayerRank(playerName);
Console.WriteLine($"Player Rank: {rank}");
Console.WriteLine($"Total Scores: {scores.Count}");
Console.WriteLine($"Best Score: {scores[0].Score}");
Console.WriteLine("\nAll Scores:");
for (int i = 0; i < scores.Count; i++)
{
Console.WriteLine($"{i + 1}. {scores[i]}");
}
// Show detailed stats for the best score
Console.WriteLine("\nBest Score Details:");
Console.WriteLine(scores[0].GetDetailedStats());
}
Console.WriteLine("\nPress any key to return to the menu...");
Console.ReadKey();
}
// Display high score statistics
private void ViewStatistics()
{
Console.Clear();
Console.WriteLine("===== HIGH SCORE STATISTICS =====");
var stats = scoreManager.GetStatistics();
if (stats.TotalScores == 0)
{
Console.WriteLine("No scores recorded yet.");
}
else
{
Console.WriteLine($"Total Scores: {stats.TotalScores}");
Console.WriteLine($"Unique Players: {stats.UniquePlayerCount}");
Console.WriteLine($"Highest Score: {stats.HighestScore}");
Console.WriteLine($"Lowest Score: {stats.LowestScore}");
Console.WriteLine($"Average Score: {stats.AverageScore}");
Console.WriteLine($"Oldest Score: {stats.OldestDate:MM/dd/yyyy}");
Console.WriteLine($"Most Recent Score: {stats.MostRecentDate:MM/dd/yyyy}");
}
Console.WriteLine("\nPress any key to return to the menu...");
Console.ReadKey();
}
// Simulate a single game
private void SimulateGame()
{
Console.Clear();
Console.WriteLine("===== SIMULATING GAME =====");
ScoreEntry score = gameSimulator.SimulateGame();
Console.WriteLine("Game completed! Results:");
Console.WriteLine(score.GetDetailedStats());
bool isHighScore = scoreManager.IsHighScore(score.Score);
if (isHighScore)
{
Console.WriteLine("\nCongratulations! This is a high score!");
}
scoreManager.AddScore(score);
Console.WriteLine("\nPress any key to return to the menu...");
Console.ReadKey();
}
// Simulate multiple games
private void SimulateMultipleGames()
{
Console.Clear();
Console.WriteLine("===== SIMULATE MULTIPLE GAMES =====");
Console.Write("How many games to simulate? ");
if (!int.TryParse(Console.ReadLine(), out int count) || count <= 0)
{
Console.WriteLine("Invalid number. Press any key to continue...");
Console.ReadKey();
return;
}
Console.WriteLine($"Simulating {count} games...");
for (int i = 0; i < count; i++)
{
ScoreEntry score = gameSimulator.SimulateGame();
scoreManager.AddScore(score);
// Show progress
if (i % 10 == 0 || i == count - 1)
{
Console.WriteLine($"Simulated {i + 1} of {count} games...");
}
}
Console.WriteLine($"\nCompleted simulation of {count} games!");
Console.WriteLine("Press any key to return to the menu...");
Console.ReadKey();
}
// Backup the high scores
private void BackupScores()
{
Console.Clear();
Console.WriteLine("===== BACKUP HIGH SCORES =====");
bool success = scoreManager.BackupScores();
if (success)
{
Console.WriteLine("High scores backed up successfully!");
}
else
{
Console.WriteLine("Failed to backup high scores or no scores to backup.");
}
Console.WriteLine("\nPress any key to return to the menu...");
Console.ReadKey();
}
}
Step 5: Put It All Together
Finally, let's create the main program that ties everything together:
public class Program
{
public static void Main()
{
// Set up the high-score file path
string documentsFolder = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string gameSavesFolder = Path.Combine(documentsFolder, "MyGame", "Saves");
string highScoreFile = Path.Combine(gameSavesFolder, "highscores.json");
// Create the high-score manager
HighScoreManager scoreManager = new HighScoreManager(highScoreFile);
// Create the game simulator
GameSimulator gameSimulator = new GameSimulator();
// Create and run the console UI
HighScoreConsoleUI ui = new HighScoreConsoleUI(scoreManager, gameSimulator);
ui.Run();
}
}
Running the Project
When you run this project, you'll see a console interface that allows you to:
- View the top 10 scores
- Filter scores by difficulty
- Filter scores by level
- View recent scores
- Search for a specific player's scores
- View statistics about all recorded scores
- Simulate a game to generate a new score
- Simulate multiple games at once
- Create backups of the high-score file
The high scores are saved to a JSON file in your Documents folder, so they persist between runs of the program.
Project Extensions
Here are some ways you could extend this project:
- Add encryption to the high-score file to prevent cheating
- Implement a web leaderboard that uploads scores to a server
- Add achievements based on player performance
- Create a more sophisticated game simulation with realistic scoring
- Add a replay system that records and can play back game actions
- Implement a score verification system to detect and prevent cheating
- Create a graphical interface using Windows Forms or WPF
Conclusion
In this mini-project, we've applied our knowledge of file I/O and data serialization to create a complete high-score system for a game. We've learned how to:
- Design a data structure for game scores
- Serialize and deserialize data using JSON
- Save and load data from files
- Create a backup system for important data
- Filter and sort data based on different criteria
- Build a simple user interface for interacting with the system
These skills are directly applicable to many aspects of game development, from save systems to configuration management to data exchange between different parts of your game.
In the next module, we'll explore algorithms and problem-solving techniques that are essential for game development.