Skip to content

Commit 9a5508c

Browse files
authored
Merge pull request #122 from Apricot-S/develop
v5.0.0
2 parents a196044 + 6e74e65 commit 9a5508c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+3921
-2051
lines changed

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
runs-on: ubuntu-latest
1212

1313
steps:
14-
- uses: actions/checkout@v4
14+
- uses: actions/checkout@v6
1515

1616
- name: Install Rust
1717
uses: dtolnay/rust-toolchain@stable

.github/workflows/rust.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
runs-on: ubuntu-latest
1616

1717
steps:
18-
- uses: actions/checkout@v4
18+
- uses: actions/checkout@v6
1919
- name: Build
2020
run: cargo build --verbose
2121
- name: Run tests
@@ -25,12 +25,12 @@ jobs:
2525
runs-on: ubuntu-latest
2626
needs: build
2727
steps:
28-
- uses: actions/checkout@v4
28+
- uses: actions/checkout@v6
2929
- name: Run doc
3030
run: cargo doc --no-deps
3131

3232
- name: Deploy
33-
uses: actions/upload-pages-artifact@v3
33+
uses: actions/upload-pages-artifact@v4
3434
with:
3535
path: target/doc
3636

Cargo.toml

Lines changed: 1 addition & 1 deletion
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 = "4.0.0"
4+
version = "5.0.0"
55
authors = ["Apricot S."]
66
edition = "2024"
77
rust-version = "1.85"

README.md

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
# xiangting
22

33
[![Crate](https://img.shields.io/crates/v/xiangting.svg)](https://crates.io/crates/xiangting)
4+
[![Minimum Supported Rust Version](https://img.shields.io/crates/msrv/xiangting)](https://crates.io/crates/xiangting)
45
[![API](https://img.shields.io/badge/api-main-yellow.svg)](https://apricot-s.github.io/xiangting/xiangting)
56
[![API](https://docs.rs/xiangting/badge.svg)](https://docs.rs/xiangting)
7+
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Apricot-S/xiangting)
68

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

9-
This library is based on the algorithm in [Cryolite's Nyanten](https://github.com/Cryolite/nyanten).
10-
However, it introduces the following additional features:
11-
12-
- Supports rules that include and exclude melded tiles when determining if a hand contains four identical tiles.
13-
- Supports three-player mahjong.
14-
1511
Documentation:
1612

1713
- [API reference (main branch)](https://Apricot-S.github.io/xiangting/xiangting)
@@ -25,6 +21,10 @@ Documentation:
2521
- [Theoretical Background of Nyanten (Efficient Computation of Shanten/Deficiency Numbers) #麻雀 - Qiita](https://qiita.com/Cryolite/items/75d504c7489426806b87)
2622
- [A Fast and Space-Efficient Algorithm for Calculating Deficient Numbers (a.k.a. Shanten Numbers).pdf](https://www.slideshare.net/slideshow/a-fast-and-space-efficient-algorithm-for-calculating-deficient-numbers-a-k-a-shanten-numbers-pdf/269706674)
2723

24+
## Language Bindings
25+
26+
- Python: [xiangting-py](https://github.com/Apricot-S/xiangting-py)
27+
2828
## Installation
2929

3030
```sh
@@ -57,7 +57,7 @@ The correspondence between the index and the tile is shown in the table below.
5757
Calculates the replacement number, which is equal to the deficiency number (a.k.a. xiangting number, 向聴数) + 1.
5858

5959
```rust
60-
use xiangting::calculate_replacement_number;
60+
use xiangting::{PlayerCount, calculate_replacement_number};
6161

6262
fn main() {
6363
// 123m456p789s11222z
@@ -68,54 +68,55 @@ fn main() {
6868
2, 3, 0, 0, 0, 0, 0, // z
6969
];
7070

71-
let replacement_number = calculate_replacement_number(&hand, None);
72-
assert_eq!(replacement_number.unwrap(), 0u8);
71+
let replacement_number = calculate_replacement_number(&hand, &PlayerCount::Four).unwrap();
72+
assert_eq!(replacement_number, 0u8);
7373
}
7474
```
7575

76-
### Handling Melds
76+
### Necessary and Unnecessary Tiles
7777

78-
In the calculation for a hand with melds (副露),
79-
the melded tiles can be included or excluded when counting tiles to determine if a hand contains four identical ones.
78+
It is also possible to calculate necessary or unnecessary tiles together with the replacement number.
8079

81-
If melds are excluded (e.g., 天鳳 (Tenhou), 雀魂 (Mahjong Soul)), specify `None` for `fulu_mianzi_list`.
80+
- Necessary tiles
81+
- Tiles needed to win with the minimum number of replacements
82+
- Tiles that reduce the replacement number when drawn
83+
- In Japanese, these are referred to as *有効牌 (yūkōhai)* or *受け入れ (ukeire)*
8284

83-
If melds are included (e.g., World Riichi Championship, M.LEAGUE), the melds should be included in the `fulu_mianzi_list`.
85+
- Unnecessary tiles
86+
- Tiles not needed to win with the minimum number of replacements
87+
- Tiles that can be discarded without changing the replacement number
88+
- In Japanese, these are referred to as *不要牌 (fuyōhai)* or *余剰牌 (yojōhai)*
8489

8590
```rust
86-
use xiangting::{ClaimedTilePosition, FuluMianzi, calculate_replacement_number};
91+
use xiangting::{PlayerCount, calculate_necessary_tiles, calculate_unnecessary_tiles};
8792

8893
fn main() {
89-
// 123m1z
94+
// 199m146779p12s246z
9095
let hand: [u8; 34] = [
91-
1, 1, 1, 0, 0, 0, 0, 0, 0, // m
92-
0, 0, 0, 0, 0, 0, 0, 0, 0, // p
93-
0, 0, 0, 0, 0, 0, 0, 0, 0, // s
94-
1, 0, 0, 0, 0, 0, 0, // z
96+
1, 0, 0, 0, 0, 0, 0, 0, 2, // m
97+
1, 0, 0, 1, 0, 1, 2, 0, 1, // p
98+
1, 1, 0, 0, 0, 0, 0, 0, 0, // s
99+
0, 1, 0, 1, 0, 1, 0, // z
95100
];
96101

97-
// 456p 7777s 111z
98-
let melds = [
99-
FuluMianzi::Shunzi(12, ClaimedTilePosition::Low),
100-
FuluMianzi::Gangzi(24),
101-
FuluMianzi::Kezi(27),
102-
];
102+
let (replacement_number1, necessary_tiles) =
103+
calculate_necessary_tiles(&hand, &PlayerCount::Four).unwrap();
104+
let (replacement_number2, unnecessary_tiles) =
105+
calculate_unnecessary_tiles(&hand, &PlayerCount::Four).unwrap();
103106

104-
let replacement_number_wo_melds = calculate_replacement_number(&hand, None);
105-
assert_eq!(replacement_number_wo_melds.unwrap(), 1u8);
106-
107-
let replacement_number_w_melds = calculate_replacement_number(&hand, Some(&melds));
108-
assert_eq!(replacement_number_w_melds.unwrap(), 2u8);
107+
assert_eq!(replacement_number1, 5);
108+
assert_eq!(replacement_number1, replacement_number2);
109+
assert_eq!(necessary_tiles, 0b1111111_100000111_111111111_100000111); // 1239m123456789p1239s1234567z
110+
assert_eq!(unnecessary_tiles, 0b0101010_000000011_101101001_000000001); // 1m14679p12s246z
109111
}
110112
```
111113

112114
### Support for Three-Player Mahjong
113115

114116
In three-player mahjong, the tiles from 2m (二萬) to 8m (八萬) are not used.
115-
In addition, melded sequences (明順子) are not allowed.
116117

117118
```rust
118-
use xiangting::{calculate_replacement_number, calculate_replacement_number_3_player};
119+
use xiangting::{PlayerCount, calculate_necessary_tiles, calculate_unnecessary_tiles};
119120

120121
fn main() {
121122
// 1111m111122233z
@@ -126,11 +127,17 @@ fn main() {
126127
4, 3, 2, 0, 0, 0, 0, // z
127128
];
128129

129-
let replacement_number_4p = calculate_replacement_number(&hand, None);
130-
assert_eq!(replacement_number_4p.unwrap(), 2u8);
131-
132-
let replacement_number_3p = calculate_replacement_number_3_player(&hand, None);
133-
assert_eq!(replacement_number_3p.unwrap(), 3u8);
130+
let (rn_4p, nt_4p) = calculate_necessary_tiles(&hand, &PlayerCount::Four).unwrap();
131+
let (_, ut_4p) = calculate_unnecessary_tiles(&hand, &PlayerCount::Four).unwrap();
132+
assert_eq!(rn_4p, 2u8);
133+
assert_eq!(nt_4p, 0b0000000_000000000_000000000_000000110); // 23m
134+
assert_eq!(ut_4p, 0b0000001_000000000_000000000_000000000); // 1z
135+
136+
let (rn_3p, nt_3p) = calculate_necessary_tiles(&hand, &PlayerCount::Three).unwrap();
137+
let (_, ut_3p) = calculate_unnecessary_tiles(&hand, &PlayerCount::Three).unwrap();
138+
assert_eq!(rn_3p, 3u8);
139+
assert_eq!(nt_3p, 0b1111100_111111111_111111111_100000000); // 9m123456789p123456789s34567z
140+
assert_eq!(ut_3p, 0b0000001_000000000_000000000_000000001); // 1m1z
134141
}
135142
```
136143

benches/baseline.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// SPDX-FileCopyrightText: 2025 Apricot S.
2+
// SPDX-License-Identifier: MIT
3+
// This file is part of https://github.com/Apricot-S/xiangting
4+
5+
use xiangting::{PlayerCount, TileCounts, calculate_replacement_number};
6+
7+
pub fn calculate_necessary_tiles(bingpai: &TileCounts) -> u64 {
8+
let mut bingpai = bingpai.clone();
9+
10+
let replacement_number = calculate_replacement_number(&bingpai, &PlayerCount::Four).unwrap();
11+
if replacement_number == 0 {
12+
return 0;
13+
}
14+
15+
let mut necessary_tiles = 0u64;
16+
17+
match bingpai.iter().sum::<u8>() {
18+
n if n % 3 == 1 => {
19+
for tile in 0..34 {
20+
if bingpai[tile] >= 4 {
21+
continue;
22+
}
23+
24+
bingpai[tile] += 1;
25+
let new_replacement_number =
26+
calculate_replacement_number(&bingpai, &PlayerCount::Four).unwrap();
27+
if new_replacement_number < replacement_number {
28+
necessary_tiles |= 1 << tile;
29+
}
30+
bingpai[tile] -= 1;
31+
}
32+
}
33+
n if n % 3 == 2 => {
34+
for tile in 0..34 {
35+
if bingpai[tile] >= 4 {
36+
continue;
37+
}
38+
39+
let new_replacement_number =
40+
calculate_replacement_number(&bingpai, &PlayerCount::Four).unwrap();
41+
if new_replacement_number < replacement_number {
42+
necessary_tiles |= 1 << tile;
43+
}
44+
}
45+
}
46+
_ => panic!("invalid hand"),
47+
}
48+
49+
necessary_tiles
50+
}
51+
52+
pub fn calculate_unnecessary_tiles(bingpai: &TileCounts) -> u64 {
53+
let mut bingpai = bingpai.clone();
54+
55+
let replacement_number = calculate_replacement_number(&bingpai, &PlayerCount::Four).unwrap();
56+
if replacement_number == 0 {
57+
return 0;
58+
}
59+
60+
let mut unnecessary_tiles = 0u64;
61+
match bingpai.iter().sum::<u8>() {
62+
n if n % 3 == 1 => {
63+
for tile in 0..34 {
64+
if bingpai[tile] > 0 {
65+
let new_replacement_number =
66+
calculate_replacement_number(&bingpai, &PlayerCount::Four).unwrap();
67+
if new_replacement_number == replacement_number {
68+
unnecessary_tiles |= 1 << tile;
69+
}
70+
}
71+
}
72+
}
73+
n if n % 3 == 2 => {
74+
for tile in 0..34 {
75+
if bingpai[tile] > 0 {
76+
bingpai[tile] -= 1;
77+
let new_replacement_number =
78+
calculate_replacement_number(&bingpai, &PlayerCount::Four).unwrap();
79+
if new_replacement_number == replacement_number {
80+
unnecessary_tiles |= 1 << tile;
81+
}
82+
bingpai[tile] += 1;
83+
}
84+
}
85+
}
86+
_ => panic!("invalid hand"),
87+
}
88+
unnecessary_tiles
89+
}

0 commit comments

Comments
 (0)