Last active
September 22, 2017 03:59
-
-
Save Ziphil/8698055c9d071506b077b9f195241518 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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