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
@@ -0,0 +1,168 @@
1
+ module Mahjong
2
+ module Tiles
3
+ class Tile
4
+ SUITS = %w[m p s z].freeze
5
+ MANZU = "m"
6
+ PINZU = "p"
7
+ SOUZU = "s"
8
+ JIHAI = "z"
9
+
10
+ # z の番号対応: 1=東, 2=南, 3=西, 4=北, 5=白, 6=發, 7=中
11
+ KAZE_NAMES = { 1 => "ton", 2 => "nan", 3 => "sha", 4 => "pei" }.freeze
12
+ SANGEN_NAMES = { 5 => "haku", 6 => "hatsu", 7 => "chun" }.freeze
13
+
14
+ attr_reader :suit, :number, :is_aka
15
+
16
+ # @param notation [String] "1m", "5p", "0s"(赤5), "1z"(東) 等
17
+ def initialize(notation)
18
+ @original = notation.to_s.freeze
19
+ parse!(notation)
20
+ freeze
21
+ end
22
+
23
+ def suuhai?
24
+ suit != JIHAI
25
+ end
26
+
27
+ def jihai?
28
+ suit == JIHAI
29
+ end
30
+
31
+ def kazehai?
32
+ jihai? && number <= 4
33
+ end
34
+
35
+ def sangenpai?
36
+ jihai? && number >= 5
37
+ end
38
+
39
+ def yaochu?
40
+ jihai? || number == 1 || number == 9
41
+ end
42
+
43
+ def chunchan?
44
+ suuhai? && number >= 2 && number <= 8
45
+ end
46
+
47
+ def routou?
48
+ suuhai? && (number == 1 || number == 9)
49
+ end
50
+
51
+ def green?
52
+ (suit == SOUZU && [2, 3, 4, 6, 8].include?(number)) ||
53
+ (suit == JIHAI && number == 6)
54
+ end
55
+
56
+ def aka?
57
+ is_aka
58
+ end
59
+
60
+ def effective_number
61
+ aka? ? 5 : number
62
+ end
63
+
64
+ def same_kind?(other)
65
+ suit == other.suit && effective_number == other.effective_number
66
+ end
67
+
68
+ def ==(other)
69
+ return false unless other.is_a?(Tile)
70
+
71
+ suit == other.suit && number == other.number && is_aka == other.is_aka
72
+ end
73
+ alias eql? ==
74
+
75
+ def hash
76
+ [suit, number, is_aka].hash
77
+ end
78
+
79
+ def <=>(other)
80
+ return nil unless other.is_a?(Tile)
81
+
82
+ [suit_order, effective_number] <=> [other.suit_order, other.effective_number]
83
+ end
84
+ include Comparable
85
+
86
+ def to_s
87
+ aka? ? "0#{suit}" : "#{number}#{suit}"
88
+ end
89
+
90
+ def inspect
91
+ "#<Tile:#{to_s}>"
92
+ end
93
+
94
+ def japanese_name
95
+ case suit
96
+ when MANZU then "#{effective_number}萬"
97
+ when PINZU then "#{effective_number}筒"
98
+ when SOUZU then "#{effective_number}索"
99
+ when JIHAI
100
+ %w[_ 東 南 西 北 白 發 中][number]
101
+ end
102
+ end
103
+
104
+ def next_tile_notation
105
+ case suit
106
+ when MANZU, PINZU, SOUZU
107
+ next_num = (effective_number % 9) + 1
108
+ "#{next_num}#{suit}"
109
+ when JIHAI
110
+ if kazehai?
111
+ kaze_order = [1, 2, 3, 4]
112
+ next_idx = (kaze_order.index(number) + 1) % 4
113
+ "#{kaze_order[next_idx]}#{suit}"
114
+ else
115
+ sangen_order = [5, 6, 7]
116
+ next_idx = (sangen_order.index(number) + 1) % 3
117
+ "#{sangen_order[next_idx]}#{suit}"
118
+ end
119
+ end
120
+ end
121
+
122
+ def suit_order
123
+ { MANZU => 0, PINZU => 1, SOUZU => 2, JIHAI => 3 }[suit]
124
+ end
125
+
126
+ def self.from(notation)
127
+ new(notation)
128
+ end
129
+
130
+ def self.all_tiles(aka_config = { "m" => 1, "p" => 1, "s" => 1 })
131
+ tiles = []
132
+ %w[m p s].each do |s|
133
+ (1..9).each do |n|
134
+ aka_count = (n == 5) ? (aka_config[s] || 0) : 0
135
+ normal_count = 4 - aka_count
136
+ normal_count.times { tiles << new("#{n}#{s}") }
137
+ aka_count.times { tiles << new("0#{s}") }
138
+ end
139
+ end
140
+ (1..7).each do |n|
141
+ 4.times { tiles << new("#{n}z") }
142
+ end
143
+ tiles
144
+ end
145
+
146
+ private
147
+
148
+ def parse!(notation)
149
+ str = notation.to_s.strip
150
+ raise ArgumentError, "Invalid tile notation: #{notation}" unless str.match?(/\A[0-9][mpsz]\z/)
151
+
152
+ @number = str[0].to_i
153
+ @suit = str[1]
154
+ @is_aka = (@number == 0)
155
+
156
+ validate_range!
157
+ end
158
+
159
+ def validate_range!
160
+ if suit == JIHAI
161
+ raise ArgumentError, "Invalid jihai number: #{number}" unless (1..7).include?(number)
162
+ else
163
+ raise ArgumentError, "Invalid suuhai number: #{number}" unless (0..9).include?(number)
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,51 @@
1
+ module Mahjong
2
+ module Tiles
3
+ class TileSet
4
+ YAOCHU_INDICES = [0, 8, 9, 17, 18, 26, 27, 28, 29, 30, 31, 32, 33].freeze
5
+
6
+ def self.to_34(tiles)
7
+ counts = Array.new(34, 0)
8
+ tiles.each { |t| counts[tile_index(t)] += 1 }
9
+ counts
10
+ end
11
+
12
+ def self.tile_index(tile)
13
+ num = tile.effective_number
14
+ case tile.suit
15
+ when "m" then num - 1
16
+ when "p" then num + 8
17
+ when "s" then num + 17
18
+ when "z" then num + 26
19
+ end
20
+ end
21
+
22
+ def self.from_index(idx)
23
+ case idx
24
+ when 0..8 then "#{idx + 1}m"
25
+ when 9..17 then "#{idx - 8}p"
26
+ when 18..26 then "#{idx - 17}s"
27
+ when 27..33 then "#{idx - 26}z"
28
+ end
29
+ end
30
+
31
+ def self.sort(tiles)
32
+ tiles.sort
33
+ end
34
+
35
+ def self.count_same(tiles, target)
36
+ tiles.count { |t| t.same_kind?(target) }
37
+ end
38
+
39
+ def self.remove_one(tiles, target)
40
+ idx = tiles.index { |t| t.same_kind?(target) }
41
+ return tiles.dup unless idx
42
+
43
+ tiles.dup.tap { |a| a.delete_at(idx) }
44
+ end
45
+
46
+ def self.yaochu_kind_count(tiles)
47
+ tiles.select(&:yaochu?).map { |t| [t.suit, t.effective_number] }.uniq.count
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,47 @@
1
+ module Mahjong
2
+ module Tiles
3
+ class Wall
4
+ attr_reader :tiles
5
+
6
+ def initialize(tiles)
7
+ @tiles = tiles.dup
8
+ end
9
+
10
+ def draw!
11
+ raise Errors::EngineError, "Wall is empty" if @tiles.empty?
12
+
13
+ @tiles.shift
14
+ end
15
+
16
+ def draw_many!(count)
17
+ raise Errors::EngineError, "Not enough tiles in wall" if @tiles.size < count
18
+
19
+ @tiles.shift(count)
20
+ end
21
+
22
+ def draw_from_end!
23
+ raise Errors::EngineError, "Wall is empty" if @tiles.empty?
24
+
25
+ @tiles.pop
26
+ end
27
+
28
+ def remaining
29
+ @tiles.size
30
+ end
31
+
32
+ def empty?
33
+ @tiles.empty?
34
+ end
35
+
36
+ def to_a
37
+ @tiles.map(&:to_s)
38
+ end
39
+
40
+ def self.build(aka_config: { "m" => 1, "p" => 1, "s" => 1 }, seed: nil)
41
+ all = Tile.all_tiles(aka_config)
42
+ rng = seed ? Random.new(seed.to_i(36)) : Random.new
43
+ new(all.shuffle(random: rng))
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,79 @@
1
+ module Mahjong
2
+ module Tiles
3
+ class Wanpai
4
+ TOTAL = 14
5
+ MAX_RINSHAN = 4
6
+ MAX_DORA_HYOUJI = 5
7
+
8
+ attr_reader :rinshan_tiles, :dora_hyouji, :uradora_hyouji
9
+
10
+ def initialize(tiles)
11
+ raise Errors::EngineError, "Wanpai must have #{TOTAL} tiles" unless tiles.size == TOTAL
12
+
13
+ @rinshan_tiles = tiles[0..3].dup
14
+ @dora_hyouji = [tiles[4]]
15
+ @uradora_hyouji = [tiles[9]]
16
+ @remaining_dora = tiles[5..8].dup
17
+ @remaining_uradora = tiles[10..13].dup
18
+ end
19
+
20
+ def draw_rinshan!
21
+ raise Errors::EngineError, "No rinshan tiles left" if @rinshan_tiles.empty?
22
+
23
+ @rinshan_tiles.shift
24
+ end
25
+
26
+ def reveal_new_dora!
27
+ raise Errors::EngineError, "No more dora to reveal" if @remaining_dora.empty?
28
+
29
+ new_dora = @remaining_dora.shift
30
+ new_uradora = @remaining_uradora.shift
31
+ @dora_hyouji << new_dora
32
+ @uradora_hyouji << new_uradora if new_uradora
33
+ new_dora
34
+ end
35
+
36
+ def dora_tiles
37
+ @dora_hyouji.map { |t| Tiles::Tile.new(t.next_tile_notation) }
38
+ end
39
+
40
+ def uradora_tiles
41
+ @uradora_hyouji.map { |t| Tiles::Tile.new(t.next_tile_notation) }
42
+ end
43
+
44
+ def rinshan_remaining
45
+ @rinshan_tiles.size
46
+ end
47
+
48
+ def dora_revealable?
49
+ @remaining_dora.any? && @remaining_uradora.any?
50
+ end
51
+
52
+ def to_h
53
+ {
54
+ rinshan: @rinshan_tiles.map(&:to_s),
55
+ dora_hyouji: @dora_hyouji.map(&:to_s),
56
+ uradora_hyouji: @uradora_hyouji.map(&:to_s),
57
+ remaining_dora: @remaining_dora.map(&:to_s),
58
+ remaining_uradora: @remaining_uradora.map(&:to_s)
59
+ }
60
+ end
61
+
62
+ def self.from_hash(h)
63
+ rinshan = (h["rinshan"] || h[:rinshan] || []).map { |s| Tiles::Tile.new(s) }
64
+ dh = (h["dora_hyouji"] || h[:dora_hyouji] || []).map { |s| Tiles::Tile.new(s) }
65
+ uh = (h["uradora_hyouji"] || h[:uradora_hyouji] || []).map { |s| Tiles::Tile.new(s) }
66
+ remaining_dora = (h["remaining_dora"] || h[:remaining_dora] || []).map { |s| Tiles::Tile.new(s) }
67
+ remaining_uradora = (h["remaining_uradora"] || h[:remaining_uradora] || []).map { |s| Tiles::Tile.new(s) }
68
+
69
+ obj = allocate
70
+ obj.instance_variable_set(:@rinshan_tiles, rinshan)
71
+ obj.instance_variable_set(:@dora_hyouji, dh)
72
+ obj.instance_variable_set(:@uradora_hyouji, uh)
73
+ obj.instance_variable_set(:@remaining_dora, remaining_dora)
74
+ obj.instance_variable_set(:@remaining_uradora, remaining_uradora)
75
+ obj
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,62 @@
1
+ module RiichiEngine
2
+ module API
3
+ InvalidActionError = Mahjong::Errors::InvalidActionError
4
+ EngineError = Mahjong::Errors::EngineError
5
+
6
+ def self.setup_round(game_snapshot:, rule:, seed: nil)
7
+ Mahjong::Flow::Rounds::RoundFlow.setup(game_snapshot:, rule:, seed:)
8
+ end
9
+
10
+ def self.apply_round_action(state:, action:, rule:)
11
+ Mahjong::Flow::Rounds::RoundFlow.apply_action(state:, action:, rule:)
12
+ end
13
+
14
+ def self.available_round_actions(state:, seat:, rule:)
15
+ Mahjong::Flow::Rounds::RoundFlow.available_actions(state:, seat:, rule:)
16
+ end
17
+
18
+ def self.resolve_pending_action(pending_actions)
19
+ Mahjong::Flow::Resolvers::ActionResolver.resolve(pending_actions)
20
+ end
21
+
22
+ def self.judge_next_game_round(progress_snapshot:, round_end_info:, rule:)
23
+ Mahjong::Flow::Games::GameFlow.judge_next(progress_snapshot:, round_end_info:, rule:)
24
+ end
25
+
26
+ def self.calculate_final_results(final_result_snapshot:, rule:)
27
+ Mahjong::Flow::Games::GameFlow.calculate_final_result(final_result_snapshot:, rule:)
28
+ end
29
+
30
+ def self.calculate_ranked_results(scores:, rule:)
31
+ Mahjong::Results::GameResult.calculate(scores:, rule:)
32
+ end
33
+
34
+ def self.calculate_tenpai_payments(tenpai_seats)
35
+ Mahjong::Flow::Games::GameFlow.calculate_tenpai_payments(tenpai_seats)
36
+ end
37
+
38
+ def self.tenpai_seats(state)
39
+ 4.times.select { |seat| Mahjong::Scoring::Judges::MachiJudge.tenpai?(state.hands[seat].to_34) }
40
+ end
41
+
42
+ def self.evaluate_win(state:, seat:, agari_type:, agari_tile:, rule:, extra_flags:, snapshot:)
43
+ Mahjong::Scoring::Evaluators::WinEvaluator.evaluate(
44
+ state:,
45
+ seat:,
46
+ agari_type:,
47
+ agari_tile:,
48
+ rule:,
49
+ extra_flags:,
50
+ snapshot:
51
+ )
52
+ end
53
+
54
+ def self.agari?(tiles_34)
55
+ Mahjong::Scoring::Judges::AgariJudge.agari?(tiles_34)
56
+ end
57
+
58
+ def self.deserialize_round_state(json)
59
+ Mahjong::State::RoundState.from_json(json)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,3 @@
1
+ module RiichiEngine
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,15 @@
1
+ require "json"
2
+ require "zeitwerk"
3
+
4
+ module RiichiEngine
5
+ def self.loader
6
+ @loader ||= Zeitwerk::Loader.new.tap do |loader|
7
+ loader.tag = "riichi_engine"
8
+ loader.push_dir(__dir__)
9
+ loader.setup
10
+ end
11
+ end
12
+ end
13
+
14
+ RiichiEngine.loader
15
+ require_relative "riichi_engine/api"
@@ -0,0 +1,32 @@
1
+ require_relative "lib/riichi_engine/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "riichi_engine"
5
+ spec.version = RiichiEngine::VERSION
6
+ spec.summary = "Pure Ruby riichi mahjong engine"
7
+ spec.description = "Provides round progression, win detection, yaku evaluation, and score " \
8
+ "calculation for riichi mahjong, independent of Rails or ActiveRecord."
9
+ spec.authors = ["HenrikStephensen"]
10
+ spec.homepage = "https://github.com/HenrikStephensen/riichi_engine"
11
+ spec.license = "MIT"
12
+
13
+ spec.required_ruby_version = ">= 3.2"
14
+
15
+ spec.metadata = {
16
+ "source_code_uri" => "https://github.com/HenrikStephensen/riichi_engine",
17
+ "changelog_uri" => "https://github.com/HenrikStephensen/riichi_engine/blob/main/CHANGELOG.md",
18
+ "bug_tracker_uri" => "https://github.com/HenrikStephensen/riichi_engine/issues"
19
+ }
20
+
21
+ spec.files = Dir.chdir(__dir__) do
22
+ Dir["lib/**/*.rb"] + Dir["docs/**/*.md"] + Dir[".github/**/*"] +
23
+ ["README.md", "CHANGELOG.md", "LICENSE", "Gemfile", ".gitignore", ".ruby-version", "riichi_engine.gemspec"]
24
+ end
25
+ spec.require_paths = ["lib"]
26
+
27
+ spec.add_dependency "zeitwerk"
28
+
29
+ spec.add_development_dependency "rspec-core"
30
+ spec.add_development_dependency "rspec-expectations"
31
+ spec.add_development_dependency "rspec-mocks"
32
+ end
metadata ADDED
@@ -0,0 +1,207 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: riichi_engine
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - HenrikStephensen
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: zeitwerk
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec-core
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec-expectations
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rspec-mocks
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ description: Provides round progression, win detection, yaku evaluation, and score
69
+ calculation for riichi mahjong, independent of Rails or ActiveRecord.
70
+ executables: []
71
+ extensions: []
72
+ extra_rdoc_files: []
73
+ files:
74
+ - ".github/ISSUE_TEMPLATE/bug_report.md"
75
+ - ".github/ISSUE_TEMPLATE/feature_request.md"
76
+ - ".github/pull_request_template.md"
77
+ - ".github/workflows/ci.yml"
78
+ - ".github/workflows/release.yml"
79
+ - ".gitignore"
80
+ - ".ruby-version"
81
+ - CHANGELOG.md
82
+ - Gemfile
83
+ - LICENSE
84
+ - README.md
85
+ - docs/adapter_contract.md
86
+ - docs/api_reference.md
87
+ - docs/concepts.md
88
+ - docs/public_api_policy.md
89
+ - docs/state_machine.md
90
+ - docs/usage_examples.md
91
+ - lib/mahjong/config/rule_config.rb
92
+ - lib/mahjong/cpu/ai/base_ai.rb
93
+ - lib/mahjong/cpu/ai/easy_ai.rb
94
+ - lib/mahjong/cpu/ai/hard_ai.rb
95
+ - lib/mahjong/cpu/ai/normal_ai.rb
96
+ - lib/mahjong/cpu/analysis/shanten.rb
97
+ - lib/mahjong/cpu/analysis/tile_evaluator.rb
98
+ - lib/mahjong/cpu/judges/naki_judge.rb
99
+ - lib/mahjong/cpu/judges/riichi_judge.rb
100
+ - lib/mahjong/cpu/selectors/dahai_selector.rb
101
+ - lib/mahjong/errors/engine_error.rb
102
+ - lib/mahjong/errors/invalid_action_error.rb
103
+ - lib/mahjong/errors/invalid_state_error.rb
104
+ - lib/mahjong/errors/tile_not_found_error.rb
105
+ - lib/mahjong/flow/detectors/naki_detector.rb
106
+ - lib/mahjong/flow/games/game_flow.rb
107
+ - lib/mahjong/flow/resolvers/action_resolver.rb
108
+ - lib/mahjong/flow/rounds/round_flow.rb
109
+ - lib/mahjong/flow/validators/action_validator.rb
110
+ - lib/mahjong/results/final_result.rb
111
+ - lib/mahjong/results/game_flow_result.rb
112
+ - lib/mahjong/results/game_result.rb
113
+ - lib/mahjong/results/round_end_info.rb
114
+ - lib/mahjong/results/round_flow_result.rb
115
+ - lib/mahjong/scoring/calculators/fu_calculator.rb
116
+ - lib/mahjong/scoring/calculators/score_calculator.rb
117
+ - lib/mahjong/scoring/evaluators/win_evaluator.rb
118
+ - lib/mahjong/scoring/judges/agari_judge.rb
119
+ - lib/mahjong/scoring/judges/furiten_judge.rb
120
+ - lib/mahjong/scoring/judges/machi_judge.rb
121
+ - lib/mahjong/scoring/judges/yaku_judge.rb
122
+ - lib/mahjong/scoring/parsers/hand_parser.rb
123
+ - lib/mahjong/scoring/value_objects/score_result.rb
124
+ - lib/mahjong/scoring/value_objects/yaku_context.rb
125
+ - lib/mahjong/scoring/value_objects/yaku_entry.rb
126
+ - lib/mahjong/scoring/value_objects/yaku_judge_result.rb
127
+ - lib/mahjong/scoring/yaku/chankan.rb
128
+ - lib/mahjong/scoring/yaku/chanta.rb
129
+ - lib/mahjong/scoring/yaku/chiihou.rb
130
+ - lib/mahjong/scoring/yaku/chiitoitsu.rb
131
+ - lib/mahjong/scoring/yaku/chinitsu.rb
132
+ - lib/mahjong/scoring/yaku/chinroutou.rb
133
+ - lib/mahjong/scoring/yaku/chuuren_poutou.rb
134
+ - lib/mahjong/scoring/yaku/daisangen.rb
135
+ - lib/mahjong/scoring/yaku/daisuushii.rb
136
+ - lib/mahjong/scoring/yaku/double_riichi.rb
137
+ - lib/mahjong/scoring/yaku/haitei.rb
138
+ - lib/mahjong/scoring/yaku/honitsu.rb
139
+ - lib/mahjong/scoring/yaku/honroutou.rb
140
+ - lib/mahjong/scoring/yaku/houtei.rb
141
+ - lib/mahjong/scoring/yaku/iipeiko.rb
142
+ - lib/mahjong/scoring/yaku/ikkitsuukan.rb
143
+ - lib/mahjong/scoring/yaku/ippatsu.rb
144
+ - lib/mahjong/scoring/yaku/junchan.rb
145
+ - lib/mahjong/scoring/yaku/kokushi_musou.rb
146
+ - lib/mahjong/scoring/yaku/menzen_tsumo.rb
147
+ - lib/mahjong/scoring/yaku/pinfu.rb
148
+ - lib/mahjong/scoring/yaku/riichi.rb
149
+ - lib/mahjong/scoring/yaku/rinshan_kaihou.rb
150
+ - lib/mahjong/scoring/yaku/ryanpeiko.rb
151
+ - lib/mahjong/scoring/yaku/ryuuiisou.rb
152
+ - lib/mahjong/scoring/yaku/san_ankou.rb
153
+ - lib/mahjong/scoring/yaku/san_kantsu.rb
154
+ - lib/mahjong/scoring/yaku/sanshoku_doujun.rb
155
+ - lib/mahjong/scoring/yaku/sanshoku_doukou.rb
156
+ - lib/mahjong/scoring/yaku/shousangen.rb
157
+ - lib/mahjong/scoring/yaku/shousuushii.rb
158
+ - lib/mahjong/scoring/yaku/suuankou.rb
159
+ - lib/mahjong/scoring/yaku/suukantsu.rb
160
+ - lib/mahjong/scoring/yaku/tanyao.rb
161
+ - lib/mahjong/scoring/yaku/tenhou.rb
162
+ - lib/mahjong/scoring/yaku/toitoihou.rb
163
+ - lib/mahjong/scoring/yaku/tsuuiisou.rb
164
+ - lib/mahjong/scoring/yaku/yakuhai.rb
165
+ - lib/mahjong/snapshots/final_result_snapshot.rb
166
+ - lib/mahjong/snapshots/game_progress_snapshot.rb
167
+ - lib/mahjong/snapshots/game_setup_snapshot.rb
168
+ - lib/mahjong/snapshots/win_evaluation_snapshot.rb
169
+ - lib/mahjong/state/fuuro.rb
170
+ - lib/mahjong/state/hand.rb
171
+ - lib/mahjong/state/kawa.rb
172
+ - lib/mahjong/state/mentsu.rb
173
+ - lib/mahjong/state/round_state.rb
174
+ - lib/mahjong/tiles/dora.rb
175
+ - lib/mahjong/tiles/tile.rb
176
+ - lib/mahjong/tiles/tile_set.rb
177
+ - lib/mahjong/tiles/wall.rb
178
+ - lib/mahjong/tiles/wanpai.rb
179
+ - lib/riichi_engine.rb
180
+ - lib/riichi_engine/api.rb
181
+ - lib/riichi_engine/version.rb
182
+ - riichi_engine.gemspec
183
+ homepage: https://github.com/HenrikStephensen/riichi_engine
184
+ licenses:
185
+ - MIT
186
+ metadata:
187
+ source_code_uri: https://github.com/HenrikStephensen/riichi_engine
188
+ changelog_uri: https://github.com/HenrikStephensen/riichi_engine/blob/main/CHANGELOG.md
189
+ bug_tracker_uri: https://github.com/HenrikStephensen/riichi_engine/issues
190
+ rdoc_options: []
191
+ require_paths:
192
+ - lib
193
+ required_ruby_version: !ruby/object:Gem::Requirement
194
+ requirements:
195
+ - - ">="
196
+ - !ruby/object:Gem::Version
197
+ version: '3.2'
198
+ required_rubygems_version: !ruby/object:Gem::Requirement
199
+ requirements:
200
+ - - ">="
201
+ - !ruby/object:Gem::Version
202
+ version: '0'
203
+ requirements: []
204
+ rubygems_version: 4.0.6
205
+ specification_version: 4
206
+ summary: Pure Ruby riichi mahjong engine
207
+ test_files: []