1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
// Copyright (C) 2025 Vince Vasta
// SPDX-License-Identifier: Apache-2.0
//! Client game state types.
use crate::{
crypto::PeerId,
message::{HandPayoff, Message, PlayerAction, PlayerUpdate, SignedMessage},
poker::{Card, Chips, PlayerCards, TableId},
};
/// Game player data.
#[derive(Debug)]
pub struct Player {
/// This player id.
pub player_id: PeerId,
/// Cache player id digits to avoid generation at every repaint.
pub player_id_digits: String,
/// This player nickname.
pub nickname: String,
/// This player chips.
pub chips: Chips,
/// The last player bet.
pub bet: Chips,
/// The hand payoff.
pub payoff: Option<HandPayoff>,
/// The last player action.
pub action: PlayerAction,
/// The last player action.
pub action_timer: Option<u16>,
/// This playe cards.
pub cards: PlayerCards,
/// The player has the button.
pub has_button: bool,
/// The player is active in the hand.
pub is_active: bool,
}
impl Player {
fn new(player_id: PeerId, nickname: String, chips: Chips) -> Self {
Self {
player_id_digits: player_id.digits(),
player_id,
nickname,
chips,
bet: Chips::ZERO,
payoff: None,
action: PlayerAction::None,
action_timer: None,
cards: PlayerCards::None,
has_button: false,
is_active: true,
}
}
}
/// A player action request from the server.
#[derive(Debug)]
pub struct ActionRequest {
/// The actions choices requested by server.
pub actions: Vec<PlayerAction>,
/// The action minimum raise
pub min_raise: Chips,
/// The hand big blind.
pub big_blind: Chips,
}
impl ActionRequest {
/// Check if a call action is in the request.
pub fn can_call(&self) -> bool {
self.check_action(PlayerAction::Call)
}
/// Check if a check action is in the request.
pub fn can_check(&self) -> bool {
self.check_action(PlayerAction::Check)
}
/// Check if a bet action is in the request.
pub fn can_bet(&self) -> bool {
self.check_action(PlayerAction::Bet)
}
/// Check if a raise action is in the request.
pub fn can_raise(&self) -> bool {
self.check_action(PlayerAction::Raise)
}
fn check_action(&self, action: PlayerAction) -> bool {
self.actions.iter().any(|a| a == &action)
}
}
/// This client game state.
#[derive(Debug)]
pub struct GameState {
player_id: PeerId,
nickname: String,
server_key: String,
table_id: TableId,
seats: usize,
game_started: bool,
players: Vec<Player>,
action_request: Option<ActionRequest>,
board: Vec<Card>,
pot: Chips,
}
impl GameState {
/// Creates a new ClientState for the local player.
pub fn new(player_id: PeerId, nickname: String) -> Self {
Self {
player_id,
nickname,
table_id: TableId::NO_TABLE,
server_key: String::default(),
seats: 0,
game_started: false,
players: Vec::default(),
action_request: None,
board: Vec::default(),
pot: Chips::ZERO,
}
}
/// Handle an incoming server message.
pub fn handle_message(&mut self, msg: SignedMessage) {
match msg.message() {
Message::TableJoined {
table_id,
chips,
seats,
} => {
self.table_id = *table_id;
self.seats = *seats as usize;
self.server_key = msg.sender().digits();
// Add this player as the first player in the players list.
self.players.push(Player::new(
self.player_id.clone(),
self.nickname.clone(),
*chips,
));
}
Message::PlayerJoined {
player_id,
nickname,
chips,
} => {
self.players
.push(Player::new(player_id.clone(), nickname.clone(), *chips));
}
Message::PlayerLeft(player_id) => {
self.players.retain(|p| &p.player_id != player_id);
}
Message::StartGame(seats) => {
// Reorder seats according to the new order.
for (idx, seat_id) in seats.iter().enumerate() {
let pos = self
.players
.iter()
.position(|p| &p.player_id == seat_id)
.expect("Player not found");
self.players.swap(idx, pos);
}
// Move local player in first position.
let pos = self
.players
.iter()
.position(|p| p.player_id == self.player_id)
.expect("Local player not found");
self.players.rotate_left(pos);
self.game_started = true;
}
Message::StartHand => {
// Prepare for a new hand.
for player in &mut self.players {
player.cards = PlayerCards::None;
player.action = PlayerAction::None;
player.payoff = None;
}
}
Message::EndHand { payoffs, .. } => {
self.action_request = None;
self.pot = Chips::ZERO;
// Update winnings for each winning player.
for payoff in payoffs {
if let Some(p) = self
.players
.iter_mut()
.find(|p| p.player_id == payoff.player_id)
{
p.payoff = Some(payoff.clone());
}
}
}
Message::DealCards(c1, c2) => {
// This client player should be in first position.
assert!(!self.players.is_empty());
assert_eq!(self.players[0].player_id, self.player_id);
self.players[0].cards = PlayerCards::Cards(*c1, *c2);
}
Message::GameUpdate {
players,
board,
pot,
} => {
self.update_players(players);
self.board = board.clone();
self.pot = *pot;
}
Message::ActionRequest {
player_id,
min_raise,
big_blind,
actions,
} => {
// Check if the action has been requested for this player.
if &self.player_id == player_id {
self.action_request = Some(ActionRequest {
actions: actions.clone(),
min_raise: *min_raise,
big_blind: *big_blind,
});
}
}
_ => {}
}
}
/// Returns the requested player action if any.
pub fn action_request(&self) -> Option<&ActionRequest> {
self.action_request.as_ref()
}
/// Reset the action request.
pub fn reset_action_request(&mut self) {
self.action_request = None;
}
/// Returns the server key.
pub fn server_key(&self) -> &str {
&self.server_key
}
/// Returns a reference to the players.
pub fn players(&self) -> &[Player] {
&self.players
}
/// The current pot.
pub fn pot(&self) -> Chips {
self.pot
}
/// The board cards.
pub fn board(&self) -> &[Card] {
&self.board
}
/// The number of seats at this table.
pub fn seats(&self) -> usize {
self.seats
}
/// Checks if the game has started.
pub fn game_started(&self) -> bool {
self.game_started
}
/// Checks if the local player is active.
pub fn is_active(&self) -> bool {
!self.players.is_empty() && self.players[0].is_active
}
fn update_players(&mut self, updates: &[PlayerUpdate]) {
for update in updates {
if let Some(pos) = self
.players
.iter_mut()
.position(|p| p.player_id == update.player_id)
{
let player = &mut self.players[pos];
player.chips = update.chips;
player.bet = update.bet;
player.action = update.action;
player.action_timer = update.action_timer;
player.has_button = update.has_button;
player.is_active = update.is_active;
// Do not override cards for the local player as they are updated
// when we get a DealCards message.
if pos != 0 {
player.cards = update.cards;
}
// If local player has folded remove its cards.
if pos == 0 && !player.is_active {
player.cards = PlayerCards::None;
self.action_request = None;
}
}
}
}
}