use serde::{Deserialize, Serialize};
#[cfg(not(target_arch = "wasm32"))]
use std::time::{SystemTime, UNIX_EPOCH};
use uuid::Uuid;
use crate::error::GfError;
use crate::game::GameEvent;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TurnRecord {
pub player: usize,
pub events: Vec<GameEvent>,
pub books_after_turn: Vec<usize>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GameRecord {
pub id: String,
pub variant: String,
pub timestamp: String,
pub players: Vec<String>,
pub turns: Vec<TurnRecord>,
pub winner: Option<usize>,
}
impl GameRecord {
#[must_use]
pub fn new(variant: impl Into<String>, players: Vec<String>) -> Self {
#[cfg(not(target_arch = "wasm32"))]
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
.to_string();
#[cfg(target_arch = "wasm32")]
let ts = "0".to_string();
Self {
id: Uuid::new_v4().to_string(),
variant: variant.into(),
timestamp: ts,
players,
turns: Vec::new(),
winner: None,
}
}
pub fn to_yaml(&self) -> Result<String, GfError> {
serde_norway::to_string(self).map_err(GfError::from)
}
pub fn from_yaml(s: &str) -> Result<Self, GfError> {
serde_norway::from_str(s).map_err(GfError::from)
}
pub fn to_json(&self) -> Result<String, GfError> {
serde_json::to_string(self).map_err(GfError::from)
}
pub fn from_json(s: &str) -> Result<Self, GfError> {
serde_json::from_str(s).map_err(GfError::from)
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct GameCollection(Vec<GameRecord>);
impl GameCollection {
#[must_use]
pub fn new() -> Self {
Self(Vec::new())
}
pub fn push(&mut self, record: GameRecord) {
self.0.push(record);
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &GameRecord> {
self.0.iter()
}
pub fn to_yaml(&self) -> Result<String, GfError> {
serde_norway::to_string(self).map_err(GfError::from)
}
pub fn from_yaml(s: &str) -> Result<Self, GfError> {
serde_norway::from_str(s).map_err(GfError::from)
}
pub fn to_json(&self) -> Result<String, GfError> {
serde_json::to_string(self).map_err(GfError::from)
}
pub fn from_json(s: &str) -> Result<Self, GfError> {
serde_json::from_str(s).map_err(GfError::from)
}
}
impl std::ops::Index<usize> for GameCollection {
type Output = GameRecord;
fn index(&self, idx: usize) -> &Self::Output {
&self.0[idx]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::game::GameEvent;
fn make_record() -> GameRecord {
GameRecord::new("Standard", vec!["Alice".to_string(), "Bob".to_string()])
}
#[test]
fn test_game_record_new_has_uuid() {
let r = make_record();
assert_eq!(r.id.len(), 36);
}
#[test]
fn test_game_record_new_has_timestamp() {
let r = make_record();
let ts: u64 = r.timestamp.parse().unwrap();
assert!(ts > 1_600_000_000);
}
#[test]
fn test_game_record_new_players_and_variant() {
let r = make_record();
assert_eq!(r.variant, "Standard");
assert_eq!(r.players, ["Alice", "Bob"]);
assert!(r.turns.is_empty());
assert!(r.winner.is_none());
}
#[test]
fn test_game_record_yaml_round_trip() {
let r = make_record();
let yaml = r.to_yaml().unwrap();
let back = GameRecord::from_yaml(&yaml).unwrap();
assert_eq!(r, back);
}
#[test]
fn test_game_record_json_round_trip() {
let r = make_record();
let json = r.to_json().unwrap();
let back = GameRecord::from_json(&json).unwrap();
assert_eq!(r, back);
}
#[test]
fn test_game_record_with_turns_round_trip() {
let mut r = make_record();
let turn = TurnRecord {
player: 0,
events: vec![
GameEvent::Asked {
asker: 0,
target: 1,
rank: "A".to_string(),
},
GameEvent::GoFish { player: 0 },
GameEvent::Drew {
player: 0,
matched: false,
},
],
books_after_turn: vec![0, 0],
};
r.turns.push(turn);
r.winner = Some(0);
let yaml = r.to_yaml().unwrap();
let back = GameRecord::from_yaml(&yaml).unwrap();
assert_eq!(r, back);
let json = r.to_json().unwrap();
let back_json = GameRecord::from_json(&json).unwrap();
assert_eq!(r, back_json);
}
#[test]
fn test_game_record_from_yaml_bad_input_returns_error() {
let result = GameRecord::from_yaml("not: valid: yaml: [[[");
assert!(result.is_err());
}
#[test]
fn test_game_record_from_json_bad_input_returns_error() {
let result = GameRecord::from_json("{not json}");
assert!(result.is_err());
}
#[test]
fn test_game_collection_new_is_empty() {
let col = GameCollection::new();
assert!(col.is_empty());
assert_eq!(col.len(), 0);
}
#[test]
fn test_game_collection_push_and_len() {
let mut col = GameCollection::new();
col.push(make_record());
assert_eq!(col.len(), 1);
assert!(!col.is_empty());
col.push(make_record());
assert_eq!(col.len(), 2);
}
#[test]
fn test_game_collection_yaml_round_trip() {
let mut col = GameCollection::new();
col.push(make_record());
col.push(make_record());
let yaml = col.to_yaml().unwrap();
let back = GameCollection::from_yaml(&yaml).unwrap();
assert_eq!(col, back);
}
#[test]
fn test_game_collection_json_round_trip() {
let mut col = GameCollection::new();
col.push(make_record());
let json = col.to_json().unwrap();
let back = GameCollection::from_json(&json).unwrap();
assert_eq!(col, back);
}
#[test]
fn test_game_collection_empty_round_trip() {
let col = GameCollection::new();
let yaml = col.to_yaml().unwrap();
let back = GameCollection::from_yaml(&yaml).unwrap();
assert_eq!(col, back);
}
}