Skip to main content

freezeout_core/
message.rs

1// Copyright (C) 2025 Vince Vasta
2// SPDX-License-Identifier: Apache-2.0
3
4//! Type definitions for messages between the client and server.
5use anyhow::{Result, bail};
6use serde::{Deserialize, Serialize};
7use std::sync::Arc;
8
9use crate::{
10    crypto::{PeerId, Signature, SigningKey, VerifyingKey},
11    poker::{Card, Chips, PlayerCards, TableId},
12};
13
14/// Message exchanged by a client and a server.
15#[derive(Debug, Serialize, Deserialize)]
16pub enum Message {
17    /// Joins a server with a nickname.
18    JoinServer {
19        /// The player nickname.
20        nickname: String,
21    },
22    /// A player account information.
23    ServerJoined {
24        /// The player nickname.
25        nickname: String,
26        /// The chips amount for the player.
27        chips: Chips,
28    },
29    /// Join a table.
30    JoinTable,
31    /// Leave a table.
32    LeaveTable,
33    /// Table joined confirmation.
34    TableJoined {
35        /// The table the player joined.
36        table_id: TableId,
37        /// The chips amount for the player who joined.
38        chips: Chips,
39        /// The number of seats at this table.
40        seats: u8,
41    },
42    /// There are no tables left.
43    NoTablesLeft,
44    /// The player doesn't have enough chips to join a game.
45    NotEnoughChips,
46    /// The player has already joined a table.
47    PlayerAlreadyJoined,
48    /// A player joined the table.
49    PlayerJoined {
50        /// The player id.
51        player_id: PeerId,
52        /// The player nickname.
53        nickname: String,
54        /// The player chips.
55        chips: Chips,
56    },
57    /// Show the account dialog.
58    ShowAccount {
59        /// The player chips.
60        chips: Chips,
61    },
62    /// Tell players the game is starting and update the seats order.
63    StartGame(Vec<PeerId>),
64    /// Tell players to prepare for a new hand.
65    StartHand,
66    /// Tell players the hand has completed and who won.
67    EndHand {
68        /// List of payoffs for the hand.
69        payoffs: Vec<HandPayoff>,
70        /// The board cards.
71        board: Vec<Card>,
72        /// Players cards.
73        cards: Vec<(PeerId, PlayerCards)>,
74    },
75    /// Deal cards to a player.
76    DealCards(Card, Card),
77    /// A player left the table.
78    PlayerLeft(PeerId),
79    /// A game state update.
80    GameUpdate {
81        /// The players update.
82        players: Vec<PlayerUpdate>,
83        /// The board cards.
84        board: Vec<Card>,
85        /// The pot.
86        pot: Chips,
87    },
88    /// Request action from a player.
89    ActionRequest {
90        /// The player that should respond with an action.
91        player_id: PeerId,
92        /// The minimum raise.
93        min_raise: Chips,
94        /// The current big blind.
95        big_blind: Chips,
96        /// The list of legal actions.
97        actions: Vec<PlayerAction>,
98    },
99    /// Player action response.
100    ActionResponse {
101        /// The action from the player.
102        action: PlayerAction,
103        /// The amount for this action (only used for bet and raise actions)
104        amount: Chips,
105    },
106}
107
108/// A player update details.
109#[derive(Debug, Serialize, Deserialize)]
110pub struct PlayerUpdate {
111    /// The player id.
112    pub player_id: PeerId,
113    /// The player chips.
114    pub chips: Chips,
115    /// The player current bet.
116    pub bet: Chips,
117    /// The last player action.
118    pub action: PlayerAction,
119    /// The player action timer.
120    pub action_timer: Option<u16>,
121    /// The player cards.
122    pub cards: PlayerCards,
123    /// The player has the button.
124    pub has_button: bool,
125    /// The player is active in the hand.
126    pub is_active: bool,
127}
128
129/// A Player action.
130#[derive(Copy, Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
131pub enum PlayerAction {
132    /// No action.
133    None,
134    /// Player pays small blind.
135    SmallBlind,
136    /// Player pays big blind.
137    BigBlind,
138    /// Player calls.
139    Call,
140    /// Player checks.
141    Check,
142    /// Player bets.
143    Bet,
144    /// Player raises.
145    Raise,
146    /// Player folds.
147    Fold,
148}
149
150impl PlayerAction {
151    /// The action label.
152    pub fn label(&self) -> &'static str {
153        match self {
154            PlayerAction::SmallBlind => "SB",
155            PlayerAction::BigBlind => "BB",
156            PlayerAction::Call => "CALL",
157            PlayerAction::Check => "CHECK",
158            PlayerAction::Bet => "BET",
159            PlayerAction::Raise => "RAISE",
160            PlayerAction::Fold => "FOLD",
161            PlayerAction::None => "",
162        }
163    }
164}
165
166/// Hand payoff description.
167#[derive(Clone, Debug, Serialize, Deserialize)]
168pub struct HandPayoff {
169    /// The player receiving the payment.
170    pub player_id: PeerId,
171    /// The payment amount.
172    pub chips: Chips,
173    /// The winning cards.
174    pub cards: Vec<Card>,
175    /// Cards rank description.
176    pub rank: String,
177}
178
179/// A signed message.
180#[derive(Debug, Clone)]
181pub struct SignedMessage {
182    /// Clonable payload for broadcasting to multiple connection tasks.
183    payload: Arc<Payload>,
184}
185
186/// Private signed message payload.
187#[derive(Debug, Serialize, Deserialize)]
188struct Payload {
189    msg: Message,
190    sig: Signature,
191    vk: VerifyingKey,
192}
193
194impl SignedMessage {
195    /// Creates a new signed message.
196    pub fn new(sk: &SigningKey, msg: Message) -> Self {
197        let sig = sk.sign(&msg);
198        Self {
199            payload: Arc::new(Payload {
200                msg,
201                sig,
202                vk: sk.verifying_key(),
203            }),
204        }
205    }
206
207    /// Deserializes this message and verifies its signature.
208    pub fn deserialize_and_verify(buf: &[u8]) -> Result<Self> {
209        let sm = Self {
210            payload: Arc::new(bincode::deserialize::<Payload>(buf)?),
211        };
212
213        if !sm.payload.vk.verify(&sm.payload.msg, &sm.payload.sig) {
214            bail!("Invalid signature");
215        }
216
217        Ok(sm)
218    }
219
220    /// Serializes this message.
221    pub fn serialize(&self) -> Vec<u8> {
222        bincode::serialize(self.payload.as_ref()).expect("Should serialize signed message")
223    }
224
225    /// Returns the identifier of the player who sent this message.
226    pub fn sender(&self) -> PeerId {
227        self.payload.vk.peer_id()
228    }
229
230    /// Extracts the signed message.
231    pub fn message(&self) -> &Message {
232        &self.payload.msg
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn signed_message() {
242        let sk = SigningKey::default();
243        let message = Message::JoinServer {
244            nickname: "Alice".to_string(),
245        };
246
247        let smsg = SignedMessage::new(&sk, message);
248        let bytes = smsg.serialize();
249
250        let deser_msg = SignedMessage::deserialize_and_verify(&bytes).unwrap();
251        assert!(
252            matches!(deser_msg.message(), Message::JoinServer{ nickname } if nickname == "Alice")
253        );
254    }
255}