use crate::game::deck::{Card, Rank};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PreflopTier {
Trash,
Marginal,
Playable,
Strong,
Premium,
}
impl PreflopTier {
pub fn base_strength(self) -> f64 {
match self {
PreflopTier::Premium => 0.90,
PreflopTier::Strong => 0.75,
PreflopTier::Playable => 0.60,
PreflopTier::Marginal => 0.45,
PreflopTier::Trash => 0.25,
}
}
}
const P: u8 = 1;
const S: u8 = 2;
const L: u8 = 3;
const M: u8 = 4;
const T: u8 = 5;
#[rustfmt::skip]
const PAIR_TIER: [u8; 13] = [
M, M, M, M, L, L, L, L, S, S, P, P, P,
];
#[rustfmt::skip]
const SUITED: [[u8; 13]; 13] = [
[0, T, T, T, T, T, T, T, T, T, T, M, L], [0, 0, M, T, T, T, T, T, T, T, T, M, L], [0, 0, 0, M, M, T, T, T, T, T, T, M, L], [0, 0, 0, 0, M, M, M, T, T, T, T, M, L], [0, 0, 0, 0, 0, M, L, M, T, T, T, M, L], [0, 0, 0, 0, 0, 0, L, L, M, T, T, M, L], [0, 0, 0, 0, 0, 0, 0, L, L, M, M, M, L], [0, 0, 0, 0, 0, 0, 0, 0, L, L, M, L, L], [0, 0, 0, 0, 0, 0, 0, 0, 0, L, L, L, S], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, L, S, S], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, S, S], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, P], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ];
#[rustfmt::skip]
const OFFSUIT: [[u8; 13]; 13] = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [T, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [T, M, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [T, T, M, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [T, T, T, M, 0, 0, 0, 0, 0, 0, 0, 0, 0], [T, T, T, T, M, 0, 0, 0, 0, 0, 0, 0, 0], [T, T, T, T, T, M, 0, 0, 0, 0, 0, 0, 0], [T, T, T, T, T, T, M, 0, 0, 0, 0, 0, 0], [T, T, T, T, T, T, T, M, 0, 0, 0, 0, 0], [T, T, T, T, T, T, T, T, M, 0, 0, 0, 0], [T, T, T, T, T, T, T, T, M, M, 0, 0, 0], [T, T, T, T, T, T, T, T, M, M, L, 0, 0], [T, T, T, M, M, M, M, M, L, L, S, P, 0], ];
fn tier_from_code(code: u8) -> PreflopTier {
match code {
P => PreflopTier::Premium,
S => PreflopTier::Strong,
L => PreflopTier::Playable,
M => PreflopTier::Marginal,
_ => PreflopTier::Trash,
}
}
fn rank_index(rank: Rank) -> usize {
(rank as u8 - 2) as usize
}
pub fn classify_preflop(cards: &[Card]) -> PreflopTier {
assert_eq!(cards.len(), 2, "classify_preflop requires exactly 2 cards");
let r0 = cards[0].rank;
let r1 = cards[1].rank;
let suited = cards[0].suit == cards[1].suit;
if r0 == r1 {
return tier_from_code(PAIR_TIER[rank_index(r0)]);
}
let (high, low) = if r0 > r1 { (r0, r1) } else { (r1, r0) };
let hi = rank_index(high);
let lo = rank_index(low);
let code = if suited {
SUITED[lo][hi]
} else {
OFFSUIT[hi][lo]
};
tier_from_code(code)
}
pub fn preflop_strength(cards: &[Card]) -> f64 {
let tier = classify_preflop(cards);
let base = tier.base_strength();
let high_rank = cards[0].rank.max(cards[1].rank);
let low_rank = cards[0].rank.min(cards[1].rank);
let kicker_bonus =
(high_rank as u8 - 2) as f64 / 12.0 * 0.04 + (low_rank as u8 - 2) as f64 / 12.0 * 0.01;
(base + kicker_bonus).min(1.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::game::deck::Suit;
fn card(rank: Rank, suit: Suit) -> Card {
Card::new(rank, suit)
}
fn hand(r0: Rank, s0: Suit, r1: Rank, s1: Suit) -> Vec<Card> {
vec![card(r0, s0), card(r1, s1)]
}
fn suited(r0: Rank, r1: Rank) -> Vec<Card> {
hand(r0, Suit::Spades, r1, Suit::Spades)
}
fn offsuit(r0: Rank, r1: Rank) -> Vec<Card> {
hand(r0, Suit::Spades, r1, Suit::Hearts)
}
fn pair(r: Rank) -> Vec<Card> {
hand(r, Suit::Spades, r, Suit::Hearts)
}
#[test]
fn test_premium_hands() {
assert_eq!(classify_preflop(&pair(Rank::Ace)), PreflopTier::Premium);
assert_eq!(classify_preflop(&pair(Rank::King)), PreflopTier::Premium);
assert_eq!(
classify_preflop(&suited(Rank::Ace, Rank::King)),
PreflopTier::Premium
);
assert_eq!(
classify_preflop(&offsuit(Rank::Ace, Rank::King)),
PreflopTier::Premium
);
}
#[test]
fn test_strong_hands() {
assert_eq!(classify_preflop(&pair(Rank::Jack)), PreflopTier::Strong);
assert_eq!(
classify_preflop(&suited(Rank::Ace, Rank::Queen)),
PreflopTier::Strong
);
assert_eq!(
classify_preflop(&suited(Rank::King, Rank::Queen)),
PreflopTier::Strong
);
}
#[test]
fn test_playable_hands() {
assert_eq!(classify_preflop(&pair(Rank::Nine)), PreflopTier::Playable);
assert_eq!(
classify_preflop(&suited(Rank::Ace, Rank::Two)),
PreflopTier::Playable
);
assert_eq!(
classify_preflop(&suited(Rank::Ten, Rank::Eight)),
PreflopTier::Playable
);
assert_eq!(
classify_preflop(&suited(Rank::Nine, Rank::Seven)),
PreflopTier::Playable
);
assert_eq!(
classify_preflop(&suited(Rank::Eight, Rank::Six)),
PreflopTier::Playable
);
}
#[test]
fn test_marginal_hands() {
assert_eq!(classify_preflop(&pair(Rank::Two)), PreflopTier::Marginal);
assert_eq!(
classify_preflop(&offsuit(Rank::Ace, Rank::Five)),
PreflopTier::Marginal
);
assert_eq!(
classify_preflop(&offsuit(Rank::Five, Rank::Four)),
PreflopTier::Marginal
);
assert_eq!(
classify_preflop(&offsuit(Rank::Four, Rank::Three)),
PreflopTier::Marginal
);
}
#[test]
fn test_trash_hands() {
assert_eq!(
classify_preflop(&offsuit(Rank::Seven, Rank::Two)),
PreflopTier::Trash
);
assert_eq!(
classify_preflop(&offsuit(Rank::Ace, Rank::Four)),
PreflopTier::Trash
);
assert_eq!(
classify_preflop(&offsuit(Rank::King, Rank::Nine)),
PreflopTier::Trash
);
assert_eq!(
classify_preflop(&offsuit(Rank::Queen, Rank::Nine)),
PreflopTier::Trash
);
assert_eq!(
classify_preflop(&offsuit(Rank::Three, Rank::Two)),
PreflopTier::Trash
);
}
#[test]
fn test_strength_ordering() {
let aa = preflop_strength(&pair(Rank::Ace));
let aks = preflop_strength(&suited(Rank::Ace, Rank::King));
let jj = preflop_strength(&pair(Rank::Jack));
let nn = preflop_strength(&pair(Rank::Nine));
let fives = preflop_strength(&pair(Rank::Five));
let seven_two = preflop_strength(&offsuit(Rank::Seven, Rank::Two));
assert!(
aa > aks,
"AA ({aa}) should beat AKs ({aks})"
);
assert!(
aks > jj,
"AKs ({aks}) should beat JJ ({jj})"
);
assert!(
jj > nn,
"JJ ({jj}) should beat 99 ({nn})"
);
assert!(
nn > fives,
"99 ({nn}) should beat 55 ({fives})"
);
assert!(
fives > seven_two,
"55 ({fives}) should beat 72o ({seven_two})"
);
}
#[test]
fn test_strength_capped_at_one() {
let s = preflop_strength(&pair(Rank::Ace));
assert!(s <= 1.0, "strength should not exceed 1.0, got {s}");
}
#[test]
fn test_fixed_matrix_entries() {
assert_eq!(classify_preflop(&suited(Rank::Jack, Rank::Eight)), PreflopTier::Marginal); assert_eq!(classify_preflop(&suited(Rank::Jack, Rank::Seven)), PreflopTier::Trash); assert_eq!(classify_preflop(&suited(Rank::Ten, Rank::Seven)), PreflopTier::Marginal); assert_eq!(classify_preflop(&suited(Rank::Ten, Rank::Six)), PreflopTier::Trash); assert_eq!(classify_preflop(&suited(Rank::Nine, Rank::Six)), PreflopTier::Marginal); assert_eq!(classify_preflop(&suited(Rank::Eight, Rank::Five)), PreflopTier::Marginal); assert_eq!(classify_preflop(&offsuit(Rank::King, Rank::Ten)), PreflopTier::Marginal); }
#[test]
fn test_card_order_does_not_matter() {
let h1 = suited(Rank::Ace, Rank::King);
let h2 = suited(Rank::King, Rank::Ace);
assert_eq!(classify_preflop(&h1), classify_preflop(&h2));
let h3 = offsuit(Rank::Seven, Rank::Two);
let h4 = offsuit(Rank::Two, Rank::Seven);
assert_eq!(classify_preflop(&h3), classify_preflop(&h4));
}
}