問題描述

我遲到了這個週末的挑戰 (對不起),但由於這一切都很好玩,我希望沒關係。我不是撲克玩家,所以我可能完全忽略了一些東西。

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 輔助翻譯的英文資料結果。如果您對結果不滿意,可以加入我們改善翻譯效果:薇曉朵技術論壇。