🀄

麻雀の符と翻から点数を計算する

2024/08/27に公開

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)

麻雀の点数は以下の様に計算ができます.

  1. 基本符の計算
  2. 基本符の制限 (青天井防止)
  3. 親・子に応じて重み付け
  4. 点数の切上げ
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)
    }
}

基本点の計算

地味にここが一番ややこしいです.計算式は以下になります.

\text{基本点} = \text{符} * 4 * 2^\text{翻}
/// 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の位以下の数があれば切上げをします.計算式は以下の様になります.

(\text{input} + 99) // 100 * 100
  • 1200のとき (整数除算に注意)
\begin{aligned} (1200 + 99) // 100 * 100 &= 1299 // 100 * 100 \\ &= 12 * 100 \\ &= 1200 \end{aligned}
  • 1201のとき
\begin{aligned} (1201 + 99) // 100 * 100 &= 1300 // 100 * 100 \\ &= 13 * 100 \\ &= 1300 \end{aligned}

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に置くことが推奨されています.

https://doc.rust-lang.org/rust-by-example/testing/integration_testing.html

具体的なテスト項目です.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

参考

https://blog.kobalab.net/entry/20151226/1451057134

https://riichi.wiki/Japanese_mahjong

Discussion