mahjong_core/
score.rs

1// http://mahjongtime.com/hong-kong-mahjong-scoring.html
2// https://en.wikipedia.org/wiki/Hong_Kong_mahjong_scoring_rules
3
4use crate::{
5    deck::DEFAULT_DECK, meld::MeldType, Flower, Game, PlayerId, Season, Tile, FLOWERS_ORDER,
6    SEASONS_ORDER, WINDS_ROUND_ORDER,
7};
8use rustc_hash::{FxHashMap, FxHashSet};
9use serde::{Deserialize, Serialize};
10use strum_macros::EnumIter;
11use ts_rs::TS;
12
13pub type ScoreItem = u32;
14pub type ScoreMap = FxHashMap<PlayerId, ScoreItem>;
15
16#[derive(Clone, Debug, Serialize, Deserialize, TS)]
17pub struct Score(pub ScoreMap);
18
19// Proxied
20impl Score {
21    pub fn get(&self, player_id: &PlayerId) -> Option<&ScoreItem> {
22        self.0.get(player_id)
23    }
24
25    pub fn iter(&self) -> impl Iterator<Item = (&PlayerId, &ScoreItem)> {
26        self.0.iter()
27    }
28}
29
30// Proxied
31impl Score {
32    pub fn insert(&mut self, player_id: impl AsRef<str>, score: ScoreItem) {
33        self.0.insert(player_id.as_ref().to_string(), score);
34    }
35
36    pub fn remove(&mut self, player_id: &PlayerId) -> ScoreItem {
37        self.0.remove(player_id).unwrap()
38    }
39}
40
41impl Score {
42    pub fn new(players: &Vec<PlayerId>) -> Self {
43        let mut score = ScoreMap::default();
44
45        for player_id in players {
46            score.insert(player_id.clone(), 0);
47        }
48
49        Self(score)
50    }
51}
52
53#[derive(Clone, Debug, PartialEq, Eq, EnumIter)]
54pub enum ScoringRule {
55    AllFlowers,
56    AllInTriplets,
57    AllSeasons,
58    BasePoint, // This is a custom rule until all other rules are implemented
59    CommonHand,
60    GreatDragons,
61    LastWallTile,
62    NoFlowersSeasons,
63    SeatFlower,
64    SeatSeason,
65    SelfDraw,
66    Purity,
67}
68
69impl Game {
70    fn get_scoring_rules_points(scoring_rules: &Vec<ScoringRule>) -> u32 {
71        enum ScoringRulePoints {
72            Addition(u32),
73            Multiplier(u32),
74        }
75        let mut round_points = 0;
76        let mut points: Vec<ScoringRulePoints> = vec![];
77
78        for rule in scoring_rules {
79            points.push(match rule {
80                ScoringRule::AllFlowers => ScoringRulePoints::Addition(2),
81                ScoringRule::AllInTriplets => ScoringRulePoints::Addition(3),
82                ScoringRule::AllSeasons => ScoringRulePoints::Addition(2),
83                ScoringRule::BasePoint => ScoringRulePoints::Addition(1),
84                ScoringRule::CommonHand => ScoringRulePoints::Addition(1),
85                ScoringRule::GreatDragons => ScoringRulePoints::Addition(8),
86                ScoringRule::LastWallTile => ScoringRulePoints::Addition(1),
87                ScoringRule::NoFlowersSeasons => ScoringRulePoints::Addition(1),
88                ScoringRule::SeatFlower => ScoringRulePoints::Addition(1),
89                ScoringRule::SeatSeason => ScoringRulePoints::Addition(1),
90                ScoringRule::SelfDraw => ScoringRulePoints::Addition(1),
91                ScoringRule::Purity => ScoringRulePoints::Multiplier(6),
92            });
93        }
94
95        round_points += points
96            .iter()
97            .filter_map(|point| {
98                if let ScoringRulePoints::Addition(value) = point {
99                    Some(*value)
100                } else {
101                    None
102                }
103            })
104            .sum::<u32>();
105        let multiplier = points
106            .iter()
107            .filter_map(|point| {
108                if let ScoringRulePoints::Multiplier(value) = point {
109                    Some(*value)
110                } else {
111                    None
112                }
113            })
114            .product::<u32>();
115        round_points *= multiplier.max(1);
116
117        round_points
118    }
119
120    fn get_scoring_rules(&self, winner_player: &PlayerId) -> Vec<ScoringRule> {
121        let mut rules = Vec::new();
122        rules.push(ScoringRule::BasePoint);
123        let empty_bonus = vec![];
124        let winner_hand = self.table.hands.0.get(winner_player).unwrap();
125        let winner_melds = winner_hand.get_melds();
126        let melds_without_pair = winner_melds
127            .melds
128            .iter()
129            .filter(|meld| meld.meld_type != MeldType::Pair)
130            .collect::<Vec<_>>();
131
132        let winner_bonus = self
133            .table
134            .bonus_tiles
135            .0
136            .get(winner_player)
137            .unwrap_or(&empty_bonus);
138
139        if winner_melds.melds.iter().all(|meld| {
140            let tile = &DEFAULT_DECK.0[meld.tiles[0]];
141
142            matches!(tile, Tile::Suit(_))
143        }) {
144            rules.push(ScoringRule::Purity);
145        }
146
147        if melds_without_pair
148            .iter()
149            .all(|meld| meld.meld_type == MeldType::Chow)
150        {
151            rules.push(ScoringRule::CommonHand);
152        }
153
154        if melds_without_pair
155            .iter()
156            .all(|meld| meld.meld_type == MeldType::Pung || meld.meld_type == MeldType::Kong)
157        {
158            rules.push(ScoringRule::AllInTriplets);
159        }
160
161        if melds_without_pair
162            .iter()
163            .filter(|meld| {
164                if meld.meld_type == MeldType::Chow {
165                    return false;
166                }
167
168                let tile = &DEFAULT_DECK.0[meld.tiles[0]];
169
170                matches!(tile, Tile::Dragon(_))
171            })
172            .count()
173            == 3
174        {
175            rules.push(ScoringRule::GreatDragons);
176        }
177
178        if self.table.draw_wall.is_empty() {
179            rules.push(ScoringRule::LastWallTile);
180        }
181
182        if self.round.tile_claimed.is_none() {
183            rules.push(ScoringRule::SelfDraw);
184        }
185
186        let mut flowers: FxHashSet<Flower> = FxHashSet::default();
187        let mut seasons: FxHashSet<Season> = FxHashSet::default();
188
189        for tile_id in winner_bonus {
190            let tile = &DEFAULT_DECK.0[*tile_id];
191            match tile {
192                Tile::Flower(flower) => {
193                    flowers.insert(flower.value.clone());
194                }
195                Tile::Season(season) => {
196                    seasons.insert(season.value.clone());
197                }
198                _ => {}
199            }
200        }
201
202        if flowers.is_empty() && seasons.is_empty() {
203            rules.push(ScoringRule::NoFlowersSeasons);
204        } else {
205            if flowers.len() == 4 {
206                rules.push(ScoringRule::AllFlowers);
207            }
208
209            if seasons.len() == 4 {
210                rules.push(ScoringRule::AllSeasons);
211            }
212
213            let player_wind = self.round.get_player_wind(&self.players.0, winner_player);
214            let has_seat_flower = flowers.iter().any(|flower| {
215                let flower_index = FLOWERS_ORDER.iter().position(|f| f == flower).unwrap();
216                WINDS_ROUND_ORDER[flower_index] == player_wind
217            });
218            let has_seat_season = seasons.iter().any(|season| {
219                let season_index = SEASONS_ORDER.iter().position(|s| s == season).unwrap();
220                WINDS_ROUND_ORDER[season_index] == player_wind
221            });
222
223            if has_seat_flower {
224                rules.push(ScoringRule::SeatFlower);
225            }
226
227            if has_seat_season {
228                rules.push(ScoringRule::SeatSeason);
229            }
230        }
231
232        rules
233    }
234}
235
236impl Game {
237    pub fn calculate_hand_score(&mut self, winner_player: &PlayerId) -> (Vec<ScoringRule>, u32) {
238        {
239            let score = &mut self.score;
240            let current_player_score = score.get(winner_player);
241            if current_player_score.is_none() {
242                return (vec![], 0);
243            }
244        }
245
246        let scoring_rules = self.get_scoring_rules(winner_player);
247        let round_points = Self::get_scoring_rules_points(&scoring_rules);
248
249        let current_player_score = self.score.get(winner_player).unwrap();
250
251        self.score
252            .insert(winner_player, current_player_score + round_points);
253
254        (scoring_rules, round_points)
255    }
256}