Skip to content

Commit bf317f8

Browse files
authored
Merge pull request #63 from Apricot-S/develop
v3.0.0
2 parents ccaf632 + ca74d87 commit bf317f8

12 files changed

+174
-169
lines changed

Cargo.toml

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "xiangting"
33
description = "A library for calculating the deficiency number (a.k.a. xiangting number, 向聴数)."
4-
version = "2.0.6"
4+
version = "3.0.0"
55
authors = ["Apricot S."]
66
edition = "2021"
77
license = "MIT"
@@ -15,16 +15,16 @@ name = "xiangting"
1515
path = "src/lib.rs"
1616

1717
[dependencies]
18-
thiserror = "2.0.0"
18+
thiserror = "2.0.11"
1919

2020
[dev-dependencies]
2121
criterion = "0.5.1"
22-
cxx = "1.0.128"
23-
mt19937 = "2.0.1"
24-
rand = "0.8.5"
22+
cxx = "1.0.137"
23+
mt19937 = "3.1.0"
24+
rand = "0.9.0"
2525

2626
[build-dependencies]
27-
cxx-build = { version = "1.0.128", optional = true }
27+
cxx-build = { version = "1.0.137", optional = true }
2828

2929
[features]
3030
build-file = []

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
A library for calculating the deficiency number (a.k.a. xiangting number, 向聴数).
44

5-
This library is based on the algorithm in [Nyanten](https://github.com/Cryolite/nyanten).
5+
This library is based on the algorithm in [Cryolite's Nyanten](https://github.com/Cryolite/nyanten).
66
However, it introduces the following additional features:
77

88
- Supports rules that include and exclude melded tiles when determining if a hand contains four identical tiles.

benches/random_hand.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// This file is part of https://github.com/Apricot-S/xiangting
44

55
use mt19937::MT19937;
6-
use rand::seq::SliceRandom;
6+
use rand::seq::{IndexedRandom, SliceRandom};
77
use rand::{Rng, SeedableRng};
88

99
pub fn create_rng() -> MT19937 {

scripts/build_map.sh

-7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
#!/usr/bin/env bash
22

3-
start_time=$(date +%s)
4-
53
cargo run --bin build-map --release --features build-map -- \
64
src/standard/shupai_map.rs \
75
src/standard/zipai_map.rs \
86
src/standard/wanzi_19_map.rs
9-
10-
end_time=$(date +%s)
11-
execution_time=$((end_time - start_time))
12-
13-
echo "Execution time: $execution_time seconds"

src/bin/build_map/main.rs

+5
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ fn dump_map<const N: usize>(map: &Map, map_path: &Path) -> io::Result<()> {
175175
}
176176

177177
fn main() {
178+
let start = std::time::Instant::now();
179+
178180
let args: Vec<String> = env::args().collect();
179181
if args.len() != 4 {
180182
eprintln!(
@@ -214,4 +216,7 @@ fn main() {
214216

215217
dump_map::<2>(&wanzi_19_map, wanzi_19_map_path).expect("Failed to dump wanzi 19 map");
216218
}
219+
220+
let elapsed_time = start.elapsed();
221+
println!("elapsed time: {:?}", elapsed_time);
217222
}

src/bin/build_map/replacement_number.rs

+15-9
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
// https://github.com/gimite/MjaiClients/blob/master/src/org/ymatsux/mjai/client/ShantensuUtil.java
77
// https://github.com/gimite/mjai-manue/blob/master/coffee/shanten_analysis.coffee
88

9+
#![allow(clippy::too_many_arguments)]
10+
11+
use std::cmp::Ordering;
12+
913
const NUM_SHUPAI_IDS: usize = 9;
1014
const NUM_ZIPAI_IDS: usize = 7;
11-
1215
// 1-7{m,p,s}
1316
const SEQUENCE_IDS: [usize; 7] = [0, 1, 2, 3, 4, 5, 6];
1417

@@ -33,12 +36,16 @@ fn update_upperbound_and_necessary_tiles_0_pair<const N: usize>(
3336
upperbound: &mut u8,
3437
necessary_tiles: &mut u16,
3538
) {
36-
if current_distance < *upperbound {
37-
*upperbound = current_distance;
38-
*current_necessary_tiles = 0;
39-
*necessary_tiles = get_necessary_tiles(hand, winning_hand);
40-
} else if current_distance == *upperbound {
41-
*necessary_tiles |= get_necessary_tiles(hand, winning_hand);
39+
match current_distance.cmp(upperbound) {
40+
Ordering::Less => {
41+
*upperbound = current_distance;
42+
*current_necessary_tiles = 0;
43+
*necessary_tiles = get_necessary_tiles(hand, winning_hand);
44+
}
45+
Ordering::Equal => {
46+
*necessary_tiles |= get_necessary_tiles(hand, winning_hand);
47+
}
48+
Ordering::Greater => {}
4249
}
4350
}
4451

@@ -149,8 +156,7 @@ pub(super) fn get_shupai_replacement_number(
149156
// Add sequences
150157
let start_sequence_id = min_meld_id.saturating_sub(NUM_SHUPAI_IDS);
151158

152-
for sequence_id in start_sequence_id..SEQUENCE_IDS.len() {
153-
let i = SEQUENCE_IDS[sequence_id];
159+
for (sequence_id, &i) in SEQUENCE_IDS.iter().enumerate().skip(start_sequence_id) {
154160
if winning_hand[i..=i + 2].iter().any(|&c| c == 4) {
155161
// Can't add a sequence
156162
continue;

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
//! A library for calculating the deficiency number (a.k.a. xiangting number, 向聴数).
88
//!
9-
//! This library is based on the algorithm in [Nyanten](https://github.com/Cryolite/nyanten).
9+
//! This library is based on the algorithm in [Cryolite's Nyanten](https://github.com/Cryolite/nyanten).
1010
//! However, it introduces the following additional features:
1111
//!
1212
//! - Supports rules that include and exclude melded tiles when determining if a hand contains four identical tiles.
@@ -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)