🀄
麻雀の符と翻から点数を計算する
Rustの麻雀の点数計算ライブラリが欲しくなったので,自分で書いています.
ディレクトリ構成
tree -L 2
.
├── Cargo.lock
├── Cargo.toml
├── README.md
├── src
│ ├── lib.rs
│ ├── point_calculator
│ └── point_calculator.rs
├── target
│ ├── CACHEDIR.TAG
│ ├── debug
│ └── tmp
└── tests
└── point_calculator_test.rs
PointCalculator (API Entry Point)
麻雀の点数は以下の様に計算ができます.
- 基本符の計算
- 基本符の制限 (青天井防止)
- 親・子に応じて重み付け
- 点数の切上げ
use crate::point_calculator::calculate_base_point::calculate_base_points;
use crate::point_calculator::calculate_weighted_parent_child_score::calculate_weighted_score_by_dealer_or_not;
use crate::point_calculator::limit_base_point::limit_base_point;
use crate::point_calculator::points_shift_up::points_shift_up;
mod calculate_base_point;
mod calculate_weighted_parent_child_score;
mod limit_base_point;
mod points_shift_up;
/// PointCalculator
///
/// # Overview
/// Calculate the score of the hand information.
///
/// # Arguments
/// * `fu: u32` - Fu, 符
/// * `han: u32` - Han, 翻
/// * `dealer: bool` - Dealer or not, 親か子か
///
/// # Example
/// ```
/// use mahjong_scorer::point_calculator::PointCalculator;
///
/// let point_calculator = PointCalculator::new(30, 2, true);
/// let score = point_calculator.calculate();
/// assert_eq!(score, 2900);
/// ```
pub struct PointCalculator {
// fu, 符
pub fu: u32,
// han, 翻
pub han: u32,
// dealer or not, 親か子か
pub dealer: bool,
}
impl PointCalculator {
pub fn new(fu: u32, han: u32, dealer: bool) -> Self {
PointCalculator { fu, han, dealer }
}
pub fn calculate(&self) -> u32 {
let base_points: u32 = calculate_base_points(self.fu, self.han);
let base_points: u32 = limit_base_point(base_points, self.han);
let base_points: u32 = calculate_weighted_score_by_dealer_or_not(base_points, self.dealer);
points_shift_up(base_points)
}
}
基本点の計算
地味にここが一番ややこしいです.計算式は以下になります.
/// Calculate base points (基本点の計算)
///
/// # Arguments
/// * `fu: u32` - 符
/// * `han: 32` - 翻
///
/// # Returns
/// * `u32` - Base points (基本点)
///
/// # Algorithm
/// English:
/// Calculate the base points (multiply fu by 4 and double it by the number of han)
/// base points = fu * 4 * 2^han
///
/// Japanese:
/// 基本点の計算 (符を4倍し、翻数分だけ2倍する)
/// 基本点 = 符 * 4 * 2^翻
///
/// # Examples
/// 20 fu, 1 han => 160
/// 30 fu, 2 han => 480
pub(super) fn calculate_base_points(fu: u32, han: u32) -> u32 {
fu * 4 * 2u32.pow(han)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_base_points() {
// 20 * 4 * 2^1
// = 80 * 2
// = 160
assert_eq!(calculate_base_points(20, 1), 160);
// 30 * 4 * 2^2
// = 120 * 4
// = 480
assert_eq!(calculate_base_points(30, 2), 480);
// 40 * 4 * 2^3
// = 160 * 8
// = 1280
assert_eq!(calculate_base_points(40, 3), 1280);
}
}
基本点の制限
麻雀は点数に上限があります.点数の上限は翻によって変わります.Rustのmatch式が便利です.
/// Limit the base points based on the number of han.
///
/// Japanese:
/// 翻数に応じて基本点を制限します。
///
/// # Arguments
/// * `base_points: u32` - Base points (基本点)
/// * `han: u32` - Number of han (翻数)
///
/// # Returns
/// * `u32` - Limited base points (制限された基本点)
///
/// # Examples
/// 5 han, 2001 points -> 2000 points
/// 6 han, any points -> 3000 points
/// 13 han, any points -> 8000 points
pub(super) fn limit_base_point(base_points: u32, han: u32) -> u32 {
match han {
// 数え役満 (13翻以上), Yakuman (13 han or more)
13..=u32::MAX => 8000,
// 三倍満 (11-12翻), Sanbaiman (11-12 han)
11..=12 => 6000,
// 倍満 (8-10翻), Baiman (8-10 han)
8..=10 => 4000,
// 跳満 (6-7翻), Haneman (6-7 han)
6..=7 => 3000,
_ => {
if base_points > 2000 {
// 満貫 (それ以外で2000点を超える場合), Mangan (2000 points or more)
2000
} else {
base_points
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_return_original_value_with_under_limitation() {
assert_eq!(limit_base_point(1000, 4), 1000);
}
#[test]
fn test_return_2000_with_over_2000_points() {
assert_eq!(limit_base_point(2001, 4), 2000);
}
#[test]
fn test_return_3000_with_6_7_han() {
assert_eq!(limit_base_point(3001, 6), 3000);
assert_eq!(limit_base_point(3001, 7), 3000);
}
#[test]
fn test_return_4000_with_8_9_10_han() {
assert_eq!(limit_base_point(4001, 8), 4000);
assert_eq!(limit_base_point(4001, 9), 4000);
assert_eq!(limit_base_point(4001, 10), 4000);
}
#[test]
fn test_return_6000_with_11_12_han() {
assert_eq!(limit_base_point(6001, 11), 6000);
assert_eq!(limit_base_point(6001, 12), 6000);
}
#[test]
fn test_return_8000_over_13_han() {
assert_eq!(limit_base_point(8001, 13), 8000);
assert_eq!(limit_base_point(8001, 14), 8000);
}
}
親・子に応じて重み付け
Wikipediaによると,親と子は英語で「dealer」「non-dealer」というとのことです.
計算式は,親なら6倍,子なら4倍です.
/// Calculate the score of the parent and non-dealer.
///
/// English:
/// Calculate weighted score by dealer or not.
///
/// Japanese:
/// 親子に応じて重み付けされた得点を計算します。
///
/// # Arguments
/// * `base_points: u32` - The base points.
/// * `dealer: bool` - Whether the player is the dealer.
///
/// # Returns
/// * `u32` - The weighted score by parent and non-dealer.
///
/// # Examples
/// 2900 points, non-dealer -> 11600 points
/// 7700 points, non-dealer -> 30800 points
pub(super) fn calculate_weighted_score_by_dealer_or_not(base_points: u32, dealer: bool) -> u32 {
match dealer {
// if the player is the dealer, the score is 6 times
// 親の場合、得点が6倍
true => base_points * 6,
// if the player is the non-dealer, the score is 4 times
// 子の場合、得点が4倍
false => base_points * 4,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_non_dealer() {
// point is 4 times when the player is the non-dealer
// 子のとき、基本点が4倍になる
assert_eq!(
calculate_weighted_score_by_dealer_or_not(2900, false),
11600
);
assert_eq!(
calculate_weighted_score_by_dealer_or_not(7700, false),
30800
);
assert_eq!(
calculate_weighted_score_by_dealer_or_not(12000, false),
48000
);
}
#[test]
fn test_calculate_parent() {
// point is 6 times when the player is the dealer
// 親のとき、基本点が6倍になる
assert_eq!(calculate_weighted_score_by_dealer_or_not(2900, true), 17400);
assert_eq!(calculate_weighted_score_by_dealer_or_not(7700, true), 46200);
assert_eq!(
calculate_weighted_score_by_dealer_or_not(12000, true),
72000
);
}
}
点数の切上げ
100の位以下の数があれば切上げをします.計算式は以下の様になります.
- 1200のとき (整数除算に注意)
- 1201のとき
Rustの場合,整数同士の除算であれば,/
が整数除算になります.整数除算に //
を使わないことに注意.
/// Shift up points to 100 units.
///
/// # Arguments
/// * `base_points: u32` - The base points.
///
/// # Returns
/// * `u32` - The points shifted up to 100 units.
///
/// # Examples
/// 1201 => 1300
/// 1210 => 1300
pub(super) fn points_shift_up(base_points: u32) -> u32 {
(base_points + 99) / 100 * 100
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_points_shift_up() {
assert_eq!(points_shift_up(1201), 1300);
assert_eq!(points_shift_up(1210), 1300);
}
}
インテグレーションテスト
念のためですが,Rustはインテグレーションテストを tests
dirに置くことが推奨されています.
具体的なテスト項目です.3, 4 翻は満貫になるケースがあるので境界値で確認しています.ちなみに3翻は70符,4翻は40符以上で満貫です.(※ ルールによっては30符の切上げもあります)
use mahjong_scorer::point_calculator::PointCalculator;
#[test]
fn test_calculate_2_han() {
// 30 fu, 2 han, dealer
// 30符, 2翻, 親
let point_calculator = PointCalculator::new(30, 2, true);
assert_eq!(point_calculator.calculate(), 2900);
// 30 fu, 2 han, non-dealer
// 30符, 2翻, 子
let point_calculator = PointCalculator::new(30, 2, false);
assert_eq!(point_calculator.calculate(), 2000);
}
#[test]
fn test_calculate_3_han() {
// 30 fu, 3 han, dealer
// 30符, 3翻, 親
let point_calculator = PointCalculator::new(30, 3, true);
assert_eq!(point_calculator.calculate(), 5800);
// 30 fu, 3 han, non-dealer
// 30符, 3翻, 子
let point_calculator = PointCalculator::new(30, 3, false);
assert_eq!(point_calculator.calculate(), 3900);
// 70 fu, 3 han, dealer (Mangan)
// 70符, 3翻, 親 (満貫)
let point_calculator = PointCalculator::new(70, 3, true);
assert_eq!(point_calculator.calculate(), 12000);
// 70 fu, 3 han, non-dealer (Mangan)
// 70符, 3翻, 子 (満貫)
let point_calculator = PointCalculator::new(70, 3, false);
assert_eq!(point_calculator.calculate(), 8000);
}
#[test]
fn test_calculate_4_han() {
// 30 fu, 4 han, dealer
// 30符, 4翻, 親
let point_calculator = PointCalculator::new(30, 4, true);
assert_eq!(point_calculator.calculate(), 11600);
// 30 fu, 4 han, non-dealer
// 30符, 4翻, 子
let point_calculator = PointCalculator::new(30, 4, false);
assert_eq!(point_calculator.calculate(), 7700);
// 40 fu, 4 han, dealer (Mangan)
// 40符, 4翻, 親 (満貫)
let point_calculator = PointCalculator::new(40, 4, true);
assert_eq!(point_calculator.calculate(), 12000);
// 40 fu, 4 han, non-dealer (Mangan)
// 40符, 4翻, 子 (満貫)
let point_calculator = PointCalculator::new(40, 4, false);
assert_eq!(point_calculator.calculate(), 8000);
}
#[test]
fn test_calculate_5_han() {
// 40 fu, 5 han, dealer (Mangan)
// 40符, 5翻, 親 (満貫)
let point_calculator = PointCalculator::new(40, 5, true);
assert_eq!(point_calculator.calculate(), 12000);
// 40 fu, 5 han, non-dealer (Mangan)
// 40符, 5翻, 子 (満貫)
let point_calculator = PointCalculator::new(40, 5, false);
assert_eq!(point_calculator.calculate(), 8000);
}
#[test]
fn test_calculate_6_7_han() {
// 6 han, dealer (Haneman)
// 6翻, 親 (跳満)
let point_calculator = PointCalculator::new(40, 6, true);
assert_eq!(point_calculator.calculate(), 18000);
// 6 han, non-dealer (Haneman)
// 6翻, 子 (跳満)
let point_calculator = PointCalculator::new(40, 6, false);
assert_eq!(point_calculator.calculate(), 12000);
// 7 han, dealer (Haneman)
// 7翻, 親 (跳満)
let point_calculator = PointCalculator::new(40, 7, true);
assert_eq!(point_calculator.calculate(), 18000);
// 7 han, non-dealer (Haneman)
// 7翻, 子 (跳満)
let point_calculator = PointCalculator::new(40, 7, false);
assert_eq!(point_calculator.calculate(), 12000);
}
#[test]
fn test_calculate_8_9_10_han() {
// 8 han, dealer (Baiman)
// 8翻, 親 (倍満)
let point_calculator = PointCalculator::new(40, 8, true);
assert_eq!(point_calculator.calculate(), 24000);
// 8 han, non-dealer (Baiman)
// 8翻, 子 (倍満)
let point_calculator = PointCalculator::new(40, 8, false);
assert_eq!(point_calculator.calculate(), 16000);
// 9 han, dealer (Baiman)
// 9翻, 親 (倍満)
let point_calculator = PointCalculator::new(40, 9, true);
assert_eq!(point_calculator.calculate(), 24000);
// 9 han, non-dealer (Baiman)
// 9翻, 子 (倍満)
let point_calculator = PointCalculator::new(40, 9, false);
assert_eq!(point_calculator.calculate(), 16000);
// 10 han, dealer (Baiman)
// 10翻, 親 (倍満)
let point_calculator = PointCalculator::new(40, 10, true);
assert_eq!(point_calculator.calculate(), 24000);
// 10 han, non-dealer (Baiman)
// 10翻, 子 (倍満)
let point_calculator = PointCalculator::new(40, 10, false);
assert_eq!(point_calculator.calculate(), 16000);
}
#[test]
fn test_calculate_11_12_han() {
// 11 han, dealer (Sanbaiman)
// 11翻, 親 (三倍満)
let point_calculator = PointCalculator::new(40, 11, true);
assert_eq!(point_calculator.calculate(), 36000);
// 11 han, non-dealer (Sanbaiman)
// 11翻, 子 (三倍満)
let point_calculator = PointCalculator::new(40, 11, false);
assert_eq!(point_calculator.calculate(), 24000);
// 12 han, dealer (Sanbaiman)
// 12翻, 親 (三倍満)
let point_calculator = PointCalculator::new(40, 12, true);
assert_eq!(point_calculator.calculate(), 36000);
// 12 han, non-dealer (Sanbaiman)
// 12翻, 子 (三倍満)
let point_calculator = PointCalculator::new(40, 12, false);
assert_eq!(point_calculator.calculate(), 24000);
}
#[test]
fn test_calculate_13_han() {
// 13 han, dealer (Yakuman)
// 13翻, 親 (役満)
let point_calculator = PointCalculator::new(40, 13, true);
assert_eq!(point_calculator.calculate(), 48000);
// 13 han, non-dealer (Yakuman)
// 13翻, 子 (役満)
let point_calculator = PointCalculator::new(40, 13, false);
assert_eq!(point_calculator.calculate(), 32000);
}
テスト結果
長いので省略します.
result
cargo test
Compiling mahjong_scorer v0.1.0 (/.../mahjong_scorer)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.49s
Running unittests src/lib.rs (target/debug/deps/mahjong_scorer-056ed10741749025)
running 10 tests
test point_calculator::calculate_base_point::tests::test_calculate_base_points ... ok
test point_calculator::calculate_weighted_parent_child_score::tests::test_calculate_non_dealer ... ok
test point_calculator::limit_base_point::tests::test_return_4000_with_8_9_10_han ... ok
test point_calculator::limit_base_point::tests::test_return_2000_with_over_2000_points ... ok
test point_calculator::limit_base_point::tests::test_return_3000_with_6_7_han ... ok
test point_calculator::calculate_weighted_parent_child_score::tests::test_calculate_parent ... ok
test point_calculator::limit_base_point::tests::test_return_6000_with_11_12_han ... ok
test point_calculator::limit_base_point::tests::test_return_8000_over_13_han ... ok
test point_calculator::limit_base_point::tests::test_return_original_value_with_under_limitation ... ok
test point_calculator::points_shift_up::tests::test_points_shift_up ... ok
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/point_calculator_test.rs (target/debug/deps/point_calculator_test-fe9418c9b813bc4d)
running 8 tests
test test_calculate_11_12_han ... ok
test test_calculate_13_han ... ok
test test_calculate_3_han ... ok
test test_calculate_4_han ... ok
test test_calculate_6_7_han ... ok
test test_calculate_2_han ... ok
test test_calculate_5_han ... ok
test test_calculate_8_9_10_han ... ok
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests mahjong_scorer
running 1 test
test src/point_calculator.rs - point_calculator::PointCalculator (line 22) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.31s
参考
Discussion