Skip to content

Instantly share code, notes, and snippets.

@Ziphil
Last active September 22, 2017 03:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Ziphil/8698055c9d071506b077b9f195241518 to your computer and use it in GitHub Desktop.
Save Ziphil/8698055c9d071506b077b9f195241518 to your computer and use it in GitHub Desktop.
require 'pp'
class Tile
SUITS = {"m" => :character, "p" => :dot, "s" => :bamboo}
attr_reader :number
attr_reader :suit
def initialize(number, suit)
@number = number
@suit = suit
end
def self.from(string)
string = string.gsub(/\s/, "")
if string.length == 2 && string[0].match(/^[1-9]$/) && SUITS.key?(string[1])
number = string[0].to_i
suit = SUITS[string[1]]
return Tile.new(number, suit)
else
raise ArgumentError
end
end
def self.sample
return Tile.new((1..9).to_a.sample, SUITS.values.sample)
end
def inspect
return "#{@number}#{SUITS.invert[@suit]}"
end
def self.enumerator
enumerator = Enumerator.new do |yielder|
SUITS.values.each do |suit|
(1..9).each do |number|
yielder << Tile.new(number, suit)
end
end
end
return enumerator
end
end
class Hand
PRIMES = {2 => [11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97],
3 => [113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 311, 313, 317,
331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 521, 523, 541, 547, 557, 563, 569, 571, 577,
587, 593, 599, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 811, 821, 823, 827, 829, 839, 853,
857, 859, 863, 877, 881, 883, 887, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997]}
def initialize(tiles)
@tiles = tiles
end
def self.from(string)
string = string.gsub(/\s/, "")
if string.length.even?
tiles = string.scan(/.{2}/).map{|s| Tile.from(s)}
return Hand.new(tiles)
else
raise ArgumentError
end
end
# size に渡された個数の牌から成るランダムな手牌を返します。
# 牌の全体の個数を考慮していないので、同じ牌が 5 つ以上含まれる場合もあります。
def self.sample(size)
return Hand.new((0...size).map{Tile.sample})
end
# 13 牌から成る手牌が聴牌していれば true を返し、聴牌していなければ false を返します。
def ready?
if @tiles.size == 13
Tile.enumerator.each do |tile|
if Hand.new(@tiles + [tile]).legal
return true
end
end
return false
else
raise ArgumentError, "hand must consist of exactly 13 tiles"
end
end
# 14 牌からなる手牌から適当な 1 牌を除けば聴牌するとき true を返し、そうでなければ false を返します。
def can_ready?
if @tiles.size == 14
can_ready = (0...14).any? do |i|
sub_tiles = @tiles.clone
sub_tiles.delete_at(i)
next Hand.new(sub_tiles).ready?
end
return can_ready
else
raise ArgumentError, "hand must consist of exactly 14 tiles"
end
end
# 13 牌からなる手牌に対し、それに加えて和了形になるような牌をキーとし、その牌を加えて理牌した手牌を値とするようなハッシュを返します。
def winning_tiles
if @tiles.size == 13
result = {}
Tile.enumerator.each do |tile|
if legal = Hand.new(@tiles + [tile]).legal
result[tile] = legal
end
end
return result
else
raise ArgumentError, "hand must consist of exactly 13 tiles"
end
end
# 14 牌からなる手牌に対し、それが和了形なら面子などが分かりやすいよう理牌した手牌を返し、和了形でないなら nil を返します。
def legal
if @tiles.size == 14
return legal_normal || legal_seven_eyes
else
return ArgumentError, "hand must consist of exactly 14 tiles"
end
end
def legal_normal
[:character, :dot, :bamboo].each do |suit|
@tiles.select{|s| s.suit == suit}.permutation(2).each do |eye_candidate|
if PRIMES[2].include?(eye_candidate.map{|s| s.number.to_s}.join.to_i)
remained_tiles = @tiles.reject{|s| eye_candidate.include?(s)}
checked_tiles = remained_tiles.group_by{|s| s.suit}.map{|_, s| Hand.dividable?(s, 3)}
unless checked_tiles.include?(nil)
result = []
result << eye_candidate
result += checked_tiles.flatten.each_slice(3).to_a
return result
end
end
end
end
return nil
end
def legal_seven_eyes
checked_tiles = @tiles.group_by{|s| s.suit}.map{|_, s| Hand.dividable?(s, 2)}
unless checked_tiles.include?(nil)
result = []
result += checked_tiles.flatten.each_slice(2).to_a
return result
end
end
def inspect
return @tiles.map{|s| s.inspect}.join
end
def self.dividable?(tiles, size)
if tiles.size > 0 && tiles.size % size == 0
PRIMES[size].each do |prime|
sub_tiles = tiles.clone
sub_result = []
deleted = prime.to_s.each_char.all? do |digit|
if tile = sub_tiles.find{|s| s.number == digit.to_i}
sub_tiles.delete(tile)
sub_result << tile
next true
else
next false
end
end
if deleted
if result = dividable?(sub_tiles, size)
return sub_result + result
end
end
end
return nil
elsif tiles.size == 0
return []
else
return nil
end
end
end
# ◆ 和了形判定
# 和了形である → 雀頭や面子などが分かりやすいよう理牌した形で表示
# 和了形でない → nil
# 通常の和了形の例
pp Hand.from("5s3s 2m4m1m 7m7m3m 6p1p7p 4s4s9s").legal
# 和了形でない例
pp Hand.from("5s3s 2m4m9m 7m7m3m 6p1p8p 4s4s8s").legal
# 七対子形の例
pp Hand.from("4m7m 5m9m 1s7s 8s3s 4s7s 1p1p 5p9p").legal
# ◆ 聴牌判定
# 和了できるツモとそのときの和了形を表示
# 例
pp Hand.from("1m4m6m8m 1s3s7s 1p1p3p5p7p8p").winning_tiles
pp Hand.from("1m4m6m7m 1s5s6s 3p3p4p6p7p8p").winning_tiles
# 18 面待ち
pp Hand.from("1m2m3m9m 1s3s3s4s5s6s7s7s9s").winning_tiles
# 21 面待ち
pp Hand.from("1m9m 2s3s 1p2p3p6p7p7p8p9p9p").winning_tiles
# 24 面待ち
pp Hand.from("1m1m2m3m5m7m9m9m 2s3s 1p1p9p").winning_tiles
# 役の判定や鳴きがある場合などは未実装
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment