Skip to main content

api/dto/
user_game.rs

1#[cfg(feature = "server")]
2use std::collections::HashMap;
3
4use serde::{Deserialize, Serialize};
5
6#[cfg(feature = "server")]
7use crate::dto::ScoreDTO;
8use crate::dto::{CardDTO, PeggingDTO, PendingDTO, PhaseDTO, PlayerDTO, PlayerStateDTO, PlaysDTO};
9
10/// A Data Transfer Object representing the full state of a game from the perspective of a user.
11#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
12pub struct UserGameDTO {
13    /// The name of the game.
14    pub name: String,
15
16    /// The current phase of the game.
17    pub phase: PhaseDTO,
18
19    /// Indicates which player(s) are pending an action.
20    pub pending: PendingDTO,
21
22    /// The player who is currently the dealer, if assigned.
23    pub dealer: Option<PlayerDTO>,
24
25    /// The state of the user’s hand, cut (for deal), and score.
26    pub user_state: PlayerStateDTO,
27
28    /// The state of the opponent’s hand, cut (for deal), and score.
29    pub opponent_state: PlayerStateDTO,
30
31    /// The cards currently in the crib.
32    pub crib: Vec<CardDTO>,
33
34    /// The starter cut card, if it has been revealed.
35    pub starter_cut: Option<CardDTO>,
36
37    /// The current and previous plays for the round, if the round has started.
38    pub plays: Option<PlaysDTO>,
39
40    /// A summary of pegging points for each kind of scoring of hands & crib.
41    /// (A Playing phase doesn't have a breakdown until the first scoring play).
42    pub pegging: PeggingDTO,
43
44    /// The winner of the game, if the game has finished.
45    pub winner: Option<PlayerDTO>,
46}
47
48#[cfg(feature = "server")]
49impl UserGameDTO {
50    fn new(name: &str, phase: PhaseDTO) -> Self {
51        Self {
52            name: String::from(name),
53            phase,
54            pending: PendingDTO::Nobody,
55            dealer: None,
56            user_state: PlayerStateDTO::default(),
57            opponent_state: PlayerStateDTO::default(),
58            crib: Vec::default(),
59            starter_cut: None,
60            plays: None,
61            pegging: PeggingDTO::default(),
62            winner: None,
63        }
64    }
65
66    fn with_pending(mut self, pending: PendingDTO) -> Self {
67        self.pending = pending;
68        self
69    }
70
71    fn with_cuts_for_deal(
72        mut self,
73        user_cut: Option<CardDTO>,
74        opponent_cut: Option<CardDTO>,
75    ) -> Self {
76        self.user_state.cut = user_cut;
77        self.opponent_state.cut = opponent_cut;
78        self
79    }
80
81    fn with_hands(mut self, user_hand: Vec<CardDTO>, opponent_hand: Vec<CardDTO>) -> Self {
82        self.user_state.hand = user_hand;
83        self.opponent_state.hand = opponent_hand;
84        self
85    }
86
87    fn with_scores(mut self, user_score: ScoreDTO, opponent_score: ScoreDTO) -> Self {
88        self.user_state.score = user_score;
89        self.opponent_state.score = opponent_score;
90        self
91    }
92
93    fn with_dealer(mut self, dealer: Option<PlayerDTO>) -> Self {
94        self.dealer = dealer;
95        self
96    }
97
98    fn with_crib_and_starter_cut(
99        mut self,
100        cards: Vec<CardDTO>,
101        starter_cut: Option<CardDTO>,
102    ) -> Self {
103        self.crib = cards;
104        self.starter_cut = starter_cut;
105        self
106    }
107
108    fn with_plays(mut self, plays: PlaysDTO) -> Self {
109        self.plays = Some(plays);
110        self
111    }
112
113    fn with_pegging(mut self, pegging: PeggingDTO) -> Self {
114        self.pegging = pegging;
115        self
116    }
117
118    fn with_winner(mut self, winner: PlayerDTO) -> Self {
119        self.winner = Some(winner);
120        self
121    }
122}
123
124#[cfg(feature = "server")]
125mod server_only {
126    use server::domain::{
127        Finished, Game, HasCrib, HasCutsForDeal, HasHands, HasPegging, HasPending, HasPlayState,
128        HasRoles, HasScoreboard, HasStarterCut, PLAYER0, PLAYER1, Phase, Play, Player, Roles,
129        ScoreKind, UserId,
130    };
131
132    use super::*;
133    use crate::dto::{CardIdDTO, PeggingKindDTO, PlayActionDTO};
134
135    fn players(game: &Game, user_id: UserId) -> (Player, Player) {
136        let is_host = game.host() == user_id;
137        if is_host {
138            (PLAYER0, PLAYER1)
139        } else {
140            (PLAYER1, PLAYER0)
141        }
142    }
143
144    fn pending<T: HasPending>(s: &T, p: Player) -> PendingDTO {
145        PendingDTO::new(p, s.pending())
146    }
147
148    fn cut_for_deal<T: HasCutsForDeal>(s: &T, p: Player) -> Option<CardDTO> {
149        s.cut_for_deal(p).map(CardDTO::face_up)
150    }
151
152    fn starter_cut<T: HasStarterCut>(s: &T) -> Option<CardDTO> {
153        Some(CardDTO::face_up(s.starter_cut()))
154    }
155
156    fn dealer_from_roles(
157        roles: &Roles,
158        player_dto_map: &HashMap<Player, PlayerDTO>,
159    ) -> Option<PlayerDTO> {
160        player_dto_map.get(&roles.dealer().player()).cloned()
161    }
162
163    fn dealer_from_maybe_roles(
164        roles: Option<Roles>,
165        player_dto_map: &HashMap<Player, PlayerDTO>,
166    ) -> Option<PlayerDTO> {
167        roles.and_then(|roles| dealer_from_roles(&roles, player_dto_map))
168    }
169
170    fn dealer<T: HasRoles>(
171        s: &T,
172        player_dto_map: &HashMap<Player, PlayerDTO>,
173    ) -> Option<PlayerDTO> {
174        dealer_from_roles(s.roles(), player_dto_map)
175    }
176
177    fn hand_down<T: HasHands>(s: &T, p: Player) -> Vec<CardDTO> {
178        s.hand(p).as_ref().iter().map(CardDTO::face_down).collect()
179    }
180
181    fn hand_up<T: HasHands>(s: &T, p: Player) -> Vec<CardDTO> {
182        let hand = s.hand(p).clone().sorted();
183        hand.into_iter().map(CardDTO::face_up).collect()
184    }
185
186    fn score<T: HasScoreboard>(s: &T, p: Player) -> ScoreDTO {
187        ScoreDTO::from(&s.scoreboard().position(p))
188    }
189
190    fn crib_down<T: HasCrib>(s: &T) -> Vec<CardDTO> {
191        s.crib().as_ref().iter().map(CardDTO::face_down).collect()
192    }
193
194    fn crib_up<T: HasCrib>(s: &T) -> Vec<CardDTO> {
195        let crib = s.crib().clone().sorted();
196        crib.into_iter().map(CardDTO::face_up).collect()
197    }
198
199    fn plays<T: HasPlayState>(s: &T, player_dto_map: &HashMap<Player, PlayerDTO>) -> PlaysDTO {
200        let play_state = s.play_state();
201
202        let (legal_plays, next_action) = {
203            if play_state.is_finished() {
204                (vec![], PlayActionDTO::ScorePone)
205            } else {
206                let next_to_play = play_state.next_to_play();
207                let next_to_play_dto = player_dto_map
208                    .get(&next_to_play)
209                    .expect("next player must be defined");
210
211                let legal_plays = play_state.legal_plays(next_to_play);
212                let legal_play_cids = legal_plays
213                    .iter()
214                    .map(|card| CardIdDTO::from(card.cid()))
215                    .collect::<Vec<_>>();
216
217                let can_play = !legal_plays.is_empty();
218                if can_play {
219                    if next_to_play_dto == &PlayerDTO::User {
220                        (legal_play_cids, PlayActionDTO::Play(PlayerDTO::User))
221                    } else {
222                        (vec![], PlayActionDTO::Play(PlayerDTO::Opponent))
223                    }
224                } else {
225                    (vec![], PlayActionDTO::Go(*next_to_play_dto))
226                }
227            }
228        };
229
230        let plays_to_dto = |plays: &Vec<Play>| {
231            plays
232                .iter()
233                .map(|play| {
234                    (
235                        *player_dto_map.get(&play.player()).expect("valid player"),
236                        CardDTO::face_up(&play.card()),
237                    )
238                })
239                .collect::<Vec<_>>()
240        };
241
242        let current = plays_to_dto(play_state.current_plays());
243        let previous = plays_to_dto(play_state.previous_plays());
244        let running_total = play_state.running_total().value() as u8;
245
246        PlaysDTO {
247            next_action,
248            legal_plays,
249            current,
250            previous,
251            running_total,
252        }
253    }
254
255    fn pegging<T: HasPegging>(s: &T, player_dto_map: &HashMap<Player, PlayerDTO>) -> PeggingDTO {
256        if let Some(pegging) = s.pegging() {
257            let recipient = *player_dto_map
258                .get(pegging.recipient())
259                .expect("valid player");
260
261            let score_items = pegging.score_sheet().items();
262
263            score_items
264                .iter()
265                .fold(PeggingDTO::new(recipient), |mut acc, item| {
266                    let kind = score_kind_to_dto(item.kind());
267
268                    let points = item.points().value();
269                    let cids = item.cards().iter().map(|c| c.cid()).collect::<Vec<_>>();
270                    let entry = acc.breakdown.entry(kind).or_default();
271                    entry.points += points;
272                    entry.breakdown.push(cids);
273
274                    acc
275                })
276        } else {
277            PeggingDTO::default()
278        }
279    }
280
281    fn score_kind_to_dto(kind: ScoreKind) -> PeggingKindDTO {
282        match kind {
283            ScoreKind::Fifteen => PeggingKindDTO::Fifteens,
284            ScoreKind::Pair | ScoreKind::Triplet | ScoreKind::Quadruplet => PeggingKindDTO::Pairs,
285            ScoreKind::Run => PeggingKindDTO::Runs,
286            ScoreKind::Flush => PeggingKindDTO::Flush,
287            ScoreKind::LastCard => PeggingKindDTO::LastCard,
288            ScoreKind::ThirtyOne => PeggingKindDTO::ThirtyOne,
289            ScoreKind::HisHeels => PeggingKindDTO::HisHeels,
290            ScoreKind::Nobs => PeggingKindDTO::Nobs,
291        }
292    }
293
294    fn winner(finished: &Finished, player_dto_map: &HashMap<Player, PlayerDTO>) -> PlayerDTO {
295        *player_dto_map
296            .get(&finished.winner())
297            .expect("valid player")
298    }
299
300    impl From<(UserId, &Game)> for UserGameDTO {
301        fn from((user_id, game): (UserId, &Game)) -> Self {
302            let (me, them) = players(game, user_id);
303            let player_dto_map: HashMap<Player, PlayerDTO> =
304                [(me, PlayerDTO::User), (them, PlayerDTO::Opponent)]
305                    .into_iter()
306                    .collect();
307            let name = game.name();
308
309            match &game.phase() {
310                Phase::Starting(state) if game.guest().is_none() => {
311                    Self::new(name, PhaseDTO::InLobby).with_pending(PendingDTO::Opponent)
312                }
313
314                Phase::Starting(state) => Self::new(name, PhaseDTO::CuttingForDeal)
315                    .with_pending(pending(state, me))
316                    .with_cuts_for_deal(cut_for_deal(state, me), cut_for_deal(state, them))
317                    .with_dealer(dealer_from_maybe_roles(state.roles(), &player_dto_map)),
318
319                Phase::Discarding(state) => UserGameDTO::new(name, PhaseDTO::Discarding)
320                    .with_scores(score(state, me), score(state, them))
321                    .with_dealer(dealer(state, &player_dto_map))
322                    .with_pending(pending(state, me))
323                    .with_hands(hand_up(state, me), hand_down(state, them))
324                    .with_crib_and_starter_cut(crib_down(state), None),
325
326                Phase::Playing(state) => Self::new(name, PhaseDTO::Playing)
327                    .with_scores(score(state, me), score(state, them))
328                    .with_dealer(dealer(state, &player_dto_map))
329                    .with_pending(pending(state, me))
330                    .with_hands(hand_up(state, me), hand_down(state, them))
331                    .with_crib_and_starter_cut(crib_down(state), starter_cut(state))
332                    .with_plays(plays(state, &player_dto_map))
333                    .with_pegging(pegging(state, &player_dto_map)),
334
335                Phase::ScoringPone(state) => {
336                    let they_are_pone = state.pone().player() == them;
337                    UserGameDTO::new(name, PhaseDTO::ScoringPone)
338                        .with_scores(score(state, me), score(state, them))
339                        .with_dealer(dealer(state, &player_dto_map))
340                        .with_pending(pending(state, me))
341                        .with_hands(
342                            hand_up(state, me),
343                            if they_are_pone {
344                                hand_up(state, them)
345                            } else {
346                                hand_down(state, them)
347                            },
348                        )
349                        .with_crib_and_starter_cut(crib_down(state), starter_cut(state))
350                        .with_pegging(pegging(state, &player_dto_map))
351                }
352
353                Phase::ScoringDealer(state) => UserGameDTO::new(name, PhaseDTO::ScoringDealer)
354                    .with_scores(score(state, me), score(state, them))
355                    .with_dealer(dealer(state, &player_dto_map))
356                    .with_pending(pending(state, me))
357                    .with_hands(hand_up(state, me), hand_up(state, them))
358                    .with_crib_and_starter_cut(crib_down(state), starter_cut(state))
359                    .with_pegging(pegging(state, &player_dto_map)),
360
361                Phase::ScoringCrib(state) => UserGameDTO::new(name, PhaseDTO::ScoringCrib)
362                    .with_scores(score(state, me), score(state, them))
363                    .with_dealer(dealer(state, &player_dto_map))
364                    .with_pending(pending(state, me))
365                    .with_hands(hand_up(state, me), hand_up(state, them))
366                    .with_crib_and_starter_cut(crib_up(state), starter_cut(state))
367                    .with_pegging(pegging(state, &player_dto_map)),
368
369                Phase::Finished(state) => UserGameDTO::new(name, PhaseDTO::Finished)
370                    .with_scores(score(state, me), score(state, them))
371                    .with_dealer(dealer(state, &player_dto_map))
372                    .with_hands(hand_up(state, me), hand_up(state, them))
373                    .with_crib_and_starter_cut(crib_up(state), starter_cut(state))
374                    .with_winner(winner(state, &player_dto_map)),
375            }
376        }
377    }
378}