323 lines
10 KiB
Rust
323 lines
10 KiB
Rust
use std::{collections::{HashMap, HashSet}, error::Error};
|
|
use csv::{Reader, Writer};
|
|
use crate::data::{Player, PlayerScore, TeamScore};
|
|
|
|
/// Load players from a CSV file
|
|
/// Expected format: username,team
|
|
pub fn load_players(file_path: &str) -> Result<Vec<Player>, Box<dyn Error>> {
|
|
let mut reader = Reader::from_path(file_path)?;
|
|
let mut players = Vec::new();
|
|
|
|
for result in reader.deserialize() {
|
|
let player: Player = result?;
|
|
players.push(player);
|
|
}
|
|
|
|
Ok(players)
|
|
}
|
|
|
|
/// Write player scores to a CSV file
|
|
pub fn write_player_scores(
|
|
file_path: &str,
|
|
player_scores: &[PlayerScore],
|
|
) -> Result<(), Box<dyn Error>> {
|
|
let mut writer = Writer::from_path(file_path)?;
|
|
|
|
// Write header
|
|
writer.write_record(&["username", "team", "total_score"])?;
|
|
|
|
// Write data
|
|
for score in player_scores {
|
|
writer.write_record(&[
|
|
&score.player.username,
|
|
&score.player.team,
|
|
&score.total_score.to_string(),
|
|
])?;
|
|
}
|
|
|
|
writer.flush()?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Write team scores to a CSV file
|
|
pub fn write_team_scores(
|
|
file_path: &str,
|
|
team_scores: &[TeamScore],
|
|
) -> Result<(), Box<dyn Error>> {
|
|
let mut writer = Writer::from_path(file_path)?;
|
|
let mut territories: HashSet<String> = HashSet::new();
|
|
for score in team_scores.iter() {
|
|
for (territory, _) in score.territory_scores.clone() {
|
|
territories.insert(territory);
|
|
}
|
|
}
|
|
let territories: Vec<String> = territories.into_iter().collect();
|
|
|
|
// Write header
|
|
let mut header = vec![String::from("team")];
|
|
header.append(&mut territories.clone());
|
|
writer.write_record(&header)?;
|
|
|
|
// Write data
|
|
for score in team_scores {
|
|
let mut scores = vec![score.team.clone()];
|
|
let default = 0;
|
|
for territory in territories.clone() {
|
|
scores.push(score.territory_scores.get(&territory).unwrap_or(&default).to_string());
|
|
}
|
|
writer.write_record(scores)?;
|
|
}
|
|
|
|
writer.flush()?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Write detailed territory scores to a CSV file
|
|
pub fn write_territory_scores(
|
|
file_path: &str,
|
|
player_scores: &[PlayerScore],
|
|
) -> Result<(), Box<dyn Error>> {
|
|
let mut writer = Writer::from_path(file_path)?;
|
|
|
|
// Collect all unique territories
|
|
let mut territories: Vec<String> = player_scores
|
|
.iter()
|
|
.flat_map(|ps| ps.territory_scores.keys())
|
|
.cloned()
|
|
.collect::<std::collections::HashSet<_>>()
|
|
.into_iter()
|
|
.collect();
|
|
territories.sort();
|
|
|
|
// Write header
|
|
let mut header = vec!["username".to_string(), "team".to_string()];
|
|
header.extend(territories.iter().cloned());
|
|
header.push("total".to_string());
|
|
writer.write_record(&header)?;
|
|
|
|
// Write data
|
|
for score in player_scores {
|
|
let mut row = vec![score.player.username.clone(), score.player.team.clone()];
|
|
|
|
for territory in &territories {
|
|
let territory_score = score.territory_scores.get(territory).unwrap_or(&0);
|
|
row.push(territory_score.to_string());
|
|
}
|
|
|
|
row.push(score.total_score.to_string());
|
|
writer.write_record(&row)?;
|
|
}
|
|
|
|
writer.flush()?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Calculate team scores from player scores
|
|
pub fn calculate_team_scores(player_scores: &[PlayerScore]) -> Vec<TeamScore> {
|
|
let mut team_map: HashMap<String, TeamScore> = HashMap::new();
|
|
|
|
for player_score in player_scores {
|
|
let team_score = team_map
|
|
.entry(player_score.player.team.clone())
|
|
.or_insert_with(|| TeamScore::new(player_score.player.team.clone()));
|
|
|
|
for (territory, score) in &player_score.territory_scores {
|
|
team_score.add_territory_score(territory.clone(), *score);
|
|
}
|
|
}
|
|
|
|
let mut teams: Vec<TeamScore> = team_map.into_values().collect();
|
|
teams.sort_by(|a, b| b.total_score.cmp(&a.total_score));
|
|
teams
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::fs;
|
|
use std::io::Write;
|
|
use tempfile::NamedTempFile;
|
|
|
|
#[test]
|
|
fn test_load_players() {
|
|
let mut temp_file = NamedTempFile::new().unwrap();
|
|
writeln!(temp_file, "username,team").unwrap();
|
|
writeln!(temp_file, "player1,Red Team").unwrap();
|
|
writeln!(temp_file, "player2,Blue Team").unwrap();
|
|
writeln!(temp_file, "player3,Red Team").unwrap();
|
|
temp_file.flush().unwrap();
|
|
|
|
let players = load_players(temp_file.path().to_str().unwrap()).unwrap();
|
|
|
|
assert_eq!(players.len(), 3);
|
|
assert_eq!(players[0].username, "player1");
|
|
assert_eq!(players[0].team, "Red Team");
|
|
assert_eq!(players[1].username, "player2");
|
|
assert_eq!(players[1].team, "Blue Team");
|
|
}
|
|
|
|
#[test]
|
|
fn test_write_player_scores() {
|
|
let temp_file = NamedTempFile::new().unwrap();
|
|
let file_path = temp_file.path().to_str().unwrap();
|
|
|
|
let player1 = Player {
|
|
username: "player1".to_string(),
|
|
team: "Red Team".to_string(),
|
|
};
|
|
let mut score1 = PlayerScore::new(player1);
|
|
score1.add_territory_score("Capitol Hill".to_string(), 10);
|
|
|
|
let player2 = Player {
|
|
username: "player2".to_string(),
|
|
team: "Blue Team".to_string(),
|
|
};
|
|
let mut score2 = PlayerScore::new(player2);
|
|
score2.add_territory_score("Downtown".to_string(), 5);
|
|
|
|
let player_scores = vec![score1, score2];
|
|
|
|
write_player_scores(file_path, &player_scores).unwrap();
|
|
|
|
let content = fs::read_to_string(file_path).unwrap();
|
|
assert!(content.contains("username,team,total_score"));
|
|
assert!(content.contains("player1,Red Team,10"));
|
|
assert!(content.contains("player2,Blue Team,5"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_write_team_scores() {
|
|
let temp_file = NamedTempFile::new().unwrap();
|
|
let file_path = temp_file.path().to_str().unwrap();
|
|
|
|
let mut team1 = TeamScore::new("Red Team".to_string());
|
|
team1.add_territory_score("Capitol Hill".to_string(), 20);
|
|
|
|
let mut team2 = TeamScore::new("Blue Team".to_string());
|
|
team2.add_territory_score("Downtown".to_string(), 15);
|
|
|
|
let team_scores = vec![team1, team2];
|
|
|
|
write_team_scores(file_path, &team_scores).unwrap();
|
|
|
|
let content = fs::read_to_string(file_path).unwrap();
|
|
assert!(content.contains("team,Capitol Hill,Downtown"));
|
|
assert!(content.contains("Red Team,20,0"));
|
|
assert!(content.contains("Blue Team,0,15"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_calculate_team_scores() {
|
|
let player1 = Player {
|
|
username: "player1".to_string(),
|
|
team: "Red Team".to_string(),
|
|
};
|
|
let mut score1 = PlayerScore::new(player1);
|
|
score1.add_territory_score("Capitol Hill".to_string(), 10);
|
|
score1.add_territory_score("Downtown".to_string(), 5);
|
|
|
|
let player2 = Player {
|
|
username: "player2".to_string(),
|
|
team: "Red Team".to_string(),
|
|
};
|
|
let mut score2 = PlayerScore::new(player2);
|
|
score2.add_territory_score("Capitol Hill".to_string(), 8);
|
|
|
|
let player3 = Player {
|
|
username: "player3".to_string(),
|
|
team: "Blue Team".to_string(),
|
|
};
|
|
let mut score3 = PlayerScore::new(player3);
|
|
score3.add_territory_score("Fremont".to_string(), 12);
|
|
|
|
let player_scores = vec![score1, score2, score3];
|
|
let team_scores = calculate_team_scores(&player_scores);
|
|
|
|
assert_eq!(team_scores.len(), 2);
|
|
|
|
// Red Team should be first (higher score)
|
|
assert_eq!(team_scores[0].team, "Red Team");
|
|
assert_eq!(team_scores[0].total_score, 23); // 10 + 5 + 8
|
|
assert_eq!(team_scores[0].territory_scores.get("Capitol Hill"), Some(&18));
|
|
assert_eq!(team_scores[0].territory_scores.get("Downtown"), Some(&5));
|
|
|
|
// Blue Team should be second
|
|
assert_eq!(team_scores[1].team, "Blue Team");
|
|
assert_eq!(team_scores[1].total_score, 12);
|
|
assert_eq!(team_scores[1].territory_scores.get("Fremont"), Some(&12));
|
|
}
|
|
|
|
#[test]
|
|
fn test_write_territory_scores() {
|
|
let temp_file = NamedTempFile::new().unwrap();
|
|
let file_path = temp_file.path().to_str().unwrap();
|
|
|
|
let player1 = Player {
|
|
username: "player1".to_string(),
|
|
team: "Red Team".to_string(),
|
|
};
|
|
let mut score1 = PlayerScore::new(player1);
|
|
score1.add_territory_score("Capitol Hill".to_string(), 10);
|
|
score1.add_territory_score("Downtown".to_string(), 5);
|
|
|
|
let player2 = Player {
|
|
username: "player2".to_string(),
|
|
team: "Blue Team".to_string(),
|
|
};
|
|
let mut score2 = PlayerScore::new(player2);
|
|
score2.add_territory_score("Capitol Hill".to_string(), 8);
|
|
|
|
let player_scores = vec![score1, score2];
|
|
|
|
write_territory_scores(file_path, &player_scores).unwrap();
|
|
|
|
let content = fs::read_to_string(file_path).unwrap();
|
|
|
|
// Check header contains territory names
|
|
assert!(content.contains("username"));
|
|
assert!(content.contains("team"));
|
|
assert!(content.contains("Capitol Hill"));
|
|
assert!(content.contains("Downtown"));
|
|
assert!(content.contains("total"));
|
|
|
|
// Check player data
|
|
assert!(content.contains("player1"));
|
|
assert!(content.contains("player2"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_calculate_team_scores_sorting() {
|
|
let player1 = Player {
|
|
username: "player1".to_string(),
|
|
team: "Team A".to_string(),
|
|
};
|
|
let mut score1 = PlayerScore::new(player1);
|
|
score1.add_territory_score("Area1".to_string(), 5);
|
|
|
|
let player2 = Player {
|
|
username: "player2".to_string(),
|
|
team: "Team B".to_string(),
|
|
};
|
|
let mut score2 = PlayerScore::new(player2);
|
|
score2.add_territory_score("Area2".to_string(), 20);
|
|
|
|
let player3 = Player {
|
|
username: "player3".to_string(),
|
|
team: "Team C".to_string(),
|
|
};
|
|
let mut score3 = PlayerScore::new(player3);
|
|
score3.add_territory_score("Area3".to_string(), 15);
|
|
|
|
let player_scores = vec![score1, score2, score3];
|
|
let team_scores = calculate_team_scores(&player_scores);
|
|
|
|
// Should be sorted by total_score descending
|
|
assert_eq!(team_scores[0].team, "Team B");
|
|
assert_eq!(team_scores[0].total_score, 20);
|
|
assert_eq!(team_scores[1].team, "Team C");
|
|
assert_eq!(team_scores[1].total_score, 15);
|
|
assert_eq!(team_scores[2].team, "Team A");
|
|
assert_eq!(team_scores[2].total_score, 5);
|
|
}
|
|
}
|