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.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +19 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +17 -0
- data/.github/pull_request_template.md +15 -0
- data/.github/workflows/ci.yml +22 -0
- data/.github/workflows/release.yml +31 -0
- data/.gitignore +6 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +164 -0
- data/docs/adapter_contract.md +144 -0
- data/docs/api_reference.md +309 -0
- data/docs/concepts.md +134 -0
- data/docs/public_api_policy.md +94 -0
- data/docs/state_machine.md +360 -0
- data/docs/usage_examples.md +265 -0
- data/lib/mahjong/config/rule_config.rb +53 -0
- data/lib/mahjong/cpu/ai/base_ai.rb +50 -0
- data/lib/mahjong/cpu/ai/easy_ai.rb +11 -0
- data/lib/mahjong/cpu/ai/hard_ai.rb +11 -0
- data/lib/mahjong/cpu/ai/normal_ai.rb +11 -0
- data/lib/mahjong/cpu/analysis/shanten.rb +112 -0
- data/lib/mahjong/cpu/analysis/tile_evaluator.rb +54 -0
- data/lib/mahjong/cpu/judges/naki_judge.rb +60 -0
- data/lib/mahjong/cpu/judges/riichi_judge.rb +42 -0
- data/lib/mahjong/cpu/selectors/dahai_selector.rb +97 -0
- data/lib/mahjong/errors/engine_error.rb +5 -0
- data/lib/mahjong/errors/invalid_action_error.rb +5 -0
- data/lib/mahjong/errors/invalid_state_error.rb +5 -0
- data/lib/mahjong/errors/tile_not_found_error.rb +5 -0
- data/lib/mahjong/flow/detectors/naki_detector.rb +82 -0
- data/lib/mahjong/flow/games/game_flow.rb +186 -0
- data/lib/mahjong/flow/resolvers/action_resolver.rb +22 -0
- data/lib/mahjong/flow/rounds/round_flow.rb +588 -0
- data/lib/mahjong/flow/validators/action_validator.rb +110 -0
- data/lib/mahjong/results/final_result.rb +11 -0
- data/lib/mahjong/results/game_flow_result.rb +20 -0
- data/lib/mahjong/results/game_result.rb +26 -0
- data/lib/mahjong/results/round_end_info.rb +18 -0
- data/lib/mahjong/results/round_flow_result.rb +18 -0
- data/lib/mahjong/scoring/calculators/fu_calculator.rb +68 -0
- data/lib/mahjong/scoring/calculators/score_calculator.rb +73 -0
- data/lib/mahjong/scoring/evaluators/win_evaluator.rb +111 -0
- data/lib/mahjong/scoring/judges/agari_judge.rb +68 -0
- data/lib/mahjong/scoring/judges/furiten_judge.rb +34 -0
- data/lib/mahjong/scoring/judges/machi_judge.rb +78 -0
- data/lib/mahjong/scoring/judges/yaku_judge.rb +161 -0
- data/lib/mahjong/scoring/parsers/hand_parser.rb +87 -0
- data/lib/mahjong/scoring/value_objects/score_result.rb +57 -0
- data/lib/mahjong/scoring/value_objects/yaku_context.rb +70 -0
- data/lib/mahjong/scoring/value_objects/yaku_entry.rb +7 -0
- data/lib/mahjong/scoring/value_objects/yaku_judge_result.rb +27 -0
- data/lib/mahjong/scoring/yaku/chankan.rb +6 -0
- data/lib/mahjong/scoring/yaku/chanta.rb +15 -0
- data/lib/mahjong/scoring/yaku/chiihou.rb +7 -0
- data/lib/mahjong/scoring/yaku/chiitoitsu.rb +6 -0
- data/lib/mahjong/scoring/yaku/chinitsu.rb +10 -0
- data/lib/mahjong/scoring/yaku/chinroutou.rb +6 -0
- data/lib/mahjong/scoring/yaku/chuuren_poutou.rb +20 -0
- data/lib/mahjong/scoring/yaku/daisangen.rb +8 -0
- data/lib/mahjong/scoring/yaku/daisuushii.rb +8 -0
- data/lib/mahjong/scoring/yaku/double_riichi.rb +6 -0
- data/lib/mahjong/scoring/yaku/haitei.rb +6 -0
- data/lib/mahjong/scoring/yaku/honitsu.rb +11 -0
- data/lib/mahjong/scoring/yaku/honroutou.rb +9 -0
- data/lib/mahjong/scoring/yaku/houtei.rb +6 -0
- data/lib/mahjong/scoring/yaku/iipeiko.rb +16 -0
- data/lib/mahjong/scoring/yaku/ikkitsuukan.rb +15 -0
- data/lib/mahjong/scoring/yaku/ippatsu.rb +6 -0
- data/lib/mahjong/scoring/yaku/junchan.rb +15 -0
- data/lib/mahjong/scoring/yaku/kokushi_musou.rb +6 -0
- data/lib/mahjong/scoring/yaku/menzen_tsumo.rb +6 -0
- data/lib/mahjong/scoring/yaku/pinfu.rb +16 -0
- data/lib/mahjong/scoring/yaku/riichi.rb +6 -0
- data/lib/mahjong/scoring/yaku/rinshan_kaihou.rb +6 -0
- data/lib/mahjong/scoring/yaku/ryanpeiko.rb +11 -0
- data/lib/mahjong/scoring/yaku/ryuuiisou.rb +6 -0
- data/lib/mahjong/scoring/yaku/san_ankou.rb +15 -0
- data/lib/mahjong/scoring/yaku/san_kantsu.rb +6 -0
- data/lib/mahjong/scoring/yaku/sanshoku_doujun.rb +15 -0
- data/lib/mahjong/scoring/yaku/sanshoku_doukou.rb +14 -0
- data/lib/mahjong/scoring/yaku/shousangen.rb +14 -0
- data/lib/mahjong/scoring/yaku/shousuushii.rb +14 -0
- data/lib/mahjong/scoring/yaku/suuankou.rb +15 -0
- data/lib/mahjong/scoring/yaku/suukantsu.rb +6 -0
- data/lib/mahjong/scoring/yaku/tanyao.rb +7 -0
- data/lib/mahjong/scoring/yaku/tenhou.rb +7 -0
- data/lib/mahjong/scoring/yaku/toitoihou.rb +7 -0
- data/lib/mahjong/scoring/yaku/tsuuiisou.rb +6 -0
- data/lib/mahjong/scoring/yaku/yakuhai.rb +27 -0
- data/lib/mahjong/snapshots/final_result_snapshot.rb +8 -0
- data/lib/mahjong/snapshots/game_progress_snapshot.rb +12 -0
- data/lib/mahjong/snapshots/game_setup_snapshot.rb +12 -0
- data/lib/mahjong/snapshots/win_evaluation_snapshot.rb +11 -0
- data/lib/mahjong/state/fuuro.rb +45 -0
- data/lib/mahjong/state/hand.rb +64 -0
- data/lib/mahjong/state/kawa.rb +68 -0
- data/lib/mahjong/state/mentsu.rb +55 -0
- data/lib/mahjong/state/round_state.rb +193 -0
- data/lib/mahjong/tiles/dora.rb +19 -0
- data/lib/mahjong/tiles/tile.rb +168 -0
- data/lib/mahjong/tiles/tile_set.rb +51 -0
- data/lib/mahjong/tiles/wall.rb +47 -0
- data/lib/mahjong/tiles/wanpai.rb +79 -0
- data/lib/riichi_engine/api.rb +62 -0
- data/lib/riichi_engine/version.rb +3 -0
- data/lib/riichi_engine.rb +15 -0
- data/riichi_engine.gemspec +32 -0
- metadata +207 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module Mahjong
|
|
2
|
+
module Scoring
|
|
3
|
+
module Parsers
|
|
4
|
+
class HandParser
|
|
5
|
+
# 手牌を全ての有効なメンツ分解パターンに分解する
|
|
6
|
+
def self.parse(tiles_34)
|
|
7
|
+
results = []
|
|
8
|
+
work = tiles_34.dup
|
|
9
|
+
|
|
10
|
+
34.times do |i|
|
|
11
|
+
next unless work[i] >= 2
|
|
12
|
+
|
|
13
|
+
work[i] -= 2
|
|
14
|
+
find_mentsu(work, 0, [], results, i)
|
|
15
|
+
work[i] += 2
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
results
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def self.find_mentsu(tiles_34, start, mentsu_list, results, jantou_index)
|
|
24
|
+
if tiles_34.all?(&:zero?)
|
|
25
|
+
results << ParsedHand.new(
|
|
26
|
+
jantou_index: jantou_index,
|
|
27
|
+
mentsu_indices: mentsu_list.dup
|
|
28
|
+
)
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
i = (start...34).find { |idx| tiles_34[idx] > 0 }
|
|
33
|
+
return unless i
|
|
34
|
+
|
|
35
|
+
if tiles_34[i] >= 3
|
|
36
|
+
tiles_34[i] -= 3
|
|
37
|
+
mentsu_list << { type: :koutsu, index: i }
|
|
38
|
+
find_mentsu(tiles_34, i, mentsu_list, results, jantou_index)
|
|
39
|
+
mentsu_list.pop
|
|
40
|
+
tiles_34[i] += 3
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if i < 27 && (i % 9) <= 6
|
|
44
|
+
if tiles_34[i] > 0 && tiles_34[i + 1] > 0 && tiles_34[i + 2] > 0
|
|
45
|
+
tiles_34[i] -= 1
|
|
46
|
+
tiles_34[i + 1] -= 1
|
|
47
|
+
tiles_34[i + 2] -= 1
|
|
48
|
+
mentsu_list << { type: :shuntsu, index: i }
|
|
49
|
+
find_mentsu(tiles_34, i, mentsu_list, results, jantou_index)
|
|
50
|
+
mentsu_list.pop
|
|
51
|
+
tiles_34[i] += 1
|
|
52
|
+
tiles_34[i + 1] += 1
|
|
53
|
+
tiles_34[i + 2] += 1
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class ParsedHand
|
|
60
|
+
attr_reader :jantou_index, :mentsu_indices
|
|
61
|
+
|
|
62
|
+
def initialize(jantou_index:, mentsu_indices:)
|
|
63
|
+
@jantou_index = jantou_index
|
|
64
|
+
@mentsu_indices = mentsu_indices.freeze
|
|
65
|
+
freeze
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def jantou_notation
|
|
69
|
+
Tiles::TileSet.from_index(jantou_index)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def mentsu_notations
|
|
73
|
+
mentsu_indices.map do |m|
|
|
74
|
+
case m[:type]
|
|
75
|
+
when :shuntsu
|
|
76
|
+
tiles = (0..2).map { |d| Tiles::TileSet.from_index(m[:index] + d) }
|
|
77
|
+
{ type: :shuntsu, tiles: tiles }
|
|
78
|
+
when :koutsu
|
|
79
|
+
tile = Tiles::TileSet.from_index(m[:index])
|
|
80
|
+
{ type: :koutsu, tiles: [tile, tile, tile] }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Mahjong
|
|
2
|
+
module Scoring
|
|
3
|
+
module ValueObjects
|
|
4
|
+
class ScoreResult
|
|
5
|
+
attr_reader :fu, :han, :base_points, :level, :is_oya,
|
|
6
|
+
:agari_type, :payments, :total, :kyoutaku_bonus, :honba, :is_yakuman
|
|
7
|
+
|
|
8
|
+
def initialize(**attrs)
|
|
9
|
+
attrs.each { |k, v| instance_variable_set(:"@#{k}", v) }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def display
|
|
13
|
+
parts = []
|
|
14
|
+
parts << "#{fu}符" if fu && level.empty?
|
|
15
|
+
parts << "#{han}翻" if han && !is_yakuman
|
|
16
|
+
parts << level unless level.empty?
|
|
17
|
+
parts << score_text
|
|
18
|
+
parts.join(" ")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def score_text
|
|
22
|
+
if agari_type == :tsumo
|
|
23
|
+
is_oya ? "#{payments[:ko_pays]}点オール" : "#{payments[:ko_pays]}/#{payments[:oya_pays]}点"
|
|
24
|
+
else
|
|
25
|
+
"#{payments[:ron_pay]}点"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def score_changes(winner_seat:, loser_seat:, oya_seat:)
|
|
30
|
+
changes = Hash.new(0)
|
|
31
|
+
|
|
32
|
+
if agari_type == :tsumo
|
|
33
|
+
4.times do |seat|
|
|
34
|
+
next if seat == winner_seat
|
|
35
|
+
|
|
36
|
+
pay = if is_oya
|
|
37
|
+
payments[:ko_pays]
|
|
38
|
+
elsif seat == oya_seat
|
|
39
|
+
payments[:oya_pays]
|
|
40
|
+
else
|
|
41
|
+
payments[:ko_pays]
|
|
42
|
+
end
|
|
43
|
+
changes[seat] -= pay
|
|
44
|
+
changes[winner_seat] += pay
|
|
45
|
+
end
|
|
46
|
+
else
|
|
47
|
+
changes[loser_seat] -= payments[:ron_pay]
|
|
48
|
+
changes[winner_seat] += payments[:ron_pay]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
changes[winner_seat] += kyoutaku_bonus
|
|
52
|
+
changes
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Mahjong
|
|
2
|
+
module Scoring
|
|
3
|
+
module ValueObjects
|
|
4
|
+
class YakuContext
|
|
5
|
+
attr_reader :agari_form, :agari_tile, :agari_type, :hand_tiles, :fuuros, :is_menzen,
|
|
6
|
+
:jikaze, :bakaze, :is_riichi, :is_double_riichi, :is_ippatsu, :is_first_turn,
|
|
7
|
+
:is_haitei, :is_houtei, :is_rinshan, :is_chankan, :dora_tiles, :uradora_tiles,
|
|
8
|
+
:machi_type, :rule
|
|
9
|
+
|
|
10
|
+
def initialize(**attrs)
|
|
11
|
+
attrs.each { |k, v| instance_variable_set(:"@#{k}", v) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def with_agari_form(form)
|
|
15
|
+
self.class.new(
|
|
16
|
+
agari_form: form,
|
|
17
|
+
agari_tile: agari_tile,
|
|
18
|
+
agari_type: agari_type,
|
|
19
|
+
hand_tiles: hand_tiles,
|
|
20
|
+
fuuros: fuuros,
|
|
21
|
+
is_menzen: is_menzen,
|
|
22
|
+
jikaze: jikaze,
|
|
23
|
+
bakaze: bakaze,
|
|
24
|
+
is_riichi: is_riichi,
|
|
25
|
+
is_double_riichi: is_double_riichi,
|
|
26
|
+
is_ippatsu: is_ippatsu,
|
|
27
|
+
is_first_turn: is_first_turn,
|
|
28
|
+
is_haitei: is_haitei,
|
|
29
|
+
is_houtei: is_houtei,
|
|
30
|
+
is_rinshan: is_rinshan,
|
|
31
|
+
is_chankan: is_chankan,
|
|
32
|
+
dora_tiles: dora_tiles,
|
|
33
|
+
uradora_tiles: uradora_tiles,
|
|
34
|
+
machi_type: machi_type,
|
|
35
|
+
rule: rule
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def all_tiles
|
|
40
|
+
hand_tiles + fuuros.flat_map(&:tiles)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def all_mentsu
|
|
44
|
+
return [] unless agari_form&.regular?
|
|
45
|
+
|
|
46
|
+
parsed_mentsu = agari_form.parsed_hand.mentsu_notations.map do |m|
|
|
47
|
+
tiles = m[:tiles].map { |n| Tiles::Tile.new(n) }
|
|
48
|
+
State::Mentsu.new(type: m[:type], tiles: tiles, state: :closed)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
parsed_mentsu + fuuros.map(&:to_mentsu)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def jantou_tile
|
|
55
|
+
return nil unless agari_form&.regular?
|
|
56
|
+
|
|
57
|
+
Tiles::Tile.new(Tiles::TileSet.from_index(agari_form.parsed_hand.jantou_index))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def jikaze_yakuhai?(tile)
|
|
61
|
+
tile.kazehai? && Tiles::Tile::KAZE_NAMES[tile.number] == jikaze
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def bakaze_yakuhai?(tile)
|
|
65
|
+
tile.kazehai? && Tiles::Tile::KAZE_NAMES[tile.number] == bakaze
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Mahjong
|
|
2
|
+
module Scoring
|
|
3
|
+
module ValueObjects
|
|
4
|
+
class YakuJudgeResult
|
|
5
|
+
attr_reader :yaku, :is_yakuman, :total_han
|
|
6
|
+
|
|
7
|
+
def initialize(yaku:, is_yakuman:, total_han:)
|
|
8
|
+
@yaku = yaku
|
|
9
|
+
@is_yakuman = is_yakuman
|
|
10
|
+
@total_han = total_han
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def yakuman?
|
|
14
|
+
is_yakuman
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def has_yaku?
|
|
18
|
+
yaku.any?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_a
|
|
22
|
+
yaku.map { |y| { name: y.name, han: y.han } }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Mahjong::Scoring::Yaku::Chanta
|
|
2
|
+
def self.check(ctx)
|
|
3
|
+
return nil unless ctx.agari_form&.regular?
|
|
4
|
+
|
|
5
|
+
mentsu = ctx.all_mentsu
|
|
6
|
+
jantou = ctx.jantou_tile
|
|
7
|
+
|
|
8
|
+
return nil unless mentsu.all?(&:includes_yaochu?) && jantou.yaochu?
|
|
9
|
+
return nil unless mentsu.any?(&:shuntsu?)
|
|
10
|
+
return nil unless ctx.all_tiles.any?(&:jihai?)
|
|
11
|
+
|
|
12
|
+
han = ctx.is_menzen ? 2 : 1
|
|
13
|
+
::Mahjong::Scoring::ValueObjects::YakuEntry.new(name: "chanta", han: han, display: "混全帯么九")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Mahjong::Scoring::Yaku::Chinitsu
|
|
2
|
+
def self.check(ctx)
|
|
3
|
+
all = ctx.all_tiles
|
|
4
|
+
return nil if all.any?(&:jihai?)
|
|
5
|
+
return nil unless all.map(&:suit).uniq.size == 1
|
|
6
|
+
|
|
7
|
+
han = ctx.is_menzen ? 6 : 5
|
|
8
|
+
::Mahjong::Scoring::ValueObjects::YakuEntry.new(name: "chinitsu", han: han, display: "清一色")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Mahjong::Scoring::Yaku::ChuurenPoutou
|
|
2
|
+
def self.check(ctx)
|
|
3
|
+
return nil unless ctx.is_menzen
|
|
4
|
+
|
|
5
|
+
all = ctx.all_tiles
|
|
6
|
+
return nil if all.any?(&:jihai?)
|
|
7
|
+
|
|
8
|
+
suits = all.map(&:suit).uniq
|
|
9
|
+
return nil unless suits.size == 1
|
|
10
|
+
|
|
11
|
+
counts = Array.new(10, 0)
|
|
12
|
+
all.each { |t| counts[t.effective_number] += 1 }
|
|
13
|
+
|
|
14
|
+
# 1112345678999 + 任意1枚 の形
|
|
15
|
+
base = [3, 1, 1, 1, 1, 1, 1, 1, 3]
|
|
16
|
+
(1..9).each { |n| return nil if counts[n] < base[n - 1] }
|
|
17
|
+
|
|
18
|
+
::Mahjong::Scoring::ValueObjects::YakuEntry.new(name: "chuuren_poutou", han: -1, display: "九蓮宝燈")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
module Mahjong::Scoring::Yaku::Daisangen
|
|
2
|
+
def self.check(ctx)
|
|
3
|
+
return nil unless ctx.agari_form&.regular?
|
|
4
|
+
count = ctx.all_mentsu.count { |m| (m.koutsu? || m.kantsu?) && m.representative_tile.sangenpai? }
|
|
5
|
+
return nil unless count == 3
|
|
6
|
+
::Mahjong::Scoring::ValueObjects::YakuEntry.new(name: "daisangen", han: -1, display: "大三元")
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
module Mahjong::Scoring::Yaku::Daisuushii
|
|
2
|
+
def self.check(ctx)
|
|
3
|
+
return nil unless ctx.agari_form&.regular?
|
|
4
|
+
count = ctx.all_mentsu.count { |m| (m.koutsu? || m.kantsu?) && m.representative_tile.kazehai? }
|
|
5
|
+
return nil unless count == 4
|
|
6
|
+
::Mahjong::Scoring::ValueObjects::YakuEntry.new(name: "daisuushii", han: -1, display: "大四喜")
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Mahjong::Scoring::Yaku::Honitsu
|
|
2
|
+
def self.check(ctx)
|
|
3
|
+
all = ctx.all_tiles
|
|
4
|
+
suits = all.select(&:suuhai?).map(&:suit).uniq
|
|
5
|
+
return nil unless suits.size == 1
|
|
6
|
+
return nil unless all.any?(&:jihai?)
|
|
7
|
+
|
|
8
|
+
han = ctx.is_menzen ? 3 : 2
|
|
9
|
+
::Mahjong::Scoring::ValueObjects::YakuEntry.new(name: "honitsu", han: han, display: "混一色")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module Mahjong::Scoring::Yaku::Honroutou
|
|
2
|
+
def self.check(ctx)
|
|
3
|
+
all = ctx.all_tiles
|
|
4
|
+
return nil unless all.all?(&:yaochu?)
|
|
5
|
+
return nil unless all.any?(&:jihai?)
|
|
6
|
+
return nil unless all.any?(&:routou?)
|
|
7
|
+
::Mahjong::Scoring::ValueObjects::YakuEntry.new(name: "honroutou", han: 2, display: "混老頭")
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Mahjong::Scoring::Yaku::Iipeiko
|
|
2
|
+
def self.check(ctx)
|
|
3
|
+
return nil unless ctx.is_menzen
|
|
4
|
+
return nil unless ctx.agari_form&.regular?
|
|
5
|
+
|
|
6
|
+
shuntsu = ctx.all_mentsu.select(&:shuntsu?)
|
|
7
|
+
return nil unless count_shuntsu_pairs(shuntsu) == 1
|
|
8
|
+
|
|
9
|
+
::Mahjong::Scoring::ValueObjects::YakuEntry.new(name: "iipeiko", han: 1, display: "一盃口")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.count_shuntsu_pairs(shuntsu_list)
|
|
13
|
+
keys = shuntsu_list.map { |m| t = m.representative_tile; [t.suit, t.effective_number] }
|
|
14
|
+
keys.tally.values.count { |c| c >= 2 }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Mahjong::Scoring::Yaku::Ikkitsuukan
|
|
2
|
+
def self.check(ctx)
|
|
3
|
+
return nil unless ctx.agari_form&.regular?
|
|
4
|
+
|
|
5
|
+
numbers = ctx.all_mentsu.select(&:shuntsu?).map do |m|
|
|
6
|
+
[m.representative_tile.suit, m.representative_tile.effective_number]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
found = %w[m p s].any? { |s| [1, 4, 7].all? { |n| numbers.include?([s, n]) } }
|
|
10
|
+
return nil unless found
|
|
11
|
+
|
|
12
|
+
han = ctx.is_menzen ? 2 : 1
|
|
13
|
+
::Mahjong::Scoring::ValueObjects::YakuEntry.new(name: "ikkitsuukan", han: han, display: "一気通貫")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Mahjong::Scoring::Yaku::Junchan
|
|
2
|
+
def self.check(ctx)
|
|
3
|
+
return nil unless ctx.agari_form&.regular?
|
|
4
|
+
|
|
5
|
+
mentsu = ctx.all_mentsu
|
|
6
|
+
jantou = ctx.jantou_tile
|
|
7
|
+
|
|
8
|
+
return nil unless mentsu.all?(&:includes_yaochu?) && jantou.routou?
|
|
9
|
+
return nil unless mentsu.any?(&:shuntsu?)
|
|
10
|
+
return nil if ctx.all_tiles.any?(&:jihai?)
|
|
11
|
+
|
|
12
|
+
han = ctx.is_menzen ? 3 : 2
|
|
13
|
+
::Mahjong::Scoring::ValueObjects::YakuEntry.new(name: "junchan", han: han, display: "純全帯么九")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Mahjong::Scoring::Yaku::Pinfu
|
|
2
|
+
def self.check(ctx)
|
|
3
|
+
return nil unless ctx.is_menzen
|
|
4
|
+
return nil unless ctx.agari_form&.regular?
|
|
5
|
+
return nil unless ctx.machi_type == :ryanmen
|
|
6
|
+
|
|
7
|
+
return nil unless ctx.all_mentsu.all?(&:shuntsu?)
|
|
8
|
+
|
|
9
|
+
jt = ctx.jantou_tile
|
|
10
|
+
return nil if jt.sangenpai?
|
|
11
|
+
return nil if ctx.jikaze_yakuhai?(jt)
|
|
12
|
+
return nil if ctx.bakaze_yakuhai?(jt)
|
|
13
|
+
|
|
14
|
+
::Mahjong::Scoring::ValueObjects::YakuEntry.new(name: "pinfu", han: 1, display: "平和")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Mahjong::Scoring::Yaku::Ryanpeiko
|
|
2
|
+
def self.check(ctx)
|
|
3
|
+
return nil unless ctx.is_menzen
|
|
4
|
+
return nil unless ctx.agari_form&.regular?
|
|
5
|
+
|
|
6
|
+
shuntsu = ctx.all_mentsu.select(&:shuntsu?)
|
|
7
|
+
return nil unless ::Mahjong::Scoring::Yaku::Iipeiko.count_shuntsu_pairs(shuntsu) == 2
|
|
8
|
+
|
|
9
|
+
::Mahjong::Scoring::ValueObjects::YakuEntry.new(name: "ryanpeiko", han: 3, display: "二盃口")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Mahjong::Scoring::Yaku::SanAnkou
|
|
2
|
+
def self.check(ctx)
|
|
3
|
+
return nil unless ctx.agari_form&.regular?
|
|
4
|
+
|
|
5
|
+
ankou_count = ctx.all_mentsu.count { |m| (m.koutsu? || m.kantsu?) && m.closed? }
|
|
6
|
+
|
|
7
|
+
# ロンかつ双碰の場合、和了牌で完成した刻子は明刻扱い
|
|
8
|
+
if ctx.agari_type == :ron && ctx.machi_type == :shanpon
|
|
9
|
+
ankou_count -= 1 if ankou_count > 0
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
return nil unless ankou_count >= 3
|
|
13
|
+
::Mahjong::Scoring::ValueObjects::YakuEntry.new(name: "san_ankou", han: 2, display: "三暗刻")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Mahjong::Scoring::Yaku::SanshokuDoujun
|
|
2
|
+
def self.check(ctx)
|
|
3
|
+
return nil unless ctx.agari_form&.regular?
|
|
4
|
+
|
|
5
|
+
numbers = ctx.all_mentsu.select(&:shuntsu?).map do |m|
|
|
6
|
+
[m.representative_tile.suit, m.representative_tile.effective_number]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
found = (1..7).any? { |n| %w[m p s].all? { |s| numbers.include?([s, n]) } }
|
|
10
|
+
return nil unless found
|
|
11
|
+
|
|
12
|
+
han = ctx.is_menzen ? 2 : 1
|
|
13
|
+
::Mahjong::Scoring::ValueObjects::YakuEntry.new(name: "sanshoku_doujun", han: han, display: "三色同順")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Mahjong::Scoring::Yaku::SanshokuDoukou
|
|
2
|
+
def self.check(ctx)
|
|
3
|
+
return nil unless ctx.agari_form&.regular?
|
|
4
|
+
|
|
5
|
+
numbers = ctx.all_mentsu.select { |m| m.koutsu? || m.kantsu? }.map do |m|
|
|
6
|
+
[m.representative_tile.suit, m.representative_tile.effective_number]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
found = (1..9).any? { |n| %w[m p s].all? { |s| numbers.include?([s, n]) } }
|
|
10
|
+
return nil unless found
|
|
11
|
+
|
|
12
|
+
::Mahjong::Scoring::ValueObjects::YakuEntry.new(name: "sanshoku_doukou", han: 2, display: "三色同刻")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Mahjong::Scoring::Yaku::Shousangen
|
|
2
|
+
def self.check(ctx)
|
|
3
|
+
return nil unless ctx.agari_form&.regular?
|
|
4
|
+
|
|
5
|
+
mentsu = ctx.all_mentsu
|
|
6
|
+
jantou = ctx.jantou_tile
|
|
7
|
+
|
|
8
|
+
sangen_koutsu = mentsu.count { |m| (m.koutsu? || m.kantsu?) && m.representative_tile.sangenpai? }
|
|
9
|
+
sangen_jantou = jantou&.sangenpai? ? 1 : 0
|
|
10
|
+
|
|
11
|
+
return nil unless sangen_koutsu == 2 && sangen_jantou == 1
|
|
12
|
+
::Mahjong::Scoring::ValueObjects::YakuEntry.new(name: "shousangen", han: 2, display: "小三元")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Mahjong::Scoring::Yaku::Shousuushii
|
|
2
|
+
def self.check(ctx)
|
|
3
|
+
return nil unless ctx.agari_form&.regular?
|
|
4
|
+
|
|
5
|
+
mentsu = ctx.all_mentsu
|
|
6
|
+
jantou = ctx.jantou_tile
|
|
7
|
+
|
|
8
|
+
kaze_koutsu = mentsu.count { |m| (m.koutsu? || m.kantsu?) && m.representative_tile.kazehai? }
|
|
9
|
+
kaze_jantou = jantou&.kazehai? ? 1 : 0
|
|
10
|
+
|
|
11
|
+
return nil unless kaze_koutsu == 3 && kaze_jantou == 1
|
|
12
|
+
::Mahjong::Scoring::ValueObjects::YakuEntry.new(name: "shousuushii", han: -1, display: "小四喜")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Mahjong::Scoring::Yaku::Suuankou
|
|
2
|
+
def self.check(ctx)
|
|
3
|
+
return nil unless ctx.agari_form&.regular?
|
|
4
|
+
return nil unless ctx.is_menzen
|
|
5
|
+
|
|
6
|
+
ankou_count = ctx.all_mentsu.count { |m| (m.koutsu? || m.kantsu?) && m.closed? }
|
|
7
|
+
|
|
8
|
+
if ctx.agari_type == :ron && ctx.machi_type == :shanpon
|
|
9
|
+
ankou_count -= 1
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
return nil unless ankou_count >= 4
|
|
13
|
+
::Mahjong::Scoring::ValueObjects::YakuEntry.new(name: "suuankou", han: -1, display: "四暗刻")
|
|
14
|
+
end
|
|
15
|
+
end
|