问题描述

我迟到了这个周末的挑战 (对不起),但由于这一切都很好玩,我希望没关系。我不是扑克玩家,所以我可能完全忽略了一些东西。

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 。

HandCard 实例被认为是不可变的 (虽然在技术上,卡不是不可变的,因为我使用 Struct,但这只是为了这个挑战的目的,否则我将定义一个”proper” 类)

再次查看代码,这里是我自己的关注点 (超出上述限制):

  • 一些方法返回无序数组,一些从高到低,而其他数组从低到高。可能会让这更加一致。

  • straight-checking 是非常幼稚的:生成连续 5 个数字,看看它们是否匹配卡值。我考虑以各种方式列举价值观,但是与生成的数组相比,简单的==比起我看起来更加直观。

  • RANKS 哈希键和方法名称中需要重复一些,但是我发现它比我玩的替代物更干净。

最佳解决方案

我认为你的榜样结构良好,我希望我的审查有一些正义。有你的疑虑和一些额外的要点我想讨论:

You’ll find a working implementation of the following code here

  1. 一般编码风格是好的和一致的。我特别喜欢你使用问号来表示返回一个布尔值的方法。

    • 也许你也可以从方法定义中省略大括号,因为你可能在方法调用中省略它们。

    • 您没有文档离开 RANKS,您的属性和初始化程序。我认为特别是 RANKS 和初始化器将从中受益。

  2. RANKS 和实例方法中重复我认为这是可以的,我没有提出更好或更可读的方式。

  3. flush 方法

    他们可以使用 Enumerable#one? 更优雅地写出来

     # If the hand only contains one suit, it's flush
     def flush?
       suits.uniq.one?
     end
    
  4. 决定使卡成为 Struct 我喜欢使用代码中的常量来提高可读性和可维护性。引起我兴趣的一件事是 ACE_LOWACE_HIGH 是全局常数,而 RANKS 不是 (这是好的) 。我认为这个设计缺陷来自于你决定使 Card 成为一个 Struct 对象,并且这些对象常常导致缺乏逻辑。让我们改变一下,使 Card 成为一流的公民:您的代码将受益匪浅。 class Card
    include Comparable

    attr_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
    end

    def 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
    end

    def successor? other
    succ == other
    end

    def 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
    end

    private

    # 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
    end

    def 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
    end

    private

    # 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]) }
    end

    end

    • 它将在 Card 中包含常数 ACE_HIGHACE_LOW

    • 换卡是不可能的。由于 value=suit=的结构响应,因此 Struct 卡在 cards 数组中的值仍然可以修改

  5. 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 实例。

以下几点是您可以从这里开始的建议

  1. 使 Hand 成为 Array 的子类

    卡牌也是一系列的卡 – 相似之处允许您使用 Hand 子类 Array 。这将允许您直接在 Hand 上使用 EnumerableArray DSL,如果您要进一步接受此代码,可能会受益您 (考虑 DeckGame 类)

    另外,我会做的这样做会给你默认的排序,应该排除你的问题与未分类的回报,加上你可以调用排序基于 suit-and-value 的排序。

    一旦您决定在初始化时决定不进行 freeze Hand,则可能需要额外的健康检查:

    • 检查 pushunshiftinsert<< 是否得到 Card as argument 的实例

    • 检查 pushunshiftinsert<< 是否不会在手中添加太多的卡

    那么这是怎么做到的呢让我们重构:

    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
    
  2. 去哪里呢

    • Hand 上实现 Comparable

    • 摆脱 freeze 在手,所以我们可以交换一些类型的游戏卡

    • 田田田田达

正如我所说,你的例子已经很棒了,我希望你感谢我和你分享我的想法!

参考文献

注:本文内容整合自 Google/Baidu/Bing 辅助翻译的英文资料结果。如果您对结果不满意,可以加入我们改善翻译效果:薇晓朵技术论坛。