tetris-io 0.1.1

Terminal-based Tetris game built with Ratatui and Crossterm
Documentation
use std::time::{Duration, Instant};

use crate::domain::state::GameState;

use super::flow::GameMode;

#[derive(Debug)]
pub struct GameSessionState {
    pub mode: GameMode,
    pub state: GameState,
    pub start_time: Instant,
    pub last_frame: Instant,
    pub paused_at: Option<Instant>,
    pub paused_total: Duration,
    pub game_over_time: Option<u64>,
    pub time_up: bool,
}

#[derive(Debug)]
pub struct NetSessionState {
    pub state: GameState,
    pub start_time: Instant,
    pub last_frame: Instant,
    pub remote_over: bool,
    pub remote_disconnect: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionEnd {
    TimeUp,
    GoalReached,
}

impl GameSessionState {
    pub fn new(seed: u64, mode: GameMode) -> Self {
        let now = Instant::now();
        let mut state = GameState::new(seed);
        if let GameMode::Cheese { target } = mode {
            state.seed_cheese(target, seed);
        }
        Self {
            mode,
            state,
            start_time: now,
            last_frame: now,
            paused_at: None,
            paused_total: Duration::from_secs(0),
            game_over_time: None,
            time_up: false,
        }
    }

    pub fn elapsed_secs(&self, now: Instant) -> u64 {
        self.game_over_time.unwrap_or_else(|| {
            let effective_now = self.paused_at.unwrap_or(now);
            let elapsed = effective_now
                .duration_since(self.start_time)
                .saturating_sub(self.paused_total);
            elapsed.as_secs()
        })
    }

    pub fn remaining_secs(&self, now: Instant) -> Option<u64> {
        let limit = self.mode.time_limit_secs()?;
        let elapsed = self.elapsed_secs(now);
        Some(limit.saturating_sub(elapsed))
    }

    pub fn pause(&mut self, now: Instant) {
        if self.paused_at.is_none() {
            self.paused_at = Some(now);
        }
    }

    pub fn resume(&mut self, now: Instant) {
        if let Some(paused_at) = self.paused_at.take() {
            self.paused_total = self
                .paused_total
                .saturating_add(now.duration_since(paused_at));
            self.last_frame = now;
        }
    }

    pub fn apply_mode_end_if_reached(&mut self, now: Instant) -> Option<SessionEnd> {
        if let Some(limit) = self.mode.time_limit_secs()
            && self.elapsed_secs(now) >= limit
        {
            self.game_over_time = Some(limit);
            self.time_up = true;
            self.state.game_over = true;
            return Some(SessionEnd::TimeUp);
        }

        if let GameMode::FortyLines { target } = self.mode
            && self.state.lines >= target
        {
            self.game_over_time = Some(self.elapsed_secs(now));
            self.time_up = true;
            self.state.game_over = true;
            return Some(SessionEnd::GoalReached);
        }

        if let GameMode::Cheese { .. } = self.mode
            && self.state.garbage_remaining == 0
        {
            self.game_over_time = Some(self.elapsed_secs(now));
            self.state.game_over = true;
            return Some(SessionEnd::GoalReached);
        }

        None
    }
}

impl NetSessionState {
    pub fn new(seed: u64) -> Self {
        let now = Instant::now();
        Self {
            state: GameState::new(seed),
            start_time: now,
            last_frame: now,
            remote_over: false,
            remote_disconnect: false,
        }
    }

    pub fn elapsed_secs(&self, now: Instant) -> u64 {
        now.duration_since(self.start_time).as_secs()
    }
}