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,
//       ),
//       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,
//       ),
//       Card(
//         rank = axle.game.cards.Ace\$@2c3ab8b0,
//       ),
//       Card(
//         rank = axle.game.cards.R9\$@3f3cdffb,
//       ),
//       Card(
//         rank = axle.game.cards.R6\$@368f1d9a,
//         suit = axle.game.cards.Hearts\$@7ae2cc93
//       ),
//       Card(
//         rank = axle.game.cards.Jack\$@10025472,
//       )
//     )
//   ),
// ...

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,
//   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>,
// )
``````

Render as SVG file

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

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

### 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 =
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) =>
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,
//       ),
//       Card(
//         rank = axle.game.cards.R9\$@3f3cdffb,
//       ),
//       Card(
//         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,
//       ),
//       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
``````