use rand::prelude::*;
use std::{
cmp::Ordering,
time::{Duration, Instant},
};
use tokio::sync::mpsc;
use freezeout_core::{
crypto::PeerId,
message::{PlayerAction, SignedMessage},
poker::{Chips, PlayerCards},
};
use super::TableMessage;
#[derive(Debug)]
pub struct Player {
pub player_id: PeerId,
pub table_tx: mpsc::Sender<TableMessage>,
pub nickname: String,
pub chips: Chips,
pub bet: Chips,
pub action: PlayerAction,
pub action_timer: Option<Instant>,
pub public_cards: PlayerCards,
pub hole_cards: PlayerCards,
pub is_active: bool,
pub has_button: bool,
}
impl Player {
pub fn new(
player_id: PeerId,
nickname: String,
chips: Chips,
table_tx: mpsc::Sender<TableMessage>,
) -> Self {
Self {
player_id,
table_tx,
nickname,
chips,
bet: Chips::default(),
action: PlayerAction::None,
action_timer: None,
public_cards: PlayerCards::None,
hole_cards: PlayerCards::None,
is_active: true,
has_button: false,
}
}
pub async fn send_message(&self, msg: SignedMessage) {
let _ = self.table_tx.send(TableMessage::Send(msg)).await;
}
pub async fn send_player_left(&self) {
let _ = self.table_tx.send(TableMessage::PlayerLeft).await;
}
pub async fn send_throttle(&self, dt: Duration) {
let _ = self.table_tx.send(TableMessage::Throttle(dt)).await;
}
pub fn bet(&mut self, action: PlayerAction, chips: Chips) {
let remainder = chips - self.bet;
if self.chips < remainder {
self.bet += self.chips;
self.chips = Chips::ZERO;
} else {
self.bet += remainder;
self.chips -= remainder;
}
self.action = action;
}
pub fn fold(&mut self) {
self.is_active = false;
self.action = PlayerAction::Fold;
self.hole_cards = PlayerCards::None;
self.public_cards = PlayerCards::None;
self.action_timer = None;
}
fn start_hand(&mut self) {
self.is_active = self.chips > Chips::ZERO;
self.has_button = false;
self.bet = Chips::ZERO;
self.action = PlayerAction::None;
self.public_cards = PlayerCards::None;
self.hole_cards = PlayerCards::None;
}
fn end_hand(&mut self) {
self.action = PlayerAction::None;
self.action_timer = None;
}
}
#[derive(Debug, Default)]
pub struct PlayersState {
players: Vec<Player>,
active_player: Option<usize>,
}
impl PlayersState {
pub fn join(&mut self, player: Player) {
self.players.push(player);
}
pub fn clear(&mut self) {
self.players.clear();
self.active_player = None;
}
pub fn leave(&mut self, player_id: &PeerId) -> Option<Player> {
if let Some(pos) = self.players.iter().position(|p| &p.player_id == player_id) {
let player = self.players.remove(pos);
let count_active = self.count_active();
if count_active == 0 {
self.active_player = None;
} else if count_active == 1 {
self.active_player = self.players.iter().position(|p| p.is_active);
} else if let Some(active_player) = self.active_player.as_mut() {
match pos.cmp(active_player) {
Ordering::Less => {
*active_player -= 1;
}
Ordering::Equal => {
if pos == self.players.len() {
*active_player = 0;
}
loop {
if self.players[*active_player].is_active {
break;
}
*active_player = (*active_player + 1) % self.players.len();
}
}
_ => {}
}
}
Some(player)
} else {
None
}
}
pub fn shuffle_seats<R: Rng>(&mut self, rng: &mut R) {
self.players.shuffle(rng);
}
pub fn count(&self) -> usize {
self.players.len()
}
pub fn count_active(&self) -> usize {
self.players.iter().filter(|p| p.is_active).count()
}
pub fn count_active_with_chips(&self) -> usize {
self.players
.iter()
.filter(|p| p.is_active && p.chips > Chips::ZERO)
.count()
}
pub fn count_with_chips(&self) -> usize {
self.players
.iter()
.filter(|p| p.chips > Chips::ZERO)
.count()
}
pub fn active_player(&mut self) -> Option<&mut Player> {
self.active_player
.and_then(|idx| self.players.get_mut(idx))
.filter(|p| p.is_active)
}
pub fn is_active(&self, player_id: &PeerId) -> bool {
self.active_player
.and_then(|idx| self.players.get(idx))
.map(|p| &p.player_id == player_id)
.unwrap_or(false)
}
#[cfg(test)]
pub fn player(&self, idx: usize) -> &Player {
self.players.get(idx).expect("No player at the given index")
}
pub fn iter(&self) -> impl Iterator<Item = &Player> {
self.players.iter()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Player> {
self.players.iter_mut()
}
pub fn activate_next_player(&mut self) {
if self.count_active() > 0 && self.active_player.is_some() {
let active_player = self.active_player.take().unwrap();
let iter = self
.players
.iter()
.enumerate()
.cycle()
.skip(active_player + 1)
.take(self.players.len() - 1);
for (pos, p) in iter {
if p.is_active && p.chips > Chips::ZERO {
self.active_player = Some(pos);
break;
}
}
}
}
pub fn start_hand(&mut self) {
for player in &mut self.players {
player.start_hand();
}
if self.count_active() > 1 {
loop {
self.players.rotate_left(1);
if self.players[0].is_active {
for p in self.players.iter_mut().rev() {
if p.is_active {
p.has_button = true;
break;
}
}
break;
}
}
self.active_player = Some(0);
} else {
self.active_player = None;
}
}
pub fn start_round(&mut self) {
self.active_player = None;
if self.count_active_with_chips() > 1 {
for (idx, p) in self.players.iter().enumerate() {
if p.chips > Chips::ZERO && p.is_active {
self.active_player = Some(idx);
return;
}
}
}
}
pub fn end_hand(&mut self) {
self.active_player = None;
self.players.iter_mut().for_each(Player::end_hand);
}
pub fn remove_with_no_chips(&mut self) {
self.players.retain(|p| p.chips > Chips::ZERO);
}
}
#[cfg(test)]
mod tests {
use super::*;
use freezeout_core::crypto::SigningKey;
fn new_player(chips: Chips) -> Player {
let peer_id = SigningKey::default().verifying_key().peer_id();
let (table_tx, _table_rx) = mpsc::channel(10);
Player::new(
peer_id.clone(),
"Alice".to_string(),
chips,
table_tx.clone(),
)
}
#[test]
fn test_player_bet() {
let init_chips = Chips::new(100_000);
let mut player = new_player(init_chips);
let bet_size = Chips::new(60_000);
player.bet(PlayerAction::Bet, bet_size);
assert_eq!(player.bet, bet_size);
assert_eq!(player.chips, init_chips - bet_size);
assert!(matches!(player.action, PlayerAction::Bet));
let bet_size = bet_size + Chips::new(20_000);
player.bet(PlayerAction::Bet, bet_size);
assert_eq!(player.bet, bet_size);
assert_eq!(player.chips, init_chips - bet_size);
player.start_hand();
assert!(matches!(player.action, PlayerAction::None));
assert!(player.is_active);
assert_eq!(player.bet, Chips::ZERO);
assert_eq!(player.chips, init_chips - bet_size);
let remaining_chips = player.chips;
player.bet(PlayerAction::Bet, Chips::new(1_000_000));
assert_eq!(player.bet, remaining_chips);
assert_eq!(player.chips, Chips::ZERO);
}
#[test]
fn test_player_fold() {
let init_chips = Chips::new(100_000);
let mut player = new_player(init_chips);
player.bet(PlayerAction::Bet, Chips::new(20_000));
player.action_timer = Some(Instant::now());
player.fold();
assert!(matches!(player.action, PlayerAction::Fold));
assert!(!player.is_active);
assert!(player.action_timer.is_none());
}
fn new_players_state(n: usize) -> PlayersState {
let mut players = PlayersState::default();
(0..n).for_each(|_| players.join(new_player(Chips::new(100_000))));
players
}
#[test]
fn player_before_active_leaves() {
const SEATS: usize = 4;
let mut players = new_players_state(SEATS);
assert_eq!(players.count_active(), SEATS);
assert!(players.active_player().is_none());
players.start_hand();
players.activate_next_player();
assert_eq!(players.active_player.unwrap(), 1);
let player_id = players.player(0).player_id.clone();
assert!(players.leave(&player_id).is_some());
assert_eq!(players.active_player.unwrap(), 0);
assert_eq!(players.count_active(), SEATS - 1);
}
#[test]
fn player_after_active_leaves() {
const SEATS: usize = 4;
let mut players = new_players_state(SEATS);
assert_eq!(players.count_active(), SEATS);
assert!(players.active_player().is_none());
players.start_hand();
players.activate_next_player();
assert_eq!(players.active_player.unwrap(), 1);
let player_id = players.player(2).player_id.clone();
assert!(players.leave(&player_id).is_some());
assert_eq!(players.active_player.unwrap(), 1);
assert_eq!(players.count_active(), SEATS - 1);
}
#[test]
fn active_player_leaves() {
const SEATS: usize = 4;
let mut players = new_players_state(SEATS);
assert_eq!(players.count_active(), SEATS);
assert!(players.active_player().is_none());
players.start_hand();
players.activate_next_player();
assert_eq!(players.active_player.unwrap(), 1);
let active_id = players.player(1).player_id.clone();
let next_id = players.player(2).player_id.clone();
assert!(players.leave(&active_id).is_some());
assert_eq!(players.active_player.unwrap(), 1);
assert_eq!(players.active_player().unwrap().player_id, next_id);
assert_eq!(players.count_active(), SEATS - 1);
}
#[test]
fn active_player_before_inactive_player_leaves() {
const SEATS: usize = 4;
let mut players = new_players_state(SEATS);
assert_eq!(players.count_active(), SEATS);
assert!(players.active_player().is_none());
players.start_hand();
players.activate_next_player();
assert_eq!(players.active_player.unwrap(), 1);
players.iter_mut().nth(2).unwrap().fold();
assert_eq!(players.count_active(), SEATS - 1);
let active_id = players.player(1).player_id.clone();
let next_id = players.player(3).player_id.clone();
assert!(players.leave(&active_id).is_some());
assert_eq!(players.active_player.unwrap(), 2);
assert_eq!(players.active_player().unwrap().player_id, next_id);
assert_eq!(players.count_active(), SEATS - 2);
}
}