mahjong_core/round/
mod.rs

1pub use self::decide_dealer::{DecideDealerWinds, SetInitialWindsError};
2use crate::{
3    game::GameStyle, macros::derive_game_common, Game, GamePhase, Hands, PlayerId, TileId, Wind,
4    WINDS_ROUND_ORDER,
5};
6use serde::{Deserialize, Serialize};
7use strum_macros::EnumIter;
8use ts_rs::TS;
9
10mod decide_dealer;
11
12#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, TS)]
13#[ts(export)]
14pub struct RoundTileClaimed {
15    pub by: Option<PlayerId>,
16    pub from: PlayerId,
17    pub id: TileId,
18}
19
20pub type TileClaimed = Option<RoundTileClaimed>;
21
22derive_game_common! {
23#[derive(TS)]
24#[ts(export)]
25pub struct Round {
26    pub consecutive_same_seats: usize,
27    pub dealer_player_index: usize,
28    pub player_index: usize,
29    pub east_player_index: usize,
30    pub round_index: u32,
31    #[serde(skip)]
32    pub style: GameStyle,
33    pub tile_claimed: TileClaimed,
34    pub wall_tile_drawn: Option<TileId>,
35    pub wind: Wind,
36    pub initial_winds: Option<u8>,
37}}
38
39#[derive(Debug, EnumIter, Eq, PartialEq, Clone)]
40pub enum NextTurnError {
41    StuckWallTileNotDrawn,
42    StuckHandNotReady(PlayerId),
43}
44
45impl Round {
46    pub fn new(game_style: &GameStyle) -> Self {
47        // This assumes that the players array is sorted
48        Self {
49            consecutive_same_seats: 0,
50            dealer_player_index: 0,
51            player_index: 0,
52            round_index: 0,
53            style: game_style.clone(),
54            tile_claimed: None,
55            wall_tile_drawn: None,
56            wind: Wind::East,
57            east_player_index: 0,
58            initial_winds: None,
59        }
60    }
61
62    pub fn get_claimable_tile(&self, player_id: &PlayerId) -> Option<TileId> {
63        let tile_claimed = self.tile_claimed.clone()?;
64
65        if tile_claimed.by.is_some() || tile_claimed.from == *player_id {
66            return None;
67        }
68
69        Some(tile_claimed.id)
70    }
71
72    pub fn get_initial_winds_slice(&self) -> DecideDealerWinds {
73        DecideDealerWinds::from_number(self.initial_winds)
74    }
75
76    pub fn get_player_wind(&self, players: &[PlayerId], player_id: &PlayerId) -> Wind {
77        let player_index = players.iter().position(|p| p == player_id).unwrap();
78        let wind_index = (player_index + 4 - self.east_player_index) % 4;
79        WINDS_ROUND_ORDER[wind_index].clone()
80    }
81}
82
83impl Round {
84    pub fn next_turn(&mut self, hands: &Hands) -> Result<(), NextTurnError> {
85        if self.wall_tile_drawn.is_none() {
86            return Err(NextTurnError::StuckWallTileNotDrawn);
87        }
88
89        let expected_tiles = hands.get_style().tiles_after_claim() - 1;
90
91        for hand_player in hands.0.keys() {
92            let hand = hands.get(hand_player);
93            if hand.is_none() {
94                return Err(NextTurnError::StuckHandNotReady(hand_player.clone()));
95            }
96            let hand = hand.unwrap();
97            if hand.len() != expected_tiles {
98                return Err(NextTurnError::StuckHandNotReady(hand_player.clone()));
99            }
100        }
101
102        self.wall_tile_drawn = None;
103        self.tile_claimed = None;
104
105        self.player_index += 1;
106        if self.player_index == Game::get_players_num(&self.style) {
107            self.player_index = 0;
108        }
109
110        Ok(())
111    }
112
113    fn common_next_round(&mut self, phase: &mut GamePhase) {
114        let mut current_wind_index = WINDS_ROUND_ORDER
115            .iter()
116            .position(|r| r == &self.wind)
117            .unwrap();
118
119        self.consecutive_same_seats = 0;
120        self.dealer_player_index += 1;
121        if self.dealer_player_index == Game::get_players_num(&self.style) {
122            self.dealer_player_index = 0;
123        }
124
125        if self.dealer_player_index == self.east_player_index {
126            current_wind_index += 1;
127
128            if current_wind_index == WINDS_ROUND_ORDER.len() {
129                *phase = GamePhase::End;
130                return;
131            }
132
133            self.wind = WINDS_ROUND_ORDER.get(current_wind_index).unwrap().clone();
134        }
135
136        self.player_index = self.dealer_player_index;
137    }
138
139    pub fn move_after_win(&mut self, phase: &mut GamePhase, winner_player_index: usize) {
140        self.wall_tile_drawn = None;
141        self.tile_claimed = None;
142        self.round_index += 1;
143
144        let max_consecutive_same_seats = self.style.max_consecutive_same_seats();
145
146        if winner_player_index == self.dealer_player_index
147            && self.consecutive_same_seats < max_consecutive_same_seats
148        {
149            self.player_index = self.dealer_player_index;
150            self.consecutive_same_seats += 1;
151            return;
152        }
153
154        self.common_next_round(phase)
155    }
156
157    pub fn move_after_draw(&mut self, phase: &mut GamePhase) {
158        self.wall_tile_drawn = None;
159        self.tile_claimed = None;
160        self.round_index += 1;
161
162        let max_consecutive_same_seats = self.style.max_consecutive_same_seats();
163
164        if self.consecutive_same_seats < max_consecutive_same_seats {
165            self.player_index = self.dealer_player_index;
166            self.consecutive_same_seats += 1;
167            return;
168        }
169
170        self.common_next_round(phase)
171    }
172
173    pub fn set_initial_winds(
174        &mut self,
175        winds: Option<[Wind; 4]>,
176    ) -> Result<(), SetInitialWindsError> {
177        let winds = DecideDealerWinds::new(winds)?;
178        self.initial_winds = winds.to_number();
179        Ok(())
180    }
181}