Skip to content

Commit d44c8d2

Browse files
authored
feat: Improve InvalidShoupaiError (#56)
* feat: Merge bingpai-related errors from `InvalidShoupaiError` into the `InvalidBingpai` variant * test: Add unit tests * feat: Merge FuluMianzi-related errors from `InvalidShoupaiError` into the `InvalidFuluMianzi` variant * refactor: Reorder variants of `InvalidShoupaiError` * refactor: Validate if hand is empty only in case of short hand
1 parent ccaf632 commit d44c8d2

File tree

6 files changed

+145
-144
lines changed

6 files changed

+145
-144
lines changed

src/bingpai.rs

+12-8
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,23 @@ use thiserror::Error;
5050
/// ```
5151
pub type Bingpai = [u8; NUM_TILE_INDEX];
5252

53+
/// Errors that occur when an invalid pure hand (純手牌) is provided.
5354
#[derive(Debug, Error)]
54-
pub(crate) enum InvalidBingpaiError {
55+
pub enum InvalidBingpaiError {
56+
/// Same tile count exceeds 4.
5557
#[error("same tile count must be 4 or less but was {0}")]
5658
ExceedsMaxNumSameTile(u8),
59+
/// Total tile count exceeds 14.
5760
#[error("total tile count must be 14 or less but was {0}")]
5861
ExceedsMaxNumBingpai(u8),
59-
#[error("hand is empty")]
62+
/// Pure hand is empty.
63+
#[error("pure hand is empty")]
6064
EmptyBingpai,
65+
/// Total tile count is not a multiple of 3 plus 1 or 2.
6166
#[error("total tile count must be a multiple of 3 plus 1 or 2 but was {0}")]
6267
InvalidNumBingpai(u8),
63-
#[error(
64-
"tile index {0} (must be outside 1 (2m) to 7 (8m)) cannot be used in 3-player mahjong"
65-
)]
68+
/// Contains tiles that cannot be used in 3-player mahjong (2m to 8m).
69+
#[error("tile index {0} cannot be used in 3-player mahjong")]
6670
InvalidTileFor3Player(usize),
6771
}
6872

@@ -83,10 +87,10 @@ impl BingpaiExt for Bingpai {
8387
if num_bingpai > MAX_NUM_SHOUPAI {
8488
return Err(InvalidBingpaiError::ExceedsMaxNumBingpai(num_bingpai));
8589
}
86-
if num_bingpai == 0 {
87-
return Err(InvalidBingpaiError::EmptyBingpai);
88-
}
8990
if num_bingpai % 3 == 0 {
91+
if num_bingpai == 0 {
92+
return Err(InvalidBingpaiError::EmptyBingpai);
93+
}
9094
return Err(InvalidBingpaiError::InvalidNumBingpai(num_bingpai));
9195
}
9296

src/calculate.rs

+76-2
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ pub fn calculate_replacement_number_3_player(
203203
#[cfg(test)]
204204
mod tests {
205205
use super::*;
206+
use crate::bingpai::InvalidBingpaiError;
206207

207208
#[test]
208209
fn calculate_replacement_number_standard_tenpai() {
@@ -251,7 +252,52 @@ mod tests {
251252
let replacement_number = calculate_replacement_number(&bingpai, &None);
252253
assert!(matches!(
253254
replacement_number.unwrap_err(),
254-
InvalidShoupaiError::EmptyShoupai
255+
InvalidShoupaiError::InvalidBingpai(InvalidBingpaiError::EmptyBingpai)
256+
));
257+
}
258+
259+
#[test]
260+
fn calculate_replacement_number_too_many_tiles() {
261+
let bingpai: Bingpai = [
262+
1, 1, 1, 1, 0, 0, 0, 0, 0, // m
263+
1, 1, 1, 1, 0, 0, 0, 0, 0, // p
264+
1, 1, 1, 1, 0, 0, 0, 0, 0, // s
265+
1, 1, 1, 0, 0, 0, 0, // z
266+
];
267+
let replacement_number = calculate_replacement_number(&bingpai, &None);
268+
assert!(matches!(
269+
replacement_number.unwrap_err(),
270+
InvalidShoupaiError::InvalidBingpai(InvalidBingpaiError::ExceedsMaxNumBingpai(15))
271+
));
272+
}
273+
274+
#[test]
275+
fn calculate_replacement_number_5th_tile() {
276+
let bingpai: Bingpai = [
277+
5, 0, 0, 0, 0, 0, 0, 0, 0, // m
278+
1, 1, 1, 1, 0, 0, 0, 0, 0, // p
279+
1, 1, 1, 1, 0, 0, 0, 0, 0, // s
280+
1, 0, 0, 0, 0, 0, 0, // z
281+
];
282+
let replacement_number = calculate_replacement_number(&bingpai, &None);
283+
assert!(matches!(
284+
replacement_number.unwrap_err(),
285+
InvalidShoupaiError::InvalidBingpai(InvalidBingpaiError::ExceedsMaxNumSameTile(5))
286+
));
287+
}
288+
289+
#[test]
290+
fn calculate_replacement_number_incomplete_hand() {
291+
let bingpai: Bingpai = [
292+
4, 4, 4, 0, 0, 0, 0, 0, 0, // m
293+
0, 0, 0, 0, 0, 0, 0, 0, 0, // p
294+
0, 0, 0, 0, 0, 0, 0, 0, 0, // s
295+
0, 0, 0, 0, 0, 0, 0, // z
296+
];
297+
let replacement_number = calculate_replacement_number(&bingpai, &None);
298+
assert!(matches!(
299+
replacement_number.unwrap_err(),
300+
InvalidShoupaiError::InvalidBingpai(InvalidBingpaiError::InvalidNumBingpai(12))
255301
));
256302
}
257303

@@ -302,7 +348,35 @@ mod tests {
302348
let replacement_number = calculate_replacement_number_3_player(&bingpai, &None);
303349
assert!(matches!(
304350
replacement_number.unwrap_err(),
305-
InvalidShoupaiError::EmptyShoupai
351+
InvalidShoupaiError::InvalidBingpai(InvalidBingpaiError::EmptyBingpai)
352+
));
353+
}
354+
355+
#[test]
356+
fn calculate_replacement_number_3_player_2m_8m() {
357+
let bingpai_2m: Bingpai = [
358+
0, 1, 0, 0, 0, 0, 0, 0, 0, // m
359+
0, 0, 0, 0, 0, 0, 0, 0, 0, // p
360+
0, 0, 0, 0, 0, 0, 0, 0, 0, // s
361+
0, 0, 0, 0, 0, 0, 0, // z
362+
];
363+
let bingpai_8m: Bingpai = [
364+
0, 0, 0, 0, 0, 0, 0, 1, 0, // m
365+
0, 0, 0, 0, 0, 0, 0, 0, 0, // p
366+
0, 0, 0, 0, 0, 0, 0, 0, 0, // s
367+
0, 0, 0, 0, 0, 0, 0, // z
368+
];
369+
370+
let replacement_number_2m = calculate_replacement_number_3_player(&bingpai_2m, &None);
371+
let replacement_number_8m = calculate_replacement_number_3_player(&bingpai_8m, &None);
372+
373+
assert!(matches!(
374+
replacement_number_2m.unwrap_err(),
375+
InvalidShoupaiError::InvalidBingpai(InvalidBingpaiError::InvalidTileFor3Player(1))
376+
));
377+
assert!(matches!(
378+
replacement_number_8m.unwrap_err(),
379+
InvalidShoupaiError::InvalidBingpai(InvalidBingpaiError::InvalidTileFor3Player(7))
306380
));
307381
}
308382
}

src/fulu_mianzi.rs

+22
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ pub enum InvalidFuluMianziError {
103103
/// The tile and position combination cannot form a valid sequence.
104104
#[error("a sequence cannot be made with {0} and {1:?}")]
105105
InvalidShunziCombination(Tile, ClaimedTilePosition),
106+
/// This meld cannot be used in 3-player mahjong (2m to 8m or sequence).
107+
#[error("{0} cannot be used in 3-player mahjong")]
108+
InvalidFuluMianziFor3Player(FuluMianzi),
106109
}
107110

108111
impl FuluMianzi {
@@ -132,6 +135,25 @@ impl FuluMianzi {
132135
}
133136
}
134137

138+
pub(crate) fn validate_3_player(&self) -> Result<(), InvalidFuluMianziError> {
139+
match self {
140+
FuluMianzi::Shunzi(_, _) => {
141+
return Err(InvalidFuluMianziError::InvalidFuluMianziFor3Player(
142+
self.clone(),
143+
));
144+
}
145+
FuluMianzi::Kezi(t) | FuluMianzi::Gangzi(t) => {
146+
if (1..8).contains(t) {
147+
return Err(InvalidFuluMianziError::InvalidFuluMianziFor3Player(
148+
self.clone(),
149+
));
150+
}
151+
}
152+
}
153+
154+
self.validate()
155+
}
156+
135157
#[inline]
136158
fn is_valid_shunzi_combination(tile: &Tile, position: &ClaimedTilePosition) -> bool {
137159
match position {

src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ mod shoupai;
5050
mod standard;
5151

5252
#[cfg(not(feature = "build-file"))]
53-
pub use bingpai::Bingpai;
53+
pub use bingpai::{Bingpai, InvalidBingpaiError};
5454
#[cfg(not(feature = "build-file"))]
5555
pub use calculate::{calculate_replacement_number, calculate_replacement_number_3_player};
5656
#[cfg(not(feature = "build-file"))]

0 commit comments

Comments
 (0)