liquidwar7core 0.2.0

Liquidwar7 core logic library, low-level things which are game-engine agnostic.
Documentation
// Copyright (C) 2025 Christian Mauduit <ufoot@ufoot.org>

//! Army management for the game.
//!
//! This module contains the [`Armies`] struct which manages all fighters
//! across all teams in the game.

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

use crate::Fighter;

/// Manages all fighters across all teams in the game.
///
/// The `Armies` struct provides a centralized collection of all fighters,
/// indexed by their unique u64 ID. It supports adding, removing, and querying
/// fighters regardless of which team they belong to.
///
/// # Example
///
/// ```
/// use liquidwar7core::{Armies, Fighter};
///
/// let mut armies = Armies::new();
/// let team_id = 0x1234567890abcdef_u64;
/// // Fighter::new(team_id, x, y, z, gradient_index, health)
/// let fighter = Fighter::new(team_id, 5.0, 5.0, 0.5, 0, 1.0);
/// let fighter_id = armies.add_fighter(fighter);
///
/// assert_eq!(armies.fighter_count(), 1);
/// assert!(armies.get_fighter(&fighter_id).is_some());
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Armies {
    fighters: HashMap<u64, Fighter>,
}

impl Armies {
    /// Creates a new empty `Armies` collection.
    pub fn new() -> Self {
        Self {
            fighters: HashMap::new(),
        }
    }

    /// Generates a random u64 ID that doesn't collide with existing fighters.
    ///
    /// Uses random generation with collision retry for safety.
    fn generate_unique_id(&self) -> u64 {
        loop {
            let id = rand::random::<u64>();
            if !self.fighters.contains_key(&id) {
                return id;
            }
            // Collision detected, retry (astronomically unlikely)
        }
    }

    /// Adds a fighter to the collection and returns its unique ID.
    ///
    /// A new random u64 ID is generated, with collision checking.
    pub fn add_fighter(&mut self, fighter: Fighter) -> u64 {
        let id = self.generate_unique_id();
        self.fighters.insert(id, fighter);
        id
    }

    /// Inserts or updates a fighter with a specific ID.
    ///
    /// Used for network sync where fighter IDs must match across server and clients.
    pub fn set_fighter(&mut self, id: u64, fighter: Fighter) {
        self.fighters.insert(id, fighter);
    }

    /// Removes a fighter from the collection by its ID.
    ///
    /// Returns the removed fighter if it existed, or `None` if not found.
    pub fn remove_fighter(&mut self, id: &u64) -> Option<Fighter> {
        self.fighters.remove(id)
    }

    /// Returns a reference to the fighter with the given ID.
    ///
    /// Returns `None` if no fighter with that ID exists.
    pub fn get_fighter(&self, id: &u64) -> Option<&Fighter> {
        self.fighters.get(id)
    }

    /// Returns a mutable reference to the fighter with the given ID.
    ///
    /// Returns `None` if no fighter with that ID exists.
    pub fn get_fighter_mut(&mut self, id: &u64) -> Option<&mut Fighter> {
        self.fighters.get_mut(id)
    }

    /// Returns the total number of fighters in the collection.
    pub fn fighter_count(&self) -> usize {
        self.fighters.len()
    }

    /// Returns an iterator over all fighters and their IDs, sorted by ID.
    ///
    /// The sorting ensures deterministic iteration order across different
    /// program instances processing similar data.
    ///
    /// # Arguments
    /// * `odd_even` - If true, reverses the sort order to avoid systematic bias
    pub fn iter(&self, odd_even: bool) -> impl Iterator<Item = (&u64, &Fighter)> {
        let mut entries: Vec<_> = self.fighters.iter().collect();
        if odd_even {
            entries.sort_by(|(a, _), (b, _)| b.cmp(a));
        } else {
            entries.sort_by_key(|(id, _)| *id);
        }
        entries.into_iter()
    }

    /// Returns a mutable iterator over all fighters and their IDs, sorted by ID.
    ///
    /// The sorting ensures deterministic iteration order across different
    /// program instances processing similar data.
    ///
    /// # Arguments
    /// * `odd_even` - If true, reverses the sort order to avoid systematic bias
    pub fn iter_mut(&mut self, odd_even: bool) -> impl Iterator<Item = (&u64, &mut Fighter)> {
        let mut entries: Vec<_> = self.fighters.iter_mut().collect();
        if odd_even {
            entries.sort_by(|(a, _), (b, _)| b.cmp(a));
        } else {
            entries.sort_by_key(|(id, _)| *id);
        }
        entries.into_iter()
    }

    /// Returns an unsorted iterator over all fighters (values only).
    ///
    /// Use this when order doesn't matter and you only need fighter data.
    /// This is faster than `iter()` which sorts by ID.
    pub fn unsorted_iter(&self) -> impl Iterator<Item = &Fighter> {
        self.fighters.values()
    }

    /// Returns an unsorted iterator over all fighters with their IDs.
    ///
    /// Use this when order doesn't matter but you need both IDs and fighter data.
    /// This is faster than `iter()` which sorts by ID.
    pub fn unsorted_iter_with_ids(&self) -> impl Iterator<Item = (&u64, &Fighter)> {
        self.fighters.iter()
    }
}

impl Default for Armies {
    fn default() -> Self {
        Self::new()
    }
}

impl std::fmt::Display for Armies {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let alive = self.fighters.values().filter(|f| f.is_alive()).count();
        write!(
            f,
            "Armies ({} fighters, {} alive)",
            self.fighters.len(),
            alive
        )
    }
}