问题描述
我迟到了这个周末的挑战 (对不起),但由于这一切都很好玩,我希望没关系。我不是扑克玩家,所以我可能完全忽略了一些东西。
Hand
类进行评估,并计算 (其中包括)score
数组,可以与另一个扑克牌进行比较。第一项是手分数 (0-8),以下 1-5 项是 tie-breaking 值/踢球者。例如。一个 three-of-a 手牌可能会被打分
[3, 5, 9, 6] # base score, value of tripled card, kicker, kicker
或者,为了比较,考虑两个 two-pair 手
player1.score #=> [2, 7, 5, 3]
player2.score #=> [2, 7, 5, 8]
两名选手都有两双 7 和 5,但是球员 2 的踢球比较高。
(已知和有意) 的限制是:
-
每只手只有 5 张卡 (即没有公用卡等)
-
不支持小丑/通配符
-
没有验证卡
确定直线时,它确实考虑到高和低的 ace,但否则它不是非常灵活。 (当然,只要 brute-force 使用 Array#combination
一次检查 5 张卡组合,您可以回避 「5 张卡」 的限制,但这是另一个故事。)
我还没有看过这个挑战如何在其他语言中得到解决,所以也许有一些我错过的技巧。但实际上,主要是要看到我能用多少功能的方法和数组/枚举的方法来获得多少。代码主要是 one-line 的方法,所以我觉得没问题。
还没有打扰优化,但是 (如果没有其他的话) 一堆值可以用一小段||=
来记录。然而,我对整体方法的批评更感兴趣 (我只是喜欢?
方法,好吗?) 和可能的替代方案 (整体或特定部分)
完整的代码 (包括测试和更详细的注释) 是 in this gist; 以下是主要班级 (见下文进一步说明)
ACE_LOW = 1
ACE_HIGH = 14
# Use Struct to model a simple Card class
Card = Struct.new :suit, :value
# This class models and evaluates a hand of cards
class Hand
attr_reader :cards
RANKS = {
straight_flush: 8,
four_of_a_kind: 7,
full_house: 6,
flush: 5,
straight: 4,
three_of_a_kind: 3,
two_pair: 2,
pair: 1
}.freeze
def initialize(cards)
raise ArgumentError unless cards.count == 5
@cards = cards.freeze
end
# The hand's rank as an array containing the hand's
# type and that type's base score
def rank
RANKS.detect { |method, rank| send :"#{method}?" } || [:high_card, 0]
end
# The hand's type (e.g. :flush or :pair)
def type
rank.first
end
# The hand's base score (based on rank)
def base_score
rank.last
end
# The hand's score is an array starting with the
# base score, followed by the kickers.
def score
[base_score] + kickers
end
# Tie-breaking kickers, ordered high to low.
def kickers
repeat_values + (aces_low? ? aces_low_values.reverse : single_values)
end
# If the hand's straight and flush, it's a straight flush
def straight_flush?
straight? && flush?
end
# Is a value repeated 4 times?
def four_of_a_kind?
repeat_counts.include? 4
end
# Three of a kind and a pair make a full house
def full_house?
three_of_a_kind? && pair?
end
# If the hand only contains one suit, it's flush
def flush?
suits.uniq.count == 1
end
# This is the only hand where high vs low aces comes into play.
def straight?
aces_high_straight? || aces_low_straight?
end
# Is a card value repeated 3 times?
def three_of_a_kind?
repeat_counts.include? 3
end
# Are there 2 instances of repeated card values?
def two_pair?
repeat_counts.count(2) == 2
end
# Any repeating card value?
def pair?
repeat_counts.include? 2
end
# Actually just an alias for aces_low_straight?
def aces_low?
aces_low_straight?
end
# Does the hand include one or more aces?
def aces?
values.include? ACE_HIGH
end
# The repeats in the hand
def repeats
cards.group_by &:value
end
# The number of repeats in the hand, unordered
def repeat_counts
repeats.values.map &:count
end
# The values that are repeated more than once, sorted by
# number of occurrences
def repeat_values
repeated = repeats.map { |value, repeats| [value.to_i, repeats.count] }
repeated = repeated.reject { |value, count| count == 1 }
repeated = repeated.sort_by { |value, count| [count, value] }.reverse
repeated.map(&:first)
end
# Values that are not repeated, sorted high to low
def single_values
repeats.select { |value, repeats| repeats.count == 1 }.map(&:first).sort.reverse
end
# Ordered (low to high) array of card values (assumes aces high)
def values
cards.map(&:value).sort
end
# Unordered array of card suits
def suits
cards.map(&:suit)
end
# A "standard" straight, treating aces as high
def aces_high_straight?
straight_values_from(values.first) == values
end
# Special case straight, treating aces as low
def aces_low_straight?
aces? && straight_values_from(aces_low_values.first) == aces_low_values
end
# The card values as an array, treating aces as low
def aces_low_values
cards.map(&:value).map { |v| v == ACE_HIGH ? ACE_LOW : v }.sort
end
private
# Generate an array of 5 consecutive values
# starting with the `from` value
def straight_values_from(from)
(from...from + 5).to_a
end
end
注释和编辑
作为一个规则,我认为 aces 很高 (值为 14),只有当它直接检查 aces-low 时才将它们计数为低 (值 1) 。也就是说,一个 ace Card
实例的值为 14,但是在 aces-low 的上下文中,Hand
实例将报告为 1 。
Hand
和 Card
实例被认为是不可变的 (虽然在技术上,卡不是不可变的,因为我使用 Struct
,但这只是为了这个挑战的目的,否则我将定义一个”proper” 类)
再次查看代码,这里是我自己的关注点 (超出上述限制):
-
一些方法返回无序数组,一些从高到低,而其他数组从低到高。可能会让这更加一致。
-
straight-checking 是非常幼稚的:生成连续 5 个数字,看看它们是否匹配卡值。我考虑以各种方式列举价值观,但是与生成的数组相比,简单的
==
比起我看起来更加直观。 -
在
RANKS
哈希键和方法名称中需要重复一些,但是我发现它比我玩的替代物更干净。
最佳解决方案
我认为你的榜样结构良好,我希望我的审查有一些正义。有你的疑虑和一些额外的要点我想讨论:
You’ll find a working implementation of the following code here
-
一般编码风格是好的和一致的。我特别喜欢你使用问号来表示返回一个布尔值的方法。
-
也许你也可以从方法定义中省略大括号,因为你可能在方法调用中省略它们。
-
您没有文档离开
RANKS
,您的属性和初始化程序。我认为特别是RANKS
和初始化器将从中受益。
-
-
在
RANKS
和实例方法中重复我认为这是可以的,我没有提出更好或更可读的方式。 -
flush
方法他们可以使用
Enumerable#one?
更优雅地写出来# If the hand only contains one suit, it's flush def flush? suits.uniq.one? end
-
决定使卡成为
Struct
我喜欢使用代码中的常量来提高可读性和可维护性。引起我兴趣的一件事是ACE_LOW
和ACE_HIGH
是全局常数,而RANKS
不是 (这是好的) 。我认为这个设计缺陷来自于你决定使Card
成为一个Struct
对象,并且这些对象常常导致缺乏逻辑。让我们改变一下,使Card
成为一流的公民:您的代码将受益匪浅。class Card
include Comparableattr_reader :suit, :value
# Value to use as ace low
ACE_LOW = 1# Value to use as ace high
ACE_HIGH = 14# initialize the card with a suit and a value
def initialize suit, value
super()
@suit = suit
@value = value
end# Return the low card
def low_card
ace? ? Card.new(suit, ACE_LOW) : self
end# Return if the card is an ace high
def ace?
value == ACE_HIGH
enddef ace_low?
value == ACE_LOW
end# Return if the card has suit spades
def spades?
suit == :spades
end# Return if the card has suit diamonds
def diamonds?
suit == :diamonds
end# Return if the card is suit hearts
def hearts?
suit == :hearts
end# Return if the card has suit clubs
def clubs?
suit == :clubs
end# Compare cards based on values and suits
# Ordered by suits and values - the suits_index will be introduced below
def <=> other
if other.is_a? Card
(suit_index(suit) <=> suit_index(other.suit)).nonzero? || value <=> other.value
else
value <=> other
end
end# Allow for construction of card ranges across suits
# the suits_index will be introduced below
def succ
if ace?
i = suit_index suit
Card.new(Deck::SUITS[i + 1] || Deck::SUITS.first, ACE_LOW)
else
Card.new(suit, value + 1)
end
enddef successor? other
succ == other
enddef straight_successor? other
succ === other
end# Compare cards for equality in value
def == other
if other.is_a? Card
value == other.value
else
value == other
end
end
alias :eql? :==# overwrite hash with value since cards with same values are considered equal
alias :hash :value# Compare cards for strict equality (value and suit)
def === other
if other.is_a? Card
value == other.value && suit == other.suit
else
false
end
endprivate
# If no deck, this has to be done with an array of suits
# gets the suit index
def suit_index suit
Deck::SUITS.index suit
end
end
这将对Hand
中的代码进行以下改进:class Hand
# ...
# Tie-breaking kickers, ordered high to low.
def kickers
same_of_kind + (aces_low? ? aces_low.reverse : single_cards)
end# If the hand's straight and flush, it's a straight flush
def straight_flush?
straight? && flush?
end# Is a value repeated 4 times?
def four_of_a_kind?
same_of_kind? 4
end# Three of a kind and a pair make a full house
def full_house?
same_of_kind?(3) && same_of_kind?(2)
end# If the hand only contains one suit, it's flush
def flush?
suits.uniq.one?
end# This is the only hand where high vs low aces comes into play.
def straight?
aces_high_straight? || aces_low_straight?
end# Is a card value repeated 3 times?
def three_of_a_kind?
collapsed_size == 2 && same_of_kind?(3)
end# Are there 2 instances of repeated card values?
def two_pair?
collapsed_size == 2 && same_of_kind?(2)
end# Any pair?
def pair?
same_of_kind? 2
enddef single_cards
cards.select{|c| cards.count(c) == 1 }
end# Does the hand include one or more aces?
def aces?
cards.any? &:ace?
end# Ordered (low to high) array of card values (assumes aces high)
def values
cards.map(&:value).sort
end# Unordered array of card suits
def suits
cards.map &:suit
end# A "standard" straight, treating aces as high
def aces_high_straight?
all_successors? cards.sort_by(&:value)
end# Special case straight, treating aces as low
def aces_low_straight?
aces? && all_successors?(aces_low)
end
alias :aces_low? :aces_low_straight?# The card values as an array, treating aces as low
def aces_low
cards.map(&:low_card).sort
endprivate
# Are there n cards same of kind?
def same_of_kind?(n)
!!cards.detect{|c| cards.count(c) == n }
end# How many cards vanish if we collapse the cards to single values
def collapsed_size
cards.size - cards.uniq.size
end# map the cards that are same of kind
def same_of_kind
2.upto(4).map{|n| cards.select{|c| cards.count(c) == n }.reverse }.sort_by(&:size).reverse.flatten.uniq
end# Are all cards succeeding each other in value?
def all_successors?(cards)
cards.all?{|a| a === cards.last || a.successor?(cards[cards.index(a) + 1]) }
endend
还-
它将在
Card
中包含常数ACE_HIGH
和ACE_LOW
-
换卡是不可能的。由于
value=
和suit=
的结构响应,因此Struct
卡在cards
数组中的值仍然可以修改
-
-
Hand
初始化程序我认为将
cards
作为一个混沌论据会更好一些。要用阵列进行初始化,不必要地降低可读性。此外,您提出的ArgumentError
不是很描述性,这可能会导致一些混乱。总而言之,这是我的改进如何:def initialize(*cards) raise ArgumentError.new "wrong number of cards (#{cards.count} for 5)" unless cards.count == 5 @cards = cards.freeze end
根据使用情况,还可能需要额外的理智检查:检查您是否真的收到 5 个
Card
实例。
以下几点是您可以从这里开始的建议
-
使
Hand
成为Array
的子类卡牌也是一系列的卡 – 相似之处允许您使用
Hand
子类Array
。这将允许您直接在Hand
上使用Enumerable
和Array
DSL,如果您要进一步接受此代码,可能会受益您 (考虑Deck
和Game
类)另外,我会做的这样做会给你默认的排序,应该排除你的问题与未分类的回报,加上你可以调用排序基于 suit-and-value 的排序。
一旦您决定在初始化时决定不进行
freeze
Hand
,则可能需要额外的健康检查:-
检查
push
,unshift
,insert
和<<
是否得到Card as argument
的实例 -
检查
push
,unshift
,insert
和<<
是否不会在手中添加太多的卡
那么这是怎么做到的呢让我们重构:
class Hand < Array # .. RANKS def initialize(*cards) raise ArgumentError.new "There must be 5 cards" unless cards.count == 5 super(cards) sort_by! &:value # This will give you a nicely sorted hand by default freeze end # The hand's rank as an array containing the hand's # type and that type's base score def rank RANKS.detect { |method, rank| send :"#{method}?" } || [:high_card, 0] end # The hand's type (e.g. :flush or :pair) def type rank.first end # The hand's base score (based on rank) def base_score rank.last end # The hand's score is an array starting with the # base score, followed by the kickers. def score ([base_score] + kickers.map(&:value)) end # Tie-breaking kickers, ordered high to low. def kickers same_of_kind + (aces_low? ? aces_low.reverse : single_cards.reverse) end # If the hand's straight and flush, it's a straight flush def straight_flush? straight? && flush? end # Is a value repeated 4 times? def four_of_a_kind? same_of_kind? 4 end # Three of a kind and a pair make a full house def full_house? same_of_kind?(3) && same_of_kind?(2) end # If the hand only contains one suit, it's flush def flush? suits.uniq.one? end # single cards in the hand def single_cards select{ |c| count(c) == 1 }.sort_by(&:value) end # This is the only hand where high vs low aces comes into play. def straight? aces_high_straight? || aces_low_straight? end # Is a card value repeated 3 times? def three_of_a_kind? collapsed_size == 2 && same_of_kind?(3) end # Are there 2 instances of repeated card values? def two_pair? collapsed_size == 2 && same_of_kind?(2) end # Any repeating card value? def pair? same_of_kind?(2) end # Does the hand include one or more aces? def aces? any? &:ace? end # Ordered (low to high) array of card values (assumes aces high) def values map(&:value).sort end # Ordered Array of card suits def suits sort.map &:suit end # A "standard" straight, treating aces as high def aces_high_straight? all?{|card| card === last || card.successor?(self[index(card) + 1]) } end alias :all_successors? :aces_high_straight? # Special case straight, treating aces as low def aces_low_straight? aces? && aces_low.all_successors? end alias :aces_low? :aces_low_straight? # The card values as an array, treating aces as low def aces_low Hand.new *map(&:low_card) end private # Are there n cards of the same kind? def same_of_kind?(n) !!detect{|card| count(card) == n } end def same_of_kind 2.upto(4).map{|n| select{|card| count(card) == n }.reverse }.sort_by(&:size).reverse.flatten.uniq end # How many cards vanish if we collapse the cards to single values def collapsed_size size - uniq.size end end
与
Card
类一起,这可以帮助您建立一个不错的 DSL:hand = Hand.new Card.new(:spades, 14), Card.new(:diamonds, 14), Card.new(:hearts, 14), Card.new(:clubs, 14), Card.new(:clubs, 14) hand.all? &:ace? #=> true, this guy is obviously cheating hand.any? &:spades? #=> true, he has spades hand.count &:ace? #=> 5
只是为了好玩,如果你有一个
Deck
类,它也将Array
子类class Deck < Array # the hands this deck creates attr_reader :hands # You can install any order here, Bridge, Preferans, Five Hundred SUITS = %i(clubs diamonds hearts spades).freeze # Initialize a deck of cards def initialize super (Card.new(SUITS.first, 1)..Card.new(SUITS.last, 14)).to_a shuffle! end # Deal n hands def deal! hands=5 @hands = hands.times.map {|i| Hand.new *pop(5) } end # ... and so on end deck = Deck.new deck.deal! deck.hands.sort_by &:rank #see who's winning hand = deck.hands.first # Select cards left in the deck that could be helpful to this hand deck.select do |card| hand.any?{|card_in_hand| (card_in_hand..card_in_hand.succ).include? card } end
-
-
去哪里呢
-
在
Hand
上实现Comparable
-
摆脱
freeze
在手,所以我们可以交换一些类型的游戏卡 -
田田田田达
-
正如我所说,你的例子已经很棒了,我希望你感谢我和你分享我的想法!
参考文献
注:本文内容整合自 Google/Baidu/Bing 辅助翻译的英文资料结果。如果您对结果不满意,可以加入我们改善翻译效果:薇晓朵技术论坛。