Files
mapbattle/src/csv_io.rs

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);
}
}