deadwood 0.1.8

Gin Rummy TUI featuring full knock, gin, and undercut rules with an algorithmic bot opponent.
use std::{
    env, fs,
    path::{Path, PathBuf},
};

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

use crate::game::Scoreboard;

#[derive(Debug, Clone)]
pub struct Paths {
    config_file: PathBuf,
    session_file: PathBuf,
}

impl Paths {
    pub fn new() -> Result<Self> {
        let root = resolve_app_root()?;
        let config_file = root.join("config.toml");
        let session_file = root.join("session.json");
        Ok(Self {
            config_file,
            session_file,
        })
    }

    pub fn config_file(&self) -> &Path {
        &self.config_file
    }

    pub fn session_file(&self) -> &Path {
        &self.session_file
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SessionData {
    pub scoreboard: Scoreboard,
    #[serde(default)]
    pub round_history: Vec<RoundSummary>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RoundSummary {
    pub round_number: u32,
    pub description: String,
}

impl SessionData {
    pub fn new(scoreboard: Scoreboard, round_history: Vec<RoundSummary>) -> Self {
        Self {
            scoreboard,
            round_history,
        }
    }
}

pub fn load_session(path: &Path) -> Result<Option<SessionData>> {
    if !path.exists() {
        return Ok(None);
    }
    let contents = fs::read_to_string(path)
        .with_context(|| format!("failed to read session data at {}", path.display()))?;
    let session = serde_json::from_str::<SessionData>(&contents)
        .with_context(|| format!("failed to parse session data at {}", path.display()))?;
    Ok(Some(session))
}

pub fn save_session(path: &Path, data: &SessionData) -> Result<()> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).with_context(|| {
            format!("failed to ensure session directory at {}", parent.display())
        })?;
    }
    let contents =
        serde_json::to_string_pretty(data).context("failed to serialise session data")?;
    fs::write(path, contents)
        .with_context(|| format!("failed to write session data to {}", path.display()))
}

fn resolve_app_root() -> Result<PathBuf> {
    if let Some(mut dir) = dirs::config_dir() {
        dir.push("deadwood");
        fs::create_dir_all(&dir)
            .with_context(|| format!("failed to create config directory at {}", dir.display()))?;
        return Ok(dir);
    }

    let mut dir = env::current_dir().context("failed to resolve current directory")?;
    dir.push(".deadwood");
    fs::create_dir_all(&dir)
        .with_context(|| format!("failed to create fallback directory at {}", dir.display()))?;
    Ok(dir)
}