use super::*;
use rbp_cards::*;
use rbp_core::*;
use std::ops::Not;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Partial {
pov: Turn,
actions: Vec<Action>,
reveals: Arrangement,
}
impl Arbitrary for Partial {
fn random() -> Self {
Self::initial(Turn::Choice(0))
}
}
impl Partial {
pub fn initial(pov: Turn) -> Self {
Self {
pov,
actions: Vec::new(),
reveals: Arrangement::from(Street::Pref),
}
}
pub fn with_pov(&self, pov: Turn) -> Self {
Self {
pov,
actions: self.actions.clone(),
reveals: self.reveals.clone(),
}
}
}
impl Recall for Partial {
fn root(&self) -> Game {
Game::blinds()
.into_iter()
.fold(self.base(), |mut g, a| g.consume(a))
}
fn actions(&self) -> &[Action] {
&self.actions
}
}
impl Partial {
pub fn betting_edges(&self) -> Vec<Edge> {
let game = self.head();
self.choices()
.into_iter()
.filter(|edge| matches!(edge, Edge::Open(_) | Edge::Raise(_)))
.filter(|edge| matches!(game.actionize(*edge), Action::Raise(_)))
.collect()
}
pub fn histories(&self) -> Vec<(Observation, Perfect)> {
self.seen()
.opponents()
.map(|villain| {
let hole = Hole::from(villain.pocket().clone());
(villain, Perfect::from((self, hole)))
})
.collect()
}
}
impl From<(Turn, Arrangement)> for Partial {
fn from((pov, reveals): (Turn, Arrangement)) -> Self {
let actions = Vec::new();
Self {
pov,
actions,
reveals,
}
}
}
impl From<Street> for Partial {
fn from(_: Street) -> Self {
todo!()
}
}
impl From<(Turn, Observation, Vec<Action>)> for Partial {
fn from((pov, seen, actions): (Turn, Observation, Vec<Action>)) -> Self {
Self::try_build(pov, seen, actions).expect("valid action sequence")
}
}
impl Partial {
pub fn try_build(pov: Turn, seen: Observation, actions: Vec<Action>) -> anyhow::Result<Self> {
let reveals = Arrangement::from(seen);
let initial = Self {
pov,
actions: Vec::new(),
reveals,
};
actions.into_iter().try_fold(initial, |r, a| r.try_push(a))
}
}
impl Partial {
pub fn base(&self) -> Game {
Game::default().wipe(Hole::from(self.seen()))
}
pub fn street(&self) -> Street {
self.head().street()
}
pub fn dealt(&self) -> Street {
Street::from(self.actions.iter().filter(|a| a.is_chance()).count() as isize)
}
pub fn turn(&self) -> Turn {
self.pov
}
pub fn arr(&self) -> Arrangement {
self.reveals.clone()
}
pub fn seen(&self) -> Observation {
self.reveals.observation()
}
pub fn reset(&self) -> Self {
Self {
pov: self.turn(),
reveals: self.reveals.clone(),
actions: Vec::new(),
}
}
pub fn cursor(&self) -> petgraph::graph::NodeIndex {
petgraph::graph::NodeIndex::new(self.actions().len().saturating_sub(1))
}
pub fn plays(&self) -> Vec<(Position, Action, Street)> {
self.states()
.windows(2)
.zip(self.actions().iter().cloned())
.filter_map(|(pair, action)| {
action
.is_choice()
.then(|| (pair[0].turn().position(), action, pair[0].street()))
})
.collect()
}
pub fn aggressor(&self) -> Option<Position> {
self.plays()
.into_iter()
.filter_map(|(pos, action, _)| action.is_aggro().then_some(pos))
.last()
}
pub fn truncate(&self, street: Street) -> Self {
let pov = self.turn();
let reveals = self.reveals.clone();
let actions = self
.states()
.into_iter()
.skip(1)
.zip(self.actions().iter().cloned())
.map(|(game, action)| (action, game))
.collect::<Vec<(Action, Game)>>()
.into_iter()
.take_while(|(_, game)| game.street() <= street)
.map(|(action, _)| action)
.collect::<Vec<Action>>();
let recall = Self {
pov,
reveals,
actions,
};
recall.sprout()
}
pub fn replace(&self, reveals: Arrangement) -> Self {
let mut actions = self.actions().to_vec();
actions
.iter_mut()
.filter(|a| a.is_chance())
.zip(reveals.draws())
.for_each(|(old, new)| *old = new);
Self {
pov: self.turn(),
actions,
reveals,
}
}
pub fn decisions(&self, street: Street) -> Vec<Action> {
let mut actions = Vec::new();
let mut current = Street::Pref;
for action in self.actions().iter().cloned() {
if action.is_chance() {
current = current.next();
} else if current == street {
actions.push(action);
}
}
actions
}
pub fn board(&self) -> Vec<Card> {
let street = self.head().street();
Street::all()
.iter()
.skip(1)
.filter(|s| **s <= street)
.cloned()
.flat_map(|s| self.revealed(s))
.collect()
}
pub fn revealed(&self, street: Street) -> Vec<Card> {
self.reveals.revealed(street)
}
pub fn isomorphism(&self) -> Isomorphism {
Isomorphism::from(self.seen())
}
pub fn empty(&self) -> bool {
self.actions().is_empty()
}
pub fn aligned(&self) -> bool {
self.seen().public().clone()
== self
.actions()
.iter()
.filter(|a| a.is_chance())
.filter_map(|a| a.hand())
.fold(Hand::empty(), Hand::add)
}
}
impl Partial {
pub fn undo(&self) -> Self {
debug_assert!(self.can_undo());
let mut copy = self.clone();
copy.actions.pop();
copy.recoil()
}
pub fn push(&self, action: Action) -> Self {
self.try_push(action).expect("valid action")
}
pub fn try_push(&self, action: Action) -> anyhow::Result<Self> {
if !self.can_push(&action) {
return Err(anyhow::anyhow!(
"illegal action {:?} at {:?}",
action,
self.head().turn()
));
}
let mut copy = self.clone();
copy.actions.push(action);
Ok(copy.sprout())
}
}
impl Partial {
pub fn validate(self) -> anyhow::Result<Self> {
let recall = self.sprout();
if !recall.aligned() {
return Err(anyhow::anyhow!("recall is not aligned {}", self));
}
if !recall.can_play() {
return Err(anyhow::anyhow!("recall is not playable {}", self));
}
Ok(recall)
}
}
impl Partial {
fn sprout(&self) -> Self {
let mut copy = self.clone();
while copy.can_deal() {
let street = copy.head().street().next();
let reveal = copy.revealed(street).into();
copy.actions.push(Action::Draw(reveal));
}
copy
}
fn recoil(&self) -> Self {
let mut copy = self.clone();
while copy.can_deal() {
copy.actions.pop();
}
copy
}
}
impl Partial {
pub fn can_play(&self) -> bool {
self.head().turn() == self.turn() && self.head().street() == self.seen().street() }
pub fn can_push(&self, action: &Action) -> bool {
self.head().is_allowed(action)
}
pub fn can_undo(&self) -> bool {
!self.actions.is_empty()
}
fn can_deal(&self) -> bool {
self.can_know() && self.head().turn() == Turn::Chance
}
fn can_know(&self) -> bool {
self.head().street() < self.seen().street()
}
}
impl std::fmt::Display for Partial {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
const L: usize = 4;
const R: usize = 44;
const A: usize = 8;
let hole = self
.reveals
.pocket()
.iter()
.map(|c| format!("{}", c))
.collect::<Vec<_>>()
.join(" ");
let board = self
.board()
.iter()
.map(|c| format!("{}", c))
.collect::<Vec<_>>()
.join(" ");
let cards = if board.is_empty() {
format!("{}", hole)
} else {
format!("{} │ {}", hole, board)
};
writeln!(f, "┌{}┬{}┐", "─".repeat(L), "─".repeat(R))?;
writeln!(
f,
"│ {:>2} │ {:<w$} │",
self.turn().label(),
cards,
w = R - 2
)?;
writeln!(f, "├{}┼{}┤", "─".repeat(L), "─".repeat(R))?;
Street::all()
.iter()
.filter_map(|street| {
let actions = self.decisions(*street);
actions.is_empty().not().then_some((street, actions))
})
.try_for_each(|(street, actions)| {
let grid = actions
.iter()
.map(|a| format!("{:<w$}", a.symbol(), w = A))
.collect::<String>();
writeln!(f, "│ {:>2} │ {:<w$} │", street.symbol(), grid, w = R - 2)
})?;
write!(f, "└{}┴{}┘", "─".repeat(L), "─".repeat(R))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ops::Not;
#[test]
fn initial_invariants() {
let r = Partial::initial(Turn::Choice(0));
assert!(r.empty());
assert!(r.aligned());
assert_eq!(r.reset(), r);
assert_eq!(r.seen().street(), Street::Pref);
assert_eq!(r.head().street(), Street::Pref);
assert_eq!(r.actions().len(), 0);
}
#[test]
fn reset_idempotent() {
let r = Partial::initial(Turn::Choice(0))
.push(Action::Call(1))
.push(Action::Raise(5))
.push(Action::Raise(20))
.push(Action::Call(15));
assert_eq!(r.reset(), r.reset().reset());
}
#[test]
fn push_undo_inverse() {
let r = Partial::initial(Turn::Choice(0));
let a = r.head().legal().first().cloned().expect("legal");
assert_eq!(r.push(a).undo().subgame().length(), r.subgame().length());
}
#[test]
fn base_vs_root_vs_head() {
let r = Partial::initial(Turn::Choice(0));
let base = r.base();
let root = r.root();
let head = r.head();
assert_eq!(base.street(), Street::Pref);
assert_eq!(root.street(), Street::Pref);
assert_eq!(head.street(), Street::Pref);
assert_eq!(base.pot(), 0); assert_eq!(root.pot(), Game::sblind() + Game::bblind()); assert_eq!(head.pot(), Game::sblind() + Game::bblind()); }
#[test]
fn states_reconstruction() {
let r = Partial::initial(Turn::Choice(0)).push(Action::Call(1));
let states = r.states();
assert_eq!(states.len(), r.actions().len() + 1);
assert_eq!(states.first(), Some(&r.root()));
assert_eq!(states.last(), Some(&r.head()));
states
.windows(2)
.zip(r.actions().iter())
.for_each(|(pair, &act)| assert_eq!(pair[1], pair[0].apply(act)));
}
#[test]
fn subgame_current_street() {
let r = Partial::initial(Turn::Choice(0));
assert_eq!(r.subgame().length(), 0);
let r = r.push(Action::Call(1));
assert_eq!(r.subgame().length(), 1);
}
#[test]
fn alignment_check() {
let obs = Observation::from(Street::Flop);
let act = vec![
Action::Call(1), Action::Check,
];
assert!(Partial::from((Turn::Choice(0), obs, act)).aligned());
assert!(
Partial::from((Turn::Choice(0), Arrangement::from(Street::Flop)))
.push(Action::Call(1))
.push(Action::Check)
.aligned()
);
}
#[test]
fn behindness_observation_ahead() {
let behind = Partial {
pov: Turn::Choice(0),
actions: Vec::new(),
reveals: Arrangement::from(Street::Turn),
};
assert!(behind.seen().street() > behind.head().street()); assert!(behind.aligned().not()); }
#[test]
fn board_by_street() {
let r = Partial::from((Turn::Choice(0), Arrangement::from(Street::Rive)));
assert_eq!(r.board().len(), 0);
let r = r.push(Action::Call(1)).push(Action::Check);
assert_eq!(r.board().len(), 3);
let r = r.push(Action::Check).push(Action::Check);
assert_eq!(r.board().len(), 4);
let r = r.push(Action::Check).push(Action::Check);
assert_eq!(r.board().len(), 5);
}
#[test]
fn truncate_to_street() {
let r = Partial::from((Turn::Choice(0), Arrangement::from(Street::Flop)))
.push(Action::Call(1)) .push(Action::Check) .push(Action::Check) .push(Action::Check); let t = r.truncate(Street::Pref);
assert!(r.head().street() == Street::Flop);
assert!(t.head().street() == Street::Flop);
assert!(t.actions().len() < r.actions().len());
}
#[test]
fn decisions_per_street() {
let r = Partial::from((Turn::Choice(0), Arrangement::from(Street::Flop)))
.push(Action::Call(1))
.push(Action::Check)
.push(Action::Check)
.push(Action::Check);
assert_eq!(r.decisions(Street::Pref).len(), 2);
assert_eq!(r.decisions(Street::Flop).len(), 2);
assert!(r.decisions(Street::Pref).iter().all(|a| a.is_choice()));
assert!(r.decisions(Street::Flop).iter().all(|a| a.is_choice()));
}
#[test]
fn playability_all_streets() {
let r = Partial::from((Turn::Choice(0), Arrangement::from(Street::Rive)));
assert_eq!(r.head().turn(), Turn::Choice(0));
assert_eq!(r.head().street(), Street::Pref);
let r = r.push(Action::Call(1)).push(Action::Check);
assert_eq!(r.head().street(), Street::Flop);
assert_eq!(r.head().turn(), Turn::Choice(1));
let r = r.push(Action::Check).push(Action::Check);
assert_eq!(r.head().street(), Street::Turn);
assert_eq!(r.head().turn(), Turn::Choice(1));
let r = r.push(Action::Check).push(Action::Check);
assert_eq!(r.head().street(), Street::Rive);
assert_eq!(r.head().turn(), Turn::Choice(1));
assert!(r.aligned());
}
#[test]
fn playability_not_our_turn() {
let r =
Partial::from((Turn::Choice(0), Arrangement::from(Street::Pref))).push(Action::Call(1));
assert_eq!(r.head().turn(), Turn::Choice(1));
}
#[test]
fn from_arrangement_empty_actions() {
let r = Partial::from((Turn::Choice(0), Arrangement::from(Street::Pref)));
assert_eq!(r.actions().len(), 0);
assert_eq!(r.root().pot(), Game::sblind() + Game::bblind());
}
#[test]
fn from_tuple_stores_actions() {
let obs = Observation::from(Street::Pref);
let act = vec![
Action::Call(1), ];
let r = Partial::from((Turn::Choice(0), obs, act.clone()));
assert_eq!(r.actions().len(), act.len());
assert_eq!(r.complete().len(), Game::blinds().len() + act.len());
}
#[test]
fn replace_swaps_arrangement() {
let obs = Observation::from(Street::Flop);
let act = vec![
Action::Call(1), Action::Check,
];
let old = Partial::from((Turn::Choice(0), obs, act));
let new = old.replace(Arrangement::from(Street::Flop));
assert_ne!(new.seen(), old.seen());
assert_eq!(new.turn(), old.turn());
}
#[test]
fn revealed_per_street() {
let r = Partial::from((Turn::Choice(0), Arrangement::from(Street::Turn)));
assert_eq!(r.revealed(Street::Flop).len(), 3);
assert_eq!(r.revealed(Street::Turn).len(), 1);
assert_eq!(r.revealed(Street::Rive).len(), 0);
}
#[test]
fn empty_means_no_decisions() {
assert!(Partial::initial(Turn::Choice(0)).empty());
assert!(
Partial::initial(Turn::Choice(0))
.push(Action::Call(1))
.empty()
.not()
);
}
#[test]
fn aggression_counts_trailing() {
let obs = Observation::from(Street::Pref);
let act = vec![
Action::Raise(4), Action::Raise(8),
];
let r = Partial::from((Turn::Choice(0), obs, act));
assert_eq!(
r.aggression(),
r.subgame()
.into_iter()
.rev()
.take_while(|e| e.is_choice())
.filter(|e| e.is_aggro())
.count()
);
}
#[test]
fn choices_nonempty() {
assert!(
Partial::from((Turn::Choice(0), Arrangement::from(Street::Pref)))
.choices()
.length()
> 0
);
}
#[test]
fn can_play_conditions() {
let r = Partial::from((Turn::Choice(0), Arrangement::from(Street::Pref)));
assert_eq!(r.can_play(), r.turn() == Turn::Choice(0)); let s = r.push(Action::Call(1));
assert_eq!(s.can_play(), s.turn() == Turn::Choice(1)); }
#[test]
fn can_undo_conditions() {
let r = Partial::initial(Turn::Choice(0));
assert!(r.can_undo().not());
assert!(r.push(Action::Call(1)).can_undo());
}
#[test]
fn can_push_conditions() {
let r = Partial::initial(Turn::Choice(0));
assert!(r.can_push(&Action::Call(1)));
assert!(r.can_push(&Action::Check).not());
}
}