use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use smallvec::SmallVec;
use crate::arena::GameState;
use crate::arena::action::Action;
use crate::arena::historian::{Historian, HistorianError, HistorianLock};
const INLINE: usize = 16;
#[derive(Clone)]
pub struct HandLog {
prefix: Arc<[Action]>,
tail: Arc<Mutex<SmallVec<[Action; INLINE]>>>,
}
impl HandLog {
pub fn new() -> Self {
Self {
prefix: Arc::from([] as [Action; 0]),
tail: Arc::new(Mutex::new(SmallVec::new())),
}
}
#[cfg(test)]
pub fn record(&self, action: Action) {
self.tail
.lock()
.expect("HandLog tail poisoned")
.push(action);
}
pub fn to_actions(&self) -> Vec<Action> {
let tail = self.tail.lock().expect("HandLog tail poisoned");
let mut out = Vec::with_capacity(self.prefix.len() + tail.len());
out.extend_from_slice(&self.prefix);
out.extend(tail.iter().cloned());
out
}
pub fn freeze(&self) -> HandLog {
HandLog {
prefix: Arc::from(self.to_actions()),
tail: Arc::new(Mutex::new(SmallVec::new())),
}
}
pub fn spawn_child(&self) -> HandLog {
let seed = self.tail.lock().expect("HandLog tail poisoned").clone();
HandLog {
prefix: self.prefix.clone(),
tail: Arc::new(Mutex::new(seed)),
}
}
}
impl Default for HandLog {
fn default() -> Self {
Self::new()
}
}
pub struct HandLogHistorian {
log: HandLog,
}
impl HandLogHistorian {
pub fn new(log: HandLog) -> Self {
Self { log }
}
}
#[async_trait]
impl Historian for HandLogHistorian {
async fn record_action(
&mut self,
_id: u128,
_game_state: &GameState,
action: &Action,
) -> Result<(), HistorianError> {
let mut tail = self
.log
.tail
.lock()
.map_err(|_| HistorianError::LockPoisoned {
lock: HistorianLock::HandLog,
})?;
tail.push(action.clone());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::arena::game_state::Round;
use crate::core::Card;
fn ra(r: Round) -> Action {
Action::RoundAdvance(r)
}
fn deal(n: u8) -> Action {
Action::DealCommunity(Card::from(n))
}
#[test]
fn new_is_empty() {
assert!(HandLog::new().to_actions().is_empty());
}
#[test]
fn record_appends_in_order() {
let log = HandLog::new();
log.record(ra(Round::Preflop));
log.record(deal(10));
assert_eq!(log.to_actions(), vec![ra(Round::Preflop), deal(10)]);
}
#[test]
fn freeze_collapses_into_prefix_and_empties_tail() {
let log = HandLog::new();
log.record(ra(Round::Preflop));
log.record(deal(10));
let frozen = log.freeze();
assert_eq!(frozen.to_actions(), vec![ra(Round::Preflop), deal(10)]);
frozen.record(deal(20));
assert_eq!(
frozen.to_actions(),
vec![ra(Round::Preflop), deal(10), deal(20)]
);
assert_eq!(log.to_actions(), vec![ra(Round::Preflop), deal(10)]);
}
#[test]
fn spawn_child_copies_tail_and_is_independent() {
let parent = HandLog::new();
parent.record(ra(Round::Flop));
let child = parent.spawn_child();
assert_eq!(child.to_actions(), vec![ra(Round::Flop)]);
child.record(deal(30));
parent.record(deal(40));
assert_eq!(child.to_actions(), vec![ra(Round::Flop), deal(30)]);
assert_eq!(parent.to_actions(), vec![ra(Round::Flop), deal(40)]);
}
#[test]
fn full_path_through_freeze_then_two_descents() {
let root = HandLog::new();
root.record(ra(Round::Preflop));
let d0 = root.freeze();
let d1 = d0.spawn_child();
d1.record(deal(1));
let d2 = d1.spawn_child();
d2.record(deal(2));
assert_eq!(d2.to_actions(), vec![ra(Round::Preflop), deal(1), deal(2)]);
}
#[tokio::test]
async fn historian_records_into_shared_log() {
use crate::arena::Historian;
let log = HandLog::new();
let mut hist = HandLogHistorian::new(log.clone());
let game_state = crate::arena::GameStateBuilder::default()
.num_players_with_stack(2, 100.0)
.big_blind(2.0)
.build()
.unwrap();
hist.record_action(0, &game_state, &ra(Round::Preflop))
.await
.unwrap();
hist.record_action(0, &game_state, &deal(7)).await.unwrap();
assert_eq!(log.to_actions(), vec![ra(Round::Preflop), deal(7)]);
}
}