tenhou_json/
score.rs

1use std::fmt;
2
3pub struct InvalidRankedScoreError;
4
5#[derive(Debug, Clone, Copy, PartialEq)]
6pub enum ScoreRank {
7    Normal { fu: u8, han: u8 },
8    Mangan,
9    Haneman,
10    Baiman,
11    Sanbaiman,
12    Yakuman, // includes kazoe yakuman
13}
14
15impl Default for ScoreRank {
16    fn default() -> Self {
17        ScoreRank::Normal { fu: 0, han: 0 }
18    }
19}
20
21const RANKS: [(&str, ScoreRank); 5] = [("満貫", ScoreRank::Mangan), ("跳満", ScoreRank::Haneman), ("倍満", ScoreRank::Baiman), ("三倍満", ScoreRank::Sanbaiman), ("役満", ScoreRank::Yakuman)];
22
23#[derive(Debug, PartialEq)]
24pub enum Score {
25    OyaTsumo(i32),
26    KoTsumo(i32, i32), // (non-dealer, dealer)
27    Ron(i32),
28}
29
30impl Default for Score {
31    fn default() -> Self {
32        Score::Ron(0)
33    }
34}
35
36#[derive(Debug, PartialEq, Default)]
37pub struct RankedScore {
38    pub rank: ScoreRank,
39    pub score: Score,
40}
41
42impl fmt::Display for ScoreRank {
43    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
44        match self {
45            ScoreRank::Normal { fu, han } => write!(f, "{}符{}飜", fu, han),
46            ScoreRank::Mangan => write!(f, "満貫"),
47            ScoreRank::Haneman => write!(f, "跳満"),
48            ScoreRank::Baiman => write!(f, "倍満"),
49            ScoreRank::Sanbaiman => write!(f, "三倍満"),
50            ScoreRank::Yakuman => write!(f, "役満"),
51        }
52    }
53}
54
55impl fmt::Display for Score {
56    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
57        match self {
58            Score::OyaTsumo(x) => write!(f, "{}点∀", x),
59            Score::KoTsumo(ko, oya) => write!(f, "{}-{}点", ko, oya),
60            Score::Ron(x) => write!(f, "{}点", x),
61        }
62    }
63}
64
65impl fmt::Display for RankedScore {
66    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
67        write!(f, "{}{}", self.rank, self.score)
68    }
69}
70
71impl std::str::FromStr for RankedScore {
72    type Err = InvalidRankedScoreError;
73
74    fn from_str(s: &str) -> Result<Self, Self::Err> {
75        parse_exact_ranked_score(s).ok_or(InvalidRankedScoreError)
76    }
77}
78
79fn parse_number<T: std::str::FromStr>(it: &mut std::str::Chars) -> Option<T> {
80    let mut num = String::new();
81    while let Some(ch) = it.clone().next() {
82        if ch.is_ascii_digit() {
83            num.push(ch);
84            it.next(); // consume
85        } else {
86            break;
87        }
88    }
89    num.parse().ok()
90}
91
92fn parse_symbol(it: &mut std::str::Chars, symbol: &str) -> bool {
93    let mut tmp = it.clone();
94    for expected in symbol.chars() {
95        if tmp.next() != Some(expected) {
96            return false;
97        }
98    }
99    *it = tmp; // consume
100    true
101}
102
103fn parse_rank_normal(it: &mut std::str::Chars) -> Option<ScoreRank> {
104    let mut tmp = it.clone();
105
106    let fu = parse_number(&mut tmp)?;
107    if !parse_symbol(&mut tmp, "符") {
108        return None;
109    }
110
111    let han = parse_number(&mut tmp)?;
112    if !parse_symbol(&mut tmp, "飜") {
113        return None;
114    }
115
116    *it = tmp; // consume
117    Some(ScoreRank::Normal { fu, han })
118}
119
120fn parse_rank_mangan(it: &mut std::str::Chars) -> Option<ScoreRank> {
121    let mut tmp = it.clone();
122
123    for (rank_str, rank) in &RANKS {
124        if parse_symbol(&mut tmp, rank_str) {
125            *it = tmp; // consume
126            return Some(*rank);
127        }
128    }
129    None
130}
131
132fn parse_rank(it: &mut std::str::Chars) -> Option<ScoreRank> {
133    let mut tmp = it.clone();
134
135    if let Some(x) = parse_rank_normal(&mut tmp) {
136        *it = tmp; // consume
137        return Some(x);
138    }
139
140    if let Some(x) = parse_rank_mangan(&mut tmp) {
141        *it = tmp; // consume
142        return Some(x);
143    }
144
145    None
146}
147
148fn parse_score(it: &mut std::str::Chars) -> Option<Score> {
149    let mut tmp = it.clone();
150
151    let num = parse_number(&mut tmp)?;
152    if parse_symbol(&mut tmp, "-") {
153        // non-dealer tsumo
154        let num2 = parse_number(&mut tmp)?;
155        if !parse_symbol(&mut tmp, "点") {
156            return None;
157        }
158
159        *it = tmp; // consume
160        Some(Score::KoTsumo(num, num2))
161    } else if parse_symbol(&mut tmp, "点") {
162        if parse_symbol(&mut tmp, "∀") {
163            // dealer tsumo
164            *it = tmp; // consume
165            Some(Score::OyaTsumo(num))
166        } else {
167            // ron
168            *it = tmp; // consume
169            Some(Score::Ron(num))
170        }
171    } else {
172        None
173    }
174}
175
176fn parse_ranked_score(it: &mut std::str::Chars) -> Option<RankedScore> {
177    Some(RankedScore {
178        rank: parse_rank(it)?,
179        score: parse_score(it)?,
180    })
181}
182
183fn parse_exact_ranked_score(s: &str) -> Option<RankedScore> {
184    let mut it = s.chars();
185    let ret = parse_ranked_score(&mut it)?;
186    if it.next().is_none() {
187        Some(ret)
188    } else {
189        None
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use crate::score::*;
196
197    #[test]
198    fn test_parse_ron() {
199        assert_eq!(
200            parse_exact_ranked_score("40符3飜7700点"),
201            Some(RankedScore {
202                rank: ScoreRank::Normal { fu: 40, han: 3 },
203                score: Score::Ron(7700)
204            })
205        );
206        assert_eq!(
207            parse_exact_ranked_score("満貫8000点"),
208            Some(RankedScore {
209                rank: ScoreRank::Mangan,
210                score: Score::Ron(8000)
211            })
212        );
213        assert_eq!(parse_exact_ranked_score("40符3飜7700点 "), None);
214    }
215
216    #[test]
217    fn test_parse_ko_tsumo() {
218        assert_eq!(
219            parse_exact_ranked_score("30符3飜1000-2000点"),
220            Some(RankedScore {
221                rank: ScoreRank::Normal { fu: 30, han: 3 },
222                score: Score::KoTsumo(1000, 2000)
223            })
224        );
225        assert_eq!(
226            parse_exact_ranked_score("跳満3000-6000点"),
227            Some(RankedScore {
228                rank: ScoreRank::Haneman,
229                score: Score::KoTsumo(3000, 6000)
230            })
231        );
232        assert_eq!(parse_exact_ranked_score("30符3飜1000-2000点 "), None);
233    }
234
235    #[test]
236    fn test_parse_oya_tsumo() {
237        assert_eq!(
238            parse_exact_ranked_score("30符3飜2000点∀"),
239            Some(RankedScore {
240                rank: ScoreRank::Normal { fu: 30, han: 3 },
241                score: Score::OyaTsumo(2000)
242            })
243        );
244        assert_eq!(
245            parse_exact_ranked_score("満貫4000点∀"),
246            Some(RankedScore {
247                rank: ScoreRank::Mangan,
248                score: Score::OyaTsumo(4000)
249            })
250        );
251        assert_eq!(parse_exact_ranked_score("30符3飜2000点∀ "), None);
252    }
253}