【问题标题】:Scala: Detecting a Straight in a 5-card Poker hand using pattern matchingScala:使用模式匹配检测 5 张扑克手牌中的顺子
【发布时间】:2011-10-22 13:04:23
【问题描述】:

对于那些不知道什么是 5 张扑克顺子的人:http://en.wikipedia.org/wiki/List_of_poker_hands#Straight

我正在用 Scala 编写一个小型扑克模拟器来帮助我学习该语言,并且我创建了一个 Hand 类,其中包含 5 张有序的。每张卡片都有一个RankSuit,两者都定义为EnumerationsHand 类具有评估手牌等级的方法,其中一个检查手牌是否包含顺子(我们暂时可以忽略同花顺)。我知道有一些很好的算法可以确定直线,但我想看看我是否可以用 Scala 的模式匹配设计一些东西,所以我想出了以下内容:

def isStraight() = {
  def matchesStraight(ranks: List[Rank.Value]): Boolean = ranks match {
    case head :: Nil  => true
    case head :: tail if (Rank(head.id + 1) == tail.head) => matchesStraight(tail)
    case _ => false
  }

  matchesStraight(cards.map(_.rank).toList)
}

这很好用并且可读性很好,但我想知道是否有任何方法可以摆脱那个if。我想像下面这样,虽然我不能让它工作:

private def isStraight() = {
  def matchesStraight(ranks: List[Rank.Value]): Boolean = ranks match {
    case head :: Nil  => true
    case head :: next(head.id + 1) :: tail => matchesStraight(next :: tail)
    case _ => false
  }

  matchesStraight(cards.map(_.rank).toList)
}

有什么想法吗?另外,作为一个附带问题,关于内部 matchesStraight 定义的一般意见是什么?这应该是私人的还是以不同的方式完成?

【问题讨论】:

  • 您可以定义自己的提取器,它看起来像您想要的任何东西,也可以完成您需要的任何东西。
  • @agilesteel 我考虑过,但是(1)我还没有找到一种方法来仍然引用递归调用的匹配值,并且(2)我相信我需要创建一个提取器'+' 也一样吗?
  • 如果枚举自动定义了unapply方法,你可以写成case head :: (next @ Rank(nextRank)) :: tail if nextRank == head.id + 1,但它们没有。
  • 感谢 Alexey,至少解决了问题 (1) ...

标签: scala pattern-matching idioms


【解决方案1】:

这是一个完整的惯用 Scala 手部分类器,适用于所有手牌(处理 5 高顺子):

case class Card(rank: Int, suit: Int) { override def toString = s"${"23456789TJQKA" rank}${"♣♠♦♥" suit}" }

object HandType extends Enumeration {
  val HighCard, OnePair, TwoPair, ThreeOfAKind, Straight, Flush, FullHouse, FourOfAKind, StraightFlush = Value
}

case class Hand(hand: Set[Card]) {
  val (handType, sorted) = {
    def rankMatches(card: Card) = hand count (_.rank == card.rank)
    val groups = hand groupBy rankMatches mapValues {_.toList.sorted}

    val isFlush = (hand groupBy {_.suit}).size == 1
    val isWheel = "A2345" forall {r => hand exists (_.rank == Card.ranks.indexOf(r))}   // A,2,3,4,5 straight
    val isStraight = groups.size == 1 && (hand.max.rank - hand.min.rank) == 4 || isWheel
    val (isThreeOfAKind, isOnePair) = (groups contains 3, groups contains 2)

    val handType = if (isStraight && isFlush)     HandType.StraightFlush
      else if (groups contains 4)                 HandType.FourOfAKind
      else if (isThreeOfAKind && isOnePair)       HandType.FullHouse
      else if (isFlush)                           HandType.Flush
      else if (isStraight)                        HandType.Straight
      else if (isThreeOfAKind)                    HandType.ThreeOfAKind
      else if (isOnePair && groups(2).size == 4)  HandType.TwoPair
      else if (isOnePair)                         HandType.OnePair
      else                                        HandType.HighCard

    val kickers = ((1 until 5) flatMap groups.get).flatten.reverse
    require(hand.size == 5 && kickers.size == 5)
    (handType, if (isWheel) (kickers takeRight 4) :+ kickers.head else kickers)
  }
}

object Hand {
  import scala.math.Ordering.Implicits._
  implicit val rankOrdering = Ordering by {hand: Hand => (hand.handType, hand.sorted)}
}

【讨论】:

    【解决方案2】:

    几天前我正在为 Project Euler 问题 54 做类似的事情。和你一样,我有 Rank 和 Suit 作为枚举。

    我的 Card 类如下所示:

      case class Card(rank: Rank.Value, suit: Suit.Value) extends Ordered[Card] {
        def compare(that: Card) = that.rank compare this.rank
      }
    

    注意我给了它Ordered trait,以便我们以后可以轻松地比较卡片。另外,在解析手时,我使用sorted将它们从高到低排序,这使得评估值变得更加容易。

    这是我的straight 测试,它根据是否为顺子返回Option 值。实际返回值(Ints 列表)用于确定手牌的强度,第一个代表手牌类型,从 0(无对子)到 9(同花顺),其他为其他牌的排名计入其价值的手。对于顺子,我们只关心排名最高的牌。

    另外,请注意,您可以用低牌 Ace、“轮盘”或 A2345 进行顺子。

      case class Hand(cards: Array[Card]) {
        ...
        def straight: Option[List[Int]] = {
    
          if( cards.sliding(2).forall { case Array(x, y) => (y compare x) == 1 } )
            Some(5 :: cards(0).rank.id :: 0 :: 0 :: 0 :: 0 :: Nil)
    
          else if ( cards.map(_.rank.id).toList == List(12, 3, 2, 1, 0) )
            Some(5 :: cards(1).rank.id :: 0 :: 0 :: 0 :: 0 :: Nil) 
    
          else None
        }
      }
    

    【讨论】:

      【解决方案3】:

      如果您正在编写扑克程序,那么您已经在检查 n-of-a-kind。当一手牌没有 n-of-a-kinds (n > 1) 并且最小面额和最大面额之间的差正好是四时,它就是顺子。

      【讨论】:

        【解决方案4】:

        这是一种完全不同的方法,但它确实使用了模式匹配。它在 match 子句中产生警告,似乎表明它不应该工作。但它实际上产生了正确的结果:

        Straight !!! 34567
        Straight !!! 34567
        Sorry no straight this time
        

        我暂时忽略了套件,也忽略了 2 下 A 的可能性。

        abstract class Rank {
            def value : Int
        }
        case class Next[A <: Rank](a : A) extends Rank {
            def value = a.value + 1
        }
        case class Two() extends Rank {
            def value = 2
        }
        
        class Hand(a : Rank, b : Rank, c : Rank, d : Rank, e : Rank) {
            val cards = List(a, b, c, d, e).sortWith(_.value < _.value)
        }
        
        object Hand{
            def unapply(h : Hand) : Option[(Rank, Rank, Rank, Rank, Rank)] = Some((h.cards(0), h.cards(1), h.cards(2), h.cards(3), h.cards(4)))
        }
        
        object Poker {
        
            val two = Two()
            val three = Next(two)
            val four = Next(three)
            val five = Next(four)
            val six = Next(five)
            val seven = Next(six)
            val eight = Next(seven)
            val nine = Next(eight)
            val ten = Next(nine)
            val jack = Next(ten)
            val queen = Next(jack)
            val king = Next(queen)
            val ace = Next(king)
        
            def main(args : Array[String]) {
                val simpleStraight = new Hand(three, four, five, six, seven)
                val unsortedStraight = new Hand(four, seven, three, six, five)
                val notStraight = new Hand (two, two, five, five, ace)
        
                printIfStraight(simpleStraight)
                printIfStraight(unsortedStraight)
                printIfStraight(notStraight)
            }
        
            def printIfStraight[A](h : Hand) {
        
                h match  {
                    case  Hand(a: A , b : Next[A], c : Next[Next[A]], d : Next[Next[Next[A]]], e : Next[Next[Next[Next[A]]]]) => println("Straight !!! " + a.value + b.value + c.value + d.value + e.value)
                    case Hand(a,b,c,d,e)  => println("Sorry no straight this time")
                }
            }
        }
        

        如果您对 google 'church numbers scala type system' 等更多内容感兴趣

        【讨论】:

        • 请注意,您的编码不是数字的 Church 编码 - 您的数字不是零函数和后继函数,因此您无法定义例如直接添加到您的号码上。因此,这不是真正的 Church 编码系统。
        • 您会收到有关不完整案例的警告,因为(根据其类型)unapply 可能会失败。要修复此警告,您可以将 Hand 定义为一个案例类,并将对其构造函数的调用替换为对执行参数排序的工厂方法的调用:因此编译器将知道匹配 Hand(a, b, c, d, e) 永远不会失败。但是,在这里,您不妨将默认大小写替换为 case _ =&gt; println("Sorry no straight this time"),因为未使用参数。
        • 一个不错的解决方案,但无法处理 ace 的歧义。
        • @Blaisorblade:你说得对,这不是教堂数字,但 Rank 的灵感来自教堂数字。
        • @Blaisorblade 我实际上并没有收到关于不完整案例的警告,而是关于类型信息被删除的警告。
        【解决方案5】:

        您不能将信息传递给提取器,也不能使用从一个值返回另一个值的信息,除非 if 语句用于涵盖所有这些情况。

        您可以做的是创建自己的提取器来测试这些东西,但如果没有任何重用,它不会给您带来太多好处。

        例如:

        class SeqExtractor[A, B](f: A => B) {
          def unapplySeq(s: Seq[A]): Option[Seq[A]] =
            if (s map f sliding 2 forall { case Seq(a, b) => a == b  } ) Some(s)
            else None
        }
        
        val Straight = new SeqExtractor((_: Card).rank)
        

        那么你可以这样使用它:

        listOfCards match {
            case Straight(cards) => true
            case _ => false
        }
        

        但是,当然,您真正想要的只是SeqExtractor 中的if 语句。所以,不要太喜欢解决方案,因为你可能会错过更简单的做事方式。

        【讨论】:

        • 谢谢丹尼尔;我低估了提取器的力量。我明白你所说的更简单的解决方案,但目前我只是想了解可用的可能选项。
        【解决方案6】:

        你可以这样做:

        val ids = ranks.map(_.id)
        ids.max - ids.min == 4 && ids.distinct.length == 5
        

        不过,正确处理 A 需要一些工作。

        更新:这是一个更好的解决方案:

        (ids zip ids.tail).forall{case (p,q) => q%13==(p+1)%13}
        

        比较中的% 13 处理的 ace 既是 1 位又是 14 位。

        【讨论】:

        • 不错。功能更强大,至少其中有一些模式匹配:) 我相信 OP 可以使用此处给出的任何方法解决这个问题,但他最初想要一个 模式匹配 解决方案(没有if)...到目前为止,还没有真正为他做到这一点。现在,我也没有一个很好的解决方案,但是这个解决方案简洁而漂亮。
        • 我非常喜欢您的解决方案,它可能是可用的“最佳”解决方案。但就像 Derek 提到的那样,这个问题更多的是帮助我理解模式匹配,而不是提出最佳解决方案。如果我找不到其他可以解决我的具体要求的东西,我会接受你的回答。
        【解决方案7】:

        比如:

        def isStraight(cards:List[Card]) = (cards zip cards.tail) forall { case (c1,c2) =&gt; c1.rank+1 == c2.rank}

        val cards = List(Card(1),Card(2),Card(3),Card(4))

        scala> isStraight(cards)
        res2: Boolean = true
        

        【讨论】:

          【解决方案8】:

          这样的事情怎么样?

          def isStraight = {
            cards.map(_.rank).toList match {
              case first :: second :: third :: fourth :: fifth :: Nil if 
                first.id == second.id - 1 &&
                second.id == third.id - 1 &&
                third.id == fourth.id - 1 &&
                fourth.id == fifth.id - 1 => true
              case _ => false
            }
          }
          

          您仍然坚持使用 if(实际上更大),但没有递归或自定义提取器(我相信您对 next 的使用不正确,这就是为什么您的第二次尝试没有工作)。

          【讨论】:

            猜你喜欢
            • 2011-04-19
            • 1970-01-01
            • 2021-05-17
            • 2017-07-11
            • 1970-01-01
            • 1970-01-01
            • 2016-02-24
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多