riichi_engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +19 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +17 -0
  4. data/.github/pull_request_template.md +15 -0
  5. data/.github/workflows/ci.yml +22 -0
  6. data/.github/workflows/release.yml +31 -0
  7. data/.gitignore +6 -0
  8. data/.ruby-version +1 -0
  9. data/CHANGELOG.md +17 -0
  10. data/Gemfile +3 -0
  11. data/LICENSE +21 -0
  12. data/README.md +164 -0
  13. data/docs/adapter_contract.md +144 -0
  14. data/docs/api_reference.md +309 -0
  15. data/docs/concepts.md +134 -0
  16. data/docs/public_api_policy.md +94 -0
  17. data/docs/state_machine.md +360 -0
  18. data/docs/usage_examples.md +265 -0
  19. data/lib/mahjong/config/rule_config.rb +53 -0
  20. data/lib/mahjong/cpu/ai/base_ai.rb +50 -0
  21. data/lib/mahjong/cpu/ai/easy_ai.rb +11 -0
  22. data/lib/mahjong/cpu/ai/hard_ai.rb +11 -0
  23. data/lib/mahjong/cpu/ai/normal_ai.rb +11 -0
  24. data/lib/mahjong/cpu/analysis/shanten.rb +112 -0
  25. data/lib/mahjong/cpu/analysis/tile_evaluator.rb +54 -0
  26. data/lib/mahjong/cpu/judges/naki_judge.rb +60 -0
  27. data/lib/mahjong/cpu/judges/riichi_judge.rb +42 -0
  28. data/lib/mahjong/cpu/selectors/dahai_selector.rb +97 -0
  29. data/lib/mahjong/errors/engine_error.rb +5 -0
  30. data/lib/mahjong/errors/invalid_action_error.rb +5 -0
  31. data/lib/mahjong/errors/invalid_state_error.rb +5 -0
  32. data/lib/mahjong/errors/tile_not_found_error.rb +5 -0
  33. data/lib/mahjong/flow/detectors/naki_detector.rb +82 -0
  34. data/lib/mahjong/flow/games/game_flow.rb +186 -0
  35. data/lib/mahjong/flow/resolvers/action_resolver.rb +22 -0
  36. data/lib/mahjong/flow/rounds/round_flow.rb +588 -0
  37. data/lib/mahjong/flow/validators/action_validator.rb +110 -0
  38. data/lib/mahjong/results/final_result.rb +11 -0
  39. data/lib/mahjong/results/game_flow_result.rb +20 -0
  40. data/lib/mahjong/results/game_result.rb +26 -0
  41. data/lib/mahjong/results/round_end_info.rb +18 -0
  42. data/lib/mahjong/results/round_flow_result.rb +18 -0
  43. data/lib/mahjong/scoring/calculators/fu_calculator.rb +68 -0
  44. data/lib/mahjong/scoring/calculators/score_calculator.rb +73 -0
  45. data/lib/mahjong/scoring/evaluators/win_evaluator.rb +111 -0
  46. data/lib/mahjong/scoring/judges/agari_judge.rb +68 -0
  47. data/lib/mahjong/scoring/judges/furiten_judge.rb +34 -0
  48. data/lib/mahjong/scoring/judges/machi_judge.rb +78 -0
  49. data/lib/mahjong/scoring/judges/yaku_judge.rb +161 -0
  50. data/lib/mahjong/scoring/parsers/hand_parser.rb +87 -0
  51. data/lib/mahjong/scoring/value_objects/score_result.rb +57 -0
  52. data/lib/mahjong/scoring/value_objects/yaku_context.rb +70 -0
  53. data/lib/mahjong/scoring/value_objects/yaku_entry.rb +7 -0
  54. data/lib/mahjong/scoring/value_objects/yaku_judge_result.rb +27 -0
  55. data/lib/mahjong/scoring/yaku/chankan.rb +6 -0
  56. data/lib/mahjong/scoring/yaku/chanta.rb +15 -0
  57. data/lib/mahjong/scoring/yaku/chiihou.rb +7 -0
  58. data/lib/mahjong/scoring/yaku/chiitoitsu.rb +6 -0
  59. data/lib/mahjong/scoring/yaku/chinitsu.rb +10 -0
  60. data/lib/mahjong/scoring/yaku/chinroutou.rb +6 -0
  61. data/lib/mahjong/scoring/yaku/chuuren_poutou.rb +20 -0
  62. data/lib/mahjong/scoring/yaku/daisangen.rb +8 -0
  63. data/lib/mahjong/scoring/yaku/daisuushii.rb +8 -0
  64. data/lib/mahjong/scoring/yaku/double_riichi.rb +6 -0
  65. data/lib/mahjong/scoring/yaku/haitei.rb +6 -0
  66. data/lib/mahjong/scoring/yaku/honitsu.rb +11 -0
  67. data/lib/mahjong/scoring/yaku/honroutou.rb +9 -0
  68. data/lib/mahjong/scoring/yaku/houtei.rb +6 -0
  69. data/lib/mahjong/scoring/yaku/iipeiko.rb +16 -0
  70. data/lib/mahjong/scoring/yaku/ikkitsuukan.rb +15 -0
  71. data/lib/mahjong/scoring/yaku/ippatsu.rb +6 -0
  72. data/lib/mahjong/scoring/yaku/junchan.rb +15 -0
  73. data/lib/mahjong/scoring/yaku/kokushi_musou.rb +6 -0
  74. data/lib/mahjong/scoring/yaku/menzen_tsumo.rb +6 -0
  75. data/lib/mahjong/scoring/yaku/pinfu.rb +16 -0
  76. data/lib/mahjong/scoring/yaku/riichi.rb +6 -0
  77. data/lib/mahjong/scoring/yaku/rinshan_kaihou.rb +6 -0
  78. data/lib/mahjong/scoring/yaku/ryanpeiko.rb +11 -0
  79. data/lib/mahjong/scoring/yaku/ryuuiisou.rb +6 -0
  80. data/lib/mahjong/scoring/yaku/san_ankou.rb +15 -0
  81. data/lib/mahjong/scoring/yaku/san_kantsu.rb +6 -0
  82. data/lib/mahjong/scoring/yaku/sanshoku_doujun.rb +15 -0
  83. data/lib/mahjong/scoring/yaku/sanshoku_doukou.rb +14 -0
  84. data/lib/mahjong/scoring/yaku/shousangen.rb +14 -0
  85. data/lib/mahjong/scoring/yaku/shousuushii.rb +14 -0
  86. data/lib/mahjong/scoring/yaku/suuankou.rb +15 -0
  87. data/lib/mahjong/scoring/yaku/suukantsu.rb +6 -0
  88. data/lib/mahjong/scoring/yaku/tanyao.rb +7 -0
  89. data/lib/mahjong/scoring/yaku/tenhou.rb +7 -0
  90. data/lib/mahjong/scoring/yaku/toitoihou.rb +7 -0
  91. data/lib/mahjong/scoring/yaku/tsuuiisou.rb +6 -0
  92. data/lib/mahjong/scoring/yaku/yakuhai.rb +27 -0
  93. data/lib/mahjong/snapshots/final_result_snapshot.rb +8 -0
  94. data/lib/mahjong/snapshots/game_progress_snapshot.rb +12 -0
  95. data/lib/mahjong/snapshots/game_setup_snapshot.rb +12 -0
  96. data/lib/mahjong/snapshots/win_evaluation_snapshot.rb +11 -0
  97. data/lib/mahjong/state/fuuro.rb +45 -0
  98. data/lib/mahjong/state/hand.rb +64 -0
  99. data/lib/mahjong/state/kawa.rb +68 -0
  100. data/lib/mahjong/state/mentsu.rb +55 -0
  101. data/lib/mahjong/state/round_state.rb +193 -0
  102. data/lib/mahjong/tiles/dora.rb +19 -0
  103. data/lib/mahjong/tiles/tile.rb +168 -0
  104. data/lib/mahjong/tiles/tile_set.rb +51 -0
  105. data/lib/mahjong/tiles/wall.rb +47 -0
  106. data/lib/mahjong/tiles/wanpai.rb +79 -0
  107. data/lib/riichi_engine/api.rb +62 -0
  108. data/lib/riichi_engine/version.rb +3 -0
  109. data/lib/riichi_engine.rb +15 -0
  110. data/riichi_engine.gemspec +32 -0
  111. metadata +207 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0705360e35c7551b07930b9c6faf76514066be31cf6c3de5ec7ffd14ec911ef3
4
+ data.tar.gz: 7216b262185cc200da28ce090f93af3af5305ab44cc335a4492ec2950adc80a3
5
+ SHA512:
6
+ metadata.gz: 6403afe314adc11f52fb58650ef8cd5037d917643589b5f05a0b67b82d21a3283f04f39665ea6a3148aaf037e7e90e28210dc73f661385d88b09548c9ff41970
7
+ data.tar.gz: 7b9ae0f5aded16e00b8fd619c538146af2cad725adf1cfb94256c31e024fbcc3107324ee9ddb71eb6fc07794105a6c89f75d28f29733756258e9cb2d360467df
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: Bug report
3
+ about: Report a defect in riichi_engine
4
+ title: "[Bug] "
5
+ labels: bug
6
+ assignees: ""
7
+ ---
8
+
9
+ ## Summary
10
+
11
+ ## Expected
12
+
13
+ ## Actual
14
+
15
+ ## Reproduction
16
+
17
+ ## Version
18
+
19
+ ## Notes
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: Feature request
3
+ about: Propose a new capability or API addition
4
+ title: "[Feature] "
5
+ labels: enhancement
6
+ assignees: ""
7
+ ---
8
+
9
+ ## Summary
10
+
11
+ ## Motivation
12
+
13
+ ## Proposed API / Behavior
14
+
15
+ ## Alternatives
16
+
17
+ ## Notes
@@ -0,0 +1,15 @@
1
+ ## Summary
2
+
3
+ ## Changes
4
+
5
+ ## Verification
6
+
7
+ - [ ] `bundle exec rspec` in `vendor/gems/riichi_engine`
8
+ - [ ] `bundle exec rspec` in host app
9
+
10
+ ## Docs
11
+
12
+ - [ ] `README.md` updated if needed
13
+ - [ ] `docs/api_reference.md` updated if needed
14
+ - [ ] `docs/usage_examples.md` updated if needed
15
+ - [ ] `docs/public_api_policy.md` updated if needed
@@ -0,0 +1,22 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - uses: ruby/setup-ruby@v1
17
+ with:
18
+ ruby-version: .ruby-version
19
+ bundler-cache: true
20
+
21
+ - name: Run specs
22
+ run: bundle exec rspec
@@ -0,0 +1,31 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ release:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: read
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: .ruby-version
20
+ bundler-cache: true
21
+
22
+ - name: Run specs
23
+ run: bundle exec rspec
24
+
25
+ - name: Build gem
26
+ run: gem build riichi_engine.gemspec
27
+
28
+ # gem push は手動で行う:
29
+ # gem push riichi_engine-*.gem
30
+ # 事前に RubyGems へのログイン(gem signin)または
31
+ # ~/.gem/credentials への API キー設定が必要。
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ /.bundle/
2
+ /.rspec_status
3
+ /coverage/
4
+ /tmp/
5
+ /Gemfile.lock
6
+ /*.gem
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 4.0.2
data/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-04-05)
4
+
5
+ 初回リリース
6
+
7
+ ### 機能
8
+
9
+ - **局進行** — 自摸・打牌・リーチ・ポン・チー・カン(暗槓・加槓・大明槓)・九種九牌の処理
10
+ - **和了判定** — 通常和了形・七対子・国士無双に対応
11
+ - **役判定** — 一般役から役満まで全役対応(ダブル役満はルール設定で切り替え可)
12
+ - **点数計算** — 符・翻計算、基本点・各プレイヤーへの支払い点算出、本場加算
13
+ - **流局処理** — 荒牌流局・途中流局(四風連打・四家リーチ・三家和・四槓散了)、ノーテン罰符計算
14
+ - **ゲーム進行** — 連荘・親流れ・オーラス終了条件・トビ判定・西入対応
15
+ - **順位計算** — ウマ・オカを含む最終スコア算出
16
+ - **CPU AI** — Easy / Normal / Hard の3段階
17
+ - **状態の永続化** — `RoundState` の JSON シリアライズ・デシリアライズ
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HenrikStephensen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # riichi_engine
2
+
3
+ Pure Ruby 実装のリーチ麻雀エンジン Gem です。
4
+
5
+ 局進行・和了判定・役判定・点数計算・順位計算を Rails・ActiveRecord 非依存で提供します。
6
+ host app から `RiichiEngine::API` を呼ぶだけで、麻雀の完全なドメインロジックを利用できます。
7
+
8
+ ---
9
+
10
+ ## 機能
11
+
12
+ | カテゴリ | 内容 |
13
+ |---|---|
14
+ | 局進行管理 | 牌山構築・配牌・自摸・打牌・鳴き(ポン/チー/カン)・リーチ処理 |
15
+ | 和了判定 | 通常和了形・七対子・国士無双 |
16
+ | 役判定 | 一般役から役満まで全役対応 |
17
+ | 点数計算 | 符・翻の計算、基本点・各プレイヤーへの支払い点算出 |
18
+ | 順位計算 | ウマ・オカを含む最終スコア計算 |
19
+ | CPU AI | Easy / Normal / Hard の3段階(host app 側での切り替え対応) |
20
+
21
+ ---
22
+
23
+ ## ドキュメント
24
+
25
+ | ドキュメント | 内容 |
26
+ |---|---|
27
+ | [概念・用語集](docs/concepts.md) | 牌記法・麻雀用語・アクション種別の解説 |
28
+ | [使用例](docs/usage_examples.md) | 局開始〜終了までのコード例 |
29
+ | [局ステートマシン](docs/state_machine.md) | フェーズ遷移・アクション優先順位・特殊シーケンスの解説 |
30
+ | [API リファレンス](docs/api_reference.md) | 公開メソッドの詳細仕様 |
31
+ | [公開 API ポリシー](docs/public_api_policy.md) | 安定保証の範囲と方針 |
32
+ | [Adapter 契約](docs/adapter_contract.md) | host app と gem の責務境界 |
33
+
34
+ ---
35
+
36
+ ## インストール
37
+
38
+ ```bash
39
+ bundle add riichi_engine
40
+ ```
41
+
42
+ または Gemfile に直接追加:
43
+
44
+ ```ruby
45
+ gem "riichi_engine"
46
+ ```
47
+
48
+ ---
49
+
50
+ ## クイックスタート
51
+
52
+ ### 1. 局を開始する
53
+
54
+ `RuleConfig` を作成し、局の初期情報を `GameSetupSnapshot` に詰めて `setup_round` を呼びます。
55
+
56
+ ```ruby
57
+ rule = Mahjong::Config::RuleConfig.new # デフォルトルール(東南戦・25000点持ち等)
58
+
59
+ snapshot = Mahjong::Snapshots::GameSetupSnapshot.new(
60
+ bakaze: "ton", # 東場
61
+ kyoku: 1, # 1局目
62
+ honba: 0,
63
+ kyoutaku: 0,
64
+ oya_index: 0, # seat 0 が親
65
+ scores: { 0 => 25_000, 1 => 25_000, 2 => 25_000, 3 => 25_000 }
66
+ )
67
+
68
+ state = RiichiEngine::API.setup_round(
69
+ game_snapshot: snapshot,
70
+ rule: rule,
71
+ seed: "round-001" # 牌山シード(省略可)
72
+ )
73
+ ```
74
+
75
+ ### 2. 自摸・打牌を進める
76
+
77
+ アクションを順に `apply_round_action` へ渡すことで局を進行させます。
78
+ 選択可能なアクション一覧は `available_round_actions` で取得できます。
79
+
80
+ ```ruby
81
+ # 自摸
82
+ RiichiEngine::API.apply_round_action(
83
+ state: state,
84
+ action: { type: :tsumo },
85
+ rule: rule
86
+ )
87
+
88
+ # 現在の手番プレイヤーが選べるアクションを取得
89
+ actions = RiichiEngine::API.available_round_actions(
90
+ state: state,
91
+ seat: state.current_seat,
92
+ rule: rule
93
+ )
94
+
95
+ # 打牌アクションを選んで適用
96
+ dahai = actions.find { |a| a[:type] == :dahai }
97
+
98
+ result = RiichiEngine::API.apply_round_action(
99
+ state: state,
100
+ action: { type: :dahai, seat: state.current_seat, tile: dahai[:tile] },
101
+ rule: rule
102
+ )
103
+ ```
104
+
105
+ `result` は `Mahjong::Results::RoundFlowResult` です。
106
+ `result.round_end?` が `true` のとき局が終了しており、`result.round_end_info` に終了情報が入ります。
107
+
108
+ ### 3. 局終了後に次局かゲーム終了かを判定する
109
+
110
+ ```ruby
111
+ flow_result = RiichiEngine::API.judge_next_game_round(
112
+ progress_snapshot: progress_snapshot,
113
+ round_end_info: result.round_end_info,
114
+ rule: rule
115
+ )
116
+
117
+ # flow_result.next_round? => true なら次局へ
118
+ # flow_result.game_end? => true ならゲーム終了
119
+ ```
120
+
121
+ ---
122
+
123
+ ## エラー
124
+
125
+ エンジンが発生させる例外は2種類です。host app 側の adapter で rescue してください。
126
+
127
+ ```ruby
128
+ RiichiEngine::API::InvalidActionError # 不正なアクション(不正な打牌・存在しない鳴き等)
129
+ RiichiEngine::API::EngineError # エンジン内部エラー
130
+ ```
131
+
132
+ ---
133
+
134
+ ## ルール設定
135
+
136
+ `RuleConfig.new` にハッシュを渡すことで細かい設定が可能です。
137
+
138
+ ```ruby
139
+ rule = Mahjong::Config::RuleConfig.new(
140
+ "game_type" => "hanchan", # "hanchan"(東南戦)or "tonpuu"(東風戦)
141
+ "initial_score" => 25_000,
142
+ "uma" => [20_000, 10_000, -10_000, -20_000],
143
+ "kuitan" => true, # 食い断あり
144
+ "double_ron" => true, # ダブロン
145
+ "tobi" => true # トビあり
146
+ )
147
+ ```
148
+
149
+ 設定項目の詳細は [API リファレンス](docs/api_reference.md#ruleconfigの設定項目) を参照してください。
150
+
151
+ ---
152
+
153
+ ## テスト実行
154
+
155
+ ```bash
156
+ bundle install --local
157
+ bundle exec rspec
158
+ ```
159
+
160
+ ---
161
+
162
+ ## ライセンス
163
+
164
+ MIT
@@ -0,0 +1,144 @@
1
+ # Adapter 契約
2
+
3
+ `riichi_engine` は pure Ruby のドメインロジックのみを持ち、副作用・永続化は一切持ちません。
4
+ host app 側の「adapter 層」がこの境界で両者をつなぐ役割を担います。
5
+
6
+ ---
7
+
8
+ ## 責務の境界
9
+
10
+ ### riichi_engine 側が持つもの
11
+
12
+ - 局進行ロジック
13
+ - 和了判定・役判定
14
+ - 点数計算・順位計算
15
+ - CPU AI の判断ロジック
16
+
17
+ ### host app 側が持つもの(gem に持ち込まないもの)
18
+
19
+ - ActiveRecord
20
+ - ActionCable / Turbo
21
+ - ActiveJob
22
+ - キャッシュバックエンド
23
+ - ロガー
24
+ - リクエスト / セッション / ユーザーコンテキスト
25
+
26
+ ---
27
+
28
+ ## host app 側 adapter の役割
29
+
30
+ host app には以下の adapter を実装します。各 adapter の責務を分けることで、gem のバージョンアップ時の修正箇所を局所化できます。
31
+
32
+ | Adapter | 責務 |
33
+ |---|---|
34
+ | `MahjongAdapter::RuleBuilder` | DB / room 設定から `RuleConfig` を生成する |
35
+ | `MahjongAdapter::SnapshotBuilder` | ActiveRecord モデルから各種 Snapshot を生成する |
36
+ | `MahjongAdapter::RoundEndInfoBuilder` | 局終了情報を `RoundEndInfo` に正規化する |
37
+ | `MahjongAdapter::FinalResultBuilder` | 最終結果にプレイヤー表示情報を合成する |
38
+ | `MahjongAdapter::RoundStateCache` | `RoundState` のキャッシュ保存・復元・削除を管理する |
39
+ | `MahjongAdapter::CpuAiFactory` | CPU レベルに応じた AI インスタンスを選択する |
40
+
41
+ ---
42
+
43
+ ## SnapshotBuilder の契約
44
+
45
+ ActiveRecord オブジェクトを pure な Snapshot へ変換します。
46
+
47
+ **生成対象:**
48
+
49
+ ```ruby
50
+ Mahjong::Snapshots::GameSetupSnapshot # 局開始時
51
+ Mahjong::Snapshots::GameProgressSnapshot # 局終了後の進行判定時
52
+ Mahjong::Snapshots::FinalResultSnapshot # ゲーム終了時の最終結果
53
+ Mahjong::Snapshots::WinEvaluationSnapshot # 和了評価時
54
+ ```
55
+
56
+ **制約:**
57
+
58
+ - Snapshot の値に ActiveRecord オブジェクトを含めない
59
+ - key は seat 番号基準で統一する
60
+ - `score` や `bakaze` などの値はドメイン側がそのまま読める形にする
61
+
62
+ ---
63
+
64
+ ## RuleBuilder の契約
65
+
66
+ host app の DB 設定や room/game 設定から `Mahjong::Config::RuleConfig` を作成します。
67
+
68
+ **制約:**
69
+
70
+ - `RuleConfig` に渡すのは plain Ruby の `Hash`(ActiveRecord モデルを渡さない)
71
+ - key の正規化は `RuleConfig` 側で行うため、adapter 側で意識しなくてよい
72
+
73
+ ```ruby
74
+ # OK: Hash で渡す
75
+ RuleConfig.new("kuitan" => room.rule_kuitan, "uma" => room.rule_uma)
76
+
77
+ # NG: モデルを直接渡す
78
+ RuleConfig.new(room)
79
+ ```
80
+
81
+ ---
82
+
83
+ ## RoundStateCache の契約
84
+
85
+ `RoundState` のシリアライズ・デシリアライズとキャッシュキーを管理します。
86
+
87
+ **制約:**
88
+
89
+ - キャッシュバックエンドの詳細は adapter 側で隠蔽する
90
+ - gem 側へは `json` 文字列のみを渡す
91
+ - バージョン管理・キャッシュ失効(invalidation)は host app 側で担う
92
+
93
+ ```ruby
94
+ # 保存
95
+ json = state.to_json
96
+ RoundStateCache.save(round_id: round.id, json:)
97
+
98
+ # 復元
99
+ json = RoundStateCache.load(round_id: round.id)
100
+ state = RiichiEngine::API.deserialize_round_state(json)
101
+ ```
102
+
103
+ ---
104
+
105
+ ## RoundEndInfoBuilder の契約
106
+
107
+ 局終了時の host app 文脈の情報を `Mahjong::Results::RoundEndInfo` に正規化します。
108
+
109
+ **担当する処理:**
110
+
111
+ - 流局時のテンパイ座席の算出(`RiichiEngine::API.tenpai_seats` を呼ぶ)
112
+ - ノーテン罰符計算のための API 呼び出し
113
+ - 和了評価のためのスナップショット組み立て
114
+
115
+ ---
116
+
117
+ ## FinalResultBuilder の契約
118
+
119
+ `RiichiEngine::API.calculate_final_results` が返す `FinalResult` に、host app 固有の表示情報を合成します。
120
+
121
+ **担当する情報:**
122
+
123
+ - `player_id`
124
+ - プレイヤー名
125
+ - 表示順・装飾に必要な補足情報
126
+
127
+ ---
128
+
129
+ ## Service / Job 層のルール
130
+
131
+ service / job が守るべきこと:
132
+
133
+ 1. gem の呼び出しは `RiichiEngine::API` を経由する
134
+ 2. API に渡すオブジェクトは adapter が生成したものだけにする
135
+ 3. DB 保存・Job enqueue・broadcast は service / job 側で行う
136
+ 4. gem の内部 namespace(`Mahjong::Flow::*` など)を直接 stub / mock しない
137
+
138
+ ```ruby
139
+ # OK: API 経由
140
+ result = RiichiEngine::API.apply_round_action(state:, action:, rule:)
141
+
142
+ # NG: 内部クラスを直接呼ぶ
143
+ result = Mahjong::Flow::Rounds::RoundFlow.apply_action(state:, action:, rule:)
144
+ ```