use super::*;
use rbp_cards::Street;
use rbp_core::*;
use std::hash::Hash;
#[derive(Debug, Clone, Copy, Hash, Ord, PartialOrd, PartialEq, Eq)]
pub enum Edge {
Draw,
Fold,
Check,
Call,
Open(Chips),
Raise(Odds),
Shove,
}
impl Edge {
pub fn is_shove(&self) -> bool {
matches!(self, Edge::Shove)
}
pub fn is_raise(&self) -> bool {
matches!(self, Edge::Raise(_) | Edge::Open(_))
}
pub fn is_folded(&self) -> bool {
matches!(self, Edge::Fold)
}
pub fn is_chance(&self) -> bool {
matches!(self, Edge::Draw)
}
pub fn is_aggro(&self) -> bool {
self.is_raise() || self.is_shove()
}
pub fn is_choice(&self) -> bool {
!self.is_chance()
}
}
impl Edge {
pub fn regret(&self) -> (Utility, Utility) {
match self {
Edge::Open(_) => (Utility::default(), BIAS_RAISE),
Edge::Raise(_) => (Utility::default(), BIAS_RAISE),
Edge::Check => (Utility::default(), BIAS_OTHER),
Edge::Shove => (Utility::default(), BIAS_RAISE),
Edge::Call => (Utility::default(), BIAS_OTHER),
Edge::Fold => (Utility::default(), BIAS_FOLDS),
Edge::Draw => panic!("chance edges have no learned regret"),
}
}
pub fn policy(&self) -> (Probability, Probability) {
(Probability::default(), Probability::default())
}
}
impl From<Action> for Edge {
fn from(action: Action) -> Self {
match action {
Action::Fold => Edge::Fold,
Action::Check => Edge::Check,
Action::Call(_) => Edge::Call,
Action::Draw(_) => Edge::Draw,
Action::Shove(_) => Edge::Shove,
Action::Raise(_) => panic!("raise must be converted via Game::edgify"),
Action::Blind(_) => panic!("blinds are not in any MCCFR trees"),
}
}
}
impl From<Odds> for Edge {
fn from(odds: Odds) -> Self {
Edge::Raise(odds)
}
}
const OPENS_GRID: [Chips; 4] = [2, 3, 4, 8];
const RAISE_GRID: [Odds; 6] = [
Odds::new(1, 3), Odds::new(1, 2), Odds::new(2, 3), Odds::new(1, 1), Odds::new(3, 2), Odds::new(2, 1), ];
impl Edge {
pub fn raises(street: Street, depth: usize) -> Vec<Self> {
if depth > MAX_RAISE_REPEATS {
return vec![];
}
match (street, depth) {
(Street::Pref, 0) => OPENS_GRID.iter().map(|&n| Edge::Open(n)).collect(),
(Street::Pref, 1) => vec![
Edge::Raise(Odds::new(1, 1)),
Edge::Raise(Odds::new(3, 2)),
Edge::Raise(Odds::new(2, 1)),
],
(Street::Pref, _) => vec![Edge::Raise(Odds::new(1, 1)), Edge::Raise(Odds::new(2, 1))],
(Street::Flop, 0) => vec![
Edge::Raise(Odds::new(1, 3)),
Edge::Raise(Odds::new(1, 2)),
Edge::Raise(Odds::new(1, 1)),
Edge::Raise(Odds::new(2, 1)),
],
(Street::Flop, 1) => vec![
Edge::Raise(Odds::new(2, 3)),
Edge::Raise(Odds::new(1, 1)),
Edge::Raise(Odds::new(3, 2)),
],
(Street::Flop, _) => vec![Edge::Raise(Odds::new(1, 1)), Edge::Raise(Odds::new(3, 2))],
(Street::Turn, 0) => vec![
Edge::Raise(Odds::new(1, 3)),
Edge::Raise(Odds::new(2, 3)),
Edge::Raise(Odds::new(1, 1)),
Edge::Raise(Odds::new(2, 1)),
],
(Street::Turn, _) => vec![Edge::Raise(Odds::new(1, 1)), Edge::Raise(Odds::new(3, 2))],
(Street::Rive, 0) => vec![
Edge::Raise(Odds::new(1, 3)),
Edge::Raise(Odds::new(1, 2)),
Edge::Raise(Odds::new(1, 1)),
Edge::Raise(Odds::new(2, 1)),
],
(Street::Rive, 1) => vec![
Edge::Raise(Odds::new(2, 3)),
Edge::Raise(Odds::new(1, 1)),
Edge::Raise(Odds::new(2, 1)),
],
(Street::Rive, _) => vec![Edge::Raise(Odds::new(1, 1))],
}
}
pub fn into_chips(self, pot: Chips) -> Chips {
match self {
Edge::Open(n) => n * B_BLIND,
Edge::Raise(odds) => (pot as Utility * Probability::from(odds)) as Chips,
_ => 0,
}
}
}
impl From<Edge> for u8 {
fn from(edge: Edge) -> Self {
match edge {
Edge::Draw => 1,
Edge::Fold => 2,
Edge::Check => 3,
Edge::Call => 4,
Edge::Shove => 5,
Edge::Open(n) => {
6 + OPENS_GRID
.iter()
.position(|&b| b == n)
.expect("invalid open size") as u8
}
Edge::Raise(odds) => {
10 + RAISE_GRID
.iter()
.position(|&o| o == odds)
.expect("invalid raise odds") as u8
}
}
}
}
impl From<u8> for Edge {
fn from(value: u8) -> Self {
match value {
1 => Edge::Draw,
2 => Edge::Fold,
3 => Edge::Check,
4 => Edge::Call,
5 => Edge::Shove,
6..=9 => Edge::Open(OPENS_GRID[value as usize - 6]),
10..=15 => Edge::Raise(RAISE_GRID[value as usize - 10]),
_ => unreachable!("invalid edge encoding: {}", value),
}
}
}
impl From<u64> for Edge {
fn from(value: u64) -> Self {
match value & 0b111 {
0 => Self::Draw,
1 => Self::Fold,
2 => Self::Check,
3 => Self::Call,
4 => {
if value & (1 << 19) != 0 {
Self::Open(((value >> 3) & 0xFF) as Chips)
} else {
Self::Raise(Odds::new(
((value >> 3) & 0xFF) as Chips,
((value >> 11) & 0xFF) as Chips,
))
}
}
5 => Self::Shove,
6 => Self::Open(((value >> 3) & 0xFF) as Chips),
_ => unreachable!("invalid edge encoding"),
}
}
}
impl From<Edge> for u64 {
fn from(edge: Edge) -> Self {
match edge {
Edge::Draw => 0,
Edge::Fold => 1,
Edge::Check => 2,
Edge::Call => 3,
Edge::Raise(odds) => 4 | ((odds.numer() as u64) << 3) | ((odds.denom() as u64) << 11),
Edge::Shove => 5,
Edge::Open(n) => 6 | ((n as u64) << 3),
}
}
}
impl TryFrom<&str> for Edge {
type Error = anyhow::Error;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"?" => Ok(Edge::Draw),
"F" => Ok(Edge::Fold),
"*" => Ok(Edge::Call),
"O" => Ok(Edge::Check),
"!" => Ok(Edge::Shove),
s if s.ends_with("bb") => {
let n = s
.strip_suffix("bb")
.and_then(|x| x.parse::<Chips>().ok())
.ok_or_else(|| anyhow::anyhow!("invalid bb format"))?;
Ok(Edge::Open(n))
}
s if s.contains(':') => {
let (n, d) = s
.split_once(':')
.ok_or_else(|| anyhow::anyhow!("invalid ratio format"))?;
let n = n.parse::<Chips>()?;
let d = d.parse::<Chips>()?;
Ok(Edge::Raise(Odds::new(n, d)))
}
_ => Err(anyhow::anyhow!("invalid edge format: {}", s)),
}
}
}
impl std::fmt::Display for Edge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Edge::Draw => write!(f, "?"),
Edge::Fold => write!(f, "F"),
Edge::Call => write!(f, "*"),
Edge::Check => write!(f, "O"),
Edge::Shove => write!(f, "!"),
Edge::Open(n) => write!(f, "{}bb", n),
Edge::Raise(odds) => write!(f, "{}:{}", odds.numer(), odds.denom()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rbp_cards::Street;
#[test]
fn bijective_u8() {
let edges = vec![Edge::Draw, Edge::Fold, Edge::Check, Edge::Call, Edge::Shove];
let opens = OPENS_GRID.iter().map(|&n| Edge::Open(n));
let raises = RAISE_GRID.iter().map(|&o| Edge::Raise(o));
for edge in edges.into_iter().chain(opens).chain(raises) {
assert_eq!(
edge,
Edge::from(u8::from(edge)),
"u8 roundtrip failed for {:?}",
edge
);
}
}
#[test]
fn bijective_u64() {
let edges = vec![Edge::Draw, Edge::Fold, Edge::Check, Edge::Call, Edge::Shove];
let opens = OPENS_GRID.iter().map(|&n| Edge::Open(n));
let raises = RAISE_GRID.iter().map(|&o| Edge::Raise(o));
for edge in edges.into_iter().chain(opens).chain(raises) {
assert_eq!(
edge,
Edge::from(u64::from(edge)),
"u64 roundtrip failed for {:?}",
edge
);
}
}
#[test]
fn string_roundtrip() {
let edges = vec![
Edge::Draw,
Edge::Fold,
Edge::Check,
Edge::Call,
Edge::Shove,
Edge::Open(2),
Edge::Open(3),
Edge::Open(8),
Edge::Raise(Odds::new(1, 2)),
Edge::Raise(Odds::new(1, 1)),
Edge::Raise(Odds::new(3, 2)),
Edge::Raise(Odds::new(2, 1)),
];
for edge in edges {
let s = edge.to_string();
let parsed = Edge::try_from(s.as_str()).unwrap();
assert_eq!(edge, parsed, "string roundtrip failed for {:?}", edge);
}
}
#[test]
fn backwards_compat_u64_bbs() {
let old_bbs_8 = 4u64 | (1 << 19) | (8 << 3);
assert_eq!(Edge::from(old_bbs_8), Edge::Open(8));
let old_bbs_2 = 4u64 | (1 << 19) | (2 << 3);
assert_eq!(Edge::from(old_bbs_2), Edge::Open(2));
}
#[test]
fn raises_preflop_depth0_returns_opens() {
let edges = Edge::raises(Street::Pref, 0);
assert!(edges.iter().all(|e| matches!(e, Edge::Open(_))));
assert_eq!(edges.len(), 4);
}
#[test]
fn raises_postflop_returns_raises() {
for street in [Street::Flop, Street::Turn, Street::Rive] {
for depth in 0..=2 {
let edges = Edge::raises(street, depth);
assert!(edges.iter().all(|e| matches!(e, Edge::Raise(_))));
}
}
}
}
impl Arbitrary for Edge {
fn random() -> Self {
use rand::prelude::IndexedRandom;
match rand::random_range(0..7) {
0 => Self::Draw,
1 => Self::Fold,
2 => Self::Check,
3 => Self::Call,
4 => Self::Shove,
5 => Self::Open(*OPENS_GRID.choose(&mut rand::rng()).unwrap()),
6 => Self::Raise(*RAISE_GRID.choose(&mut rand::rng()).unwrap()),
_ => unreachable!(),
}
}
}