An N-Player, Imperfect Information, Zero-sum game

Example

The axle.game.cards package models decks, cards, ranks, suits, and ordering.

Define a function that takes the hand size and returns the best 5-card hand

import cats.implicits._
import cats.Order.catsKernelOrderingForOrder

import axle.game.cards.Deck
import axle.game.poker.PokerHand

def winnerFromHandSize(handSize: Int) =
  Deck().cards.take(handSize).combinations(5).map(cs => PokerHand(cs.toVector)).toList.max

winnerFromHandSize(7).show
// res0: String = "6\u2661 9\u2661 9\u2662 T\u2660 K\u2663"

20 simulated 5-card hands made of 7-card hands. Sorted.

val hands = (1 to 20).map(n => winnerFromHandSize(7)).sorted
// hands: IndexedSeq[PokerHand] = Vector(
//   PokerHand(
//     cards = Vector(
//       Card(
//         rank = axle.game.cards.Queen$@8b2f924,
//         suit = axle.game.cards.Clubs$@71916f9e
//       ),
//       Card(
//         rank = axle.game.cards.R10$@5d1825dc,
//         suit = axle.game.cards.Spades$@278f5c7e
//       ),
//       Card(
//         rank = axle.game.cards.R7$@56b8373d,
//         suit = axle.game.cards.Hearts$@7ae2cc93
//       ),
//       Card(
//         rank = axle.game.cards.King$@6fec2598,
//         suit = axle.game.cards.Hearts$@7ae2cc93
//       ),
//       Card(
//         rank = axle.game.cards.Jack$@10025472,
//         suit = axle.game.cards.Diamonds$@14015c5f
//       )
//     )
//   ),
//   PokerHand(
//     cards = Vector(
//       Card(
//         rank = axle.game.cards.R10$@5d1825dc,
//         suit = axle.game.cards.Spades$@278f5c7e
//       ),
//       Card(
//         rank = axle.game.cards.Ace$@2c3ab8b0,
//         suit = axle.game.cards.Spades$@278f5c7e
//       ),
//       Card(
//         rank = axle.game.cards.R9$@3f3cdffb,
//         suit = axle.game.cards.Spades$@278f5c7e
//       ),
//       Card(
//         rank = axle.game.cards.R6$@368f1d9a,
//         suit = axle.game.cards.Hearts$@7ae2cc93
//       ),
//       Card(
//         rank = axle.game.cards.Jack$@10025472,
//         suit = axle.game.cards.Spades$@278f5c7e
//       )
//     )
//   ),
// ...

hands.map({ hand => hand.show + "  " + hand.description }).mkString("\n")
// res1: String = """7♡ T♠ J♢ Q♣ K♡  high K high
// 6♡ 9♠ T♠ J♠ A♠  high A high
// 8♣ T♠ J♠ Q♡ A♠  high A high
// 8♠ 9♠ T♣ K♣ A♠  high A high
// 6♣ 7♡ J♣ K♢ A♢  high A high
// 7♠ 9♣ Q♡ K♣ A♡  high A high
// 7♢ J♢ Q♢ K♡ A♠  high A high
// 5♠ 5♣ 9♣ T♡ K♠  pair of 5
// 5♡ 5♢ Q♡ K♢ A♡  pair of 5
// 5♢ 5♣ Q♣ K♣ A♠  pair of 5
// 8♠ 8♢ T♢ K♠ A♡  pair of 8
// 5♠ 7♢ 9♠ 9♣ T♠  pair of 9
// 6♢ 9♠ T♢ T♣ J♣  pair of T
// 7♠ 9♣ J♡ A♠ A♢  pair of A
// 6♢ J♡ Q♣ A♢ A♣  pair of A
// 2♢ 2♣ 6♠ 6♡ T♠  two pair 6 and 2
// 5♠ 5♣ 7♠ 7♡ A♢  two pair 7 and 5
// 3♠ 3♣ T♢ T♣ Q♠  two pair T and 3
// 6♠ 6♡ 6♢ Q♠ A♣  three of a kind of 6
// 2♠ 3♠ 4♠ 7♠ 9♠  flush in ♠"""

Record 1000 simulated hands for each drawn hand size from 5 to 9

import axle.game.poker.PokerHandCategory

val data: IndexedSeq[(PokerHandCategory, Int)] =
  for {
    handSize <- 5 to 9
    trial <- 1 to 1000
  } yield (winnerFromHandSize(handSize).category, handSize)
// data: IndexedSeq[(PokerHandCategory, Int)] = Vector(
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.ThreeOfAKind$@77f302ee, 5),
//   (axle.game.poker.TwoPair$@33f19829, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.TwoPair$@33f19829, 5),
//   (axle.game.poker.ThreeOfAKind$@77f302ee, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.ThreeOfAKind$@77f302ee, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.High$@1c1ffaee, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.Pair$@22660571, 5),
//   (axle.game.poker.Pair$@22660571, 5),
// ...

BarChartGrouped to visualize the results

import spire.algebra.CRing

import axle.visualize.BarChartGrouped
import axle.visualize.Color._
import axle.syntax.talliable.talliableOps

implicit val ringInt: CRing[Int] = spire.implicits.IntAlgebra
// ringInt: CRing[Int] = spire.std.IntAlgebra@e09d5e0

val colors = List(black, red, blue, yellow, green)
// colors: List[axle.visualize.Color] = List(
//   Color(r = 0, g = 0, b = 0),
//   Color(r = 255, g = 0, b = 0),
//   Color(r = 0, g = 0, b = 255),
//   Color(r = 255, g = 255, b = 0),
//   Color(r = 0, g = 255, b = 0)
// )

val chart = BarChartGrouped[PokerHandCategory, Int, Int, Map[(PokerHandCategory, Int), Int], String](
  () => data.tally.withDefaultValue(0),
  title = Some("Poker Hands"),
  drawKey = false,
  yAxisLabel = Some("instances of category by hand size (1000 trials each)"),
  colorOf = (cat: PokerHandCategory, handSize: Int) => colors( (handSize - 5) % colors.size),
  hoverOf = (cat: PokerHandCategory, handSize: Int) => Some(s"${cat.show} from $handSize")
)
// chart: BarChartGrouped[PokerHandCategory, Int, Int, Map[(PokerHandCategory, Int), Int], String] = BarChartGrouped(
//   dataFn = <function0>,
//   drawKey = false,
//   width = 700,
//   height = 600,
//   border = 50,
//   barWidthPercent = 0.8,
//   keyLeftPadding = 20,
//   keyTopPadding = 50,
//   keyWidth = 80,
//   title = Some(value = "Poker Hands"),
//   keyTitle = None,
//   normalFontName = "Courier New",
//   normalFontSize = 12,
//   titleFontName = "Palatino",
//   titleFontSize = 20,
//   xAxis = None,
//   xAxisLabel = None,
//   yAxisLabel = Some(
//     value = "instances of category by hand size (1000 trials each)"
//   ),
//   labelAngle = Some(
//     value = UnittedQuantity(
//       magnitude = 36.0,
//       unit = UnitOfMeasurement(
//         name = "degree",
//         symbol = "\u00b0",
//         wikipediaUrl = Some(
//           value = "http://en.wikipedia.org/wiki/Degree_(angle)"
//         )
//       )
//     )
//   ),
//   colorOf = <function2>,
//   hoverOf = <function2>,
//   linkOf = axle.visualize.BarChartGrouped$$$Lambda$7262/0x0000000802be8840@1f98dd80
// )

Render as SVG file

import axle.web._
import cats.effect._

chart.svg[IO]("poker_hands.svg").unsafeRunSync()

poker hands

Texas Hold ‘Em Poker

As a game of “imperfect information”, poker introduces the concept of Information Set.

import axle.game._
import axle.game.poker._

val p1 = Player("P1", "Player 1")
// p1: Player = Player(id = "P1", description = "Player 1")
val p2 = Player("P2", "Player 2")
// p2: Player = Player(id = "P2", description = "Player 2")

val game = Poker(Vector(p1, p2))
// game: Poker = Poker(
//   betters = Vector(
//     Player(id = "P1", description = "Player 1"),
//     Player(id = "P2", description = "Player 2")
//   )
// )

Create a writer for each player that prefixes the player id to all output.

import cats.effect.IO
import axle.IO.printMultiLinePrefixed

val playerToWriter: Map[Player, String => IO[Unit]] =
  evGame.players(game).map { player =>
    player -> (printMultiLinePrefixed[IO](player.id) _)
  } toMap
// playerToWriter: Map[Player, String => IO[Unit]] = Map(
//   Player(id = "D", description = "Dealer") -> <function1>,
//   Player(id = "P1", description = "Player 1") -> <function1>,
//   Player(id = "P2", description = "Player 2") -> <function1>
// )

Use a uniform distribution on moves as the demo strategy:

import axle.probability._
import spire.math.Rational

val randomMove =
  (state: PokerStateMasked) =>
    ConditionalProbabilityTable.uniform[PokerMove, Rational](evGame.moves(game, state))
// randomMove: PokerStateMasked => ConditionalProbabilityTable[PokerMove, Rational] = <function1>

Wrap the strategies in the calls to writer that log the transitions from state to state.

val strategies: Player => PokerStateMasked => IO[ConditionalProbabilityTable[PokerMove, Rational]] = 
  (player: Player) =>
    (state: PokerStateMasked) =>
      for {
        _ <- playerToWriter(player)(evGameIO.displayStateTo(game, state, player))
        move <- randomMove.andThen( m => IO { m })(state)
      } yield move
// strategies: Player => PokerStateMasked => IO[ConditionalProbabilityTable[PokerMove, Rational]] = <function1>

Play the game – compute the end state from the start state.

import spire.random.Generator.rng

val endState = play(game, strategies, evGame.startState(game), rng).unsafeRunSync()
// D> To: You
// D> Current bet: 0
// D> Pot: 0
// D> Shared: 
// D> 
// D> P1:  hand -- in for $--, $100 remaining
// D> P2:  hand -- in for $--, $100 remaining
// P1> To: You
// P1> Current bet: 2
// P1> Pot: 3
// P1> Shared: 
// P1> 
// P1> P1:  hand 2♢ 2♠ in for $1, $99 remaining
// P1> P2:  hand -- in for $2, $98 remaining
// P2> To: You
// P2> Current bet: 86
// P2> Pot: 88
// P2> Shared: 
// P2> 
// P2> P1:  hand -- in for $86, $14 remaining
// P2> P2:  hand 5♠ T♡ in for $2, $98 remaining
// P1> To: You
// P1> Current bet: 93
// P1> Pot: 179
// P1> Shared: 
// P1> 
// P1> P1:  hand 2♢ 2♠ in for $86, $14 remaining
// P1> P2:  hand -- in for $93, $7 remaining
// P2> To: You
// P2> Current bet: 97
// P2> Pot: 190
// P2> Shared: 
// P2> 
// P2> P1:  hand -- in for $97, $3 remaining
// P2> P2:  hand 5♠ T♡ in for $93, $7 remaining
// P1> To: You
// P1> Current bet: 98
// P1> Pot: 195
// P1> Shared: 
// P1> 
// P1> P1:  hand 2♢ 2♠ in for $97, $3 remaining
// P1> P2:  hand -- in for $98, $2 remaining
// P2> To: You
// P2> Current bet: 100
// P2> Pot: 198
// P2> Shared: 
// P2> 
// P2> P1:  hand -- in for $100, $0 remaining
// P2> P2:  hand 5♠ T♡ in for $98, $2 remaining
// D> To: You
// D> Current bet: 100
// D> Pot: 198
// D> Shared: 
// D> 
// D> P1:  hand -- in for $100, $0 remaining
// D> P2:  hand -- out, $2 remaining
// endState: PokerState = PokerState(
//   moverFn = axle.game.poker.package$$anon$1$$Lambda$7567/0x0000000802d62040@e78a99b,
//   deck = Deck(
//     cards = List(
//       Card(
//         rank = axle.game.cards.R6$@368f1d9a,
//         suit = axle.game.cards.Hearts$@7ae2cc93
//       ),
//       Card(
//         rank = axle.game.cards.R2$@fe6e773,
//         suit = axle.game.cards.Clubs$@71916f9e
//       ),
//       Card(
//         rank = axle.game.cards.King$@6fec2598,
//         suit = axle.game.cards.Hearts$@7ae2cc93
//       ),
//       Card(
//         rank = axle.game.cards.R6$@368f1d9a,
//         suit = axle.game.cards.Diamonds$@14015c5f
//       ),
//       Card(
//         rank = axle.game.cards.R10$@5d1825dc,
//         suit = axle.game.cards.Spades$@278f5c7e
//       ),
//       Card(
//         rank = axle.game.cards.R9$@3f3cdffb,
//         suit = axle.game.cards.Spades$@278f5c7e
//       ),
//       Card(
//         rank = axle.game.cards.R3$@2076adaa,
//         suit = axle.game.cards.Clubs$@71916f9e
//       ),
//       Card(
//         rank = axle.game.cards.R9$@3f3cdffb,
//         suit = axle.game.cards.Diamonds$@14015c5f
//       ),
//       Card(
//         rank = axle.game.cards.R8$@698491fa,
//         suit = axle.game.cards.Hearts$@7ae2cc93
//       ),
//       Card(
//         rank = axle.game.cards.Ace$@2c3ab8b0,
//         suit = axle.game.cards.Spades$@278f5c7e
//       ),
//       Card(
//         rank = axle.game.cards.Jack$@10025472,
//         suit = axle.game.cards.Clubs$@71916f9e
//       ),
// ...

Display outcome to each player

val outcome = evGame.mover(game, endState).swap.toOption.get
// outcome: PokerOutcome = PokerOutcome(
//   winner = Some(value = Player(id = "P1", description = "Player 1")),
//   hand = None
// )

evGame.players(game).foreach { player =>
  playerToWriter(player)(evGameIO.displayOutcomeTo(game, outcome, player)).unsafeRunSync()
}
// D> Winner: Player 1
// D> Hand  : not shown
// P1> Winner: Player 1
// P1> Hand  : not shown
// P2> Winner: Player 1
// P2> Hand  : not shown