Algo to determine pot and split pots

hh
Laurent 6 years ago
parent e71316d70a
commit ce5a1d219b
  1. 1
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Action.kt
  2. 11
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Card.kt
  3. 314
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/HandHistory.kt
  4. 162
      app/src/test/java/net/pokeranalytics/android/HandHistoryTest.kt

@ -10,6 +10,7 @@ import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.extensions.formatted
import timber.log.Timber
import java.text.FieldPosition
/***

@ -102,9 +102,20 @@ open class Card : RealmObject() {
fun format(suit: Suit?) : String {
return suit?.value ?: UNDEFINED.value
}
fun color(suit: Suit?) : Int {
return suit?.color ?: R.color.white
}
fun valueOf(suit: Int): Suit {
return when (suit) {
0 -> SPADES
1 -> DIAMOND
2 -> CLOVER
3 -> HEART
else -> throw PAIllegalStateException("unknow value: $suit")
}
}
}
val color: Int

@ -11,7 +11,6 @@ import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.handhistory.HandSetup
import net.pokeranalytics.android.model.handhistory.Position
@ -29,11 +28,13 @@ import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.extensions.addLineReturn
import net.pokeranalytics.android.util.extensions.formatted
import net.pokeranalytics.android.util.extensions.fullDate
import timber.log.Timber
import java.util.*
import kotlin.Comparator
data class PositionAmount(var position: Int, var amount: Double, var isAllin: Boolean)
open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable, TimeFilterable,
CardHolder {
CardHolder, Comparator<PositionAmount> {
@PrimaryKey
override var id = UUID.randomUUID().toString()
@ -481,74 +482,74 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
return views
}
data class Pot(var amount: Double, var positions: Set<Int>, var level: Double? = null)
fun pots(): List<Pot> {
var currentPot = 0.0
val positions = this.positionIndexes.toMutableSet()
val pots = mutableListOf<Pot>()
var allinAtStreet: Street? = null
val allinActions = mutableListOf<Action>()
this.sortedActions.forEach {
if (allinAtStreet == null) {
when {
it.type == Action.Type.FOLD -> {
positions.remove(it.position)
}
it.type?.isPullOut == false -> {
currentPot += it.effectiveAmount
}
it.type == Action.Type.CALL_ALLIN -> {
allinAtStreet = it.street
// TODO create pot
}
it.type?.isAllin == true -> {
currentPot += it.effectiveAmount
allinAtStreet = it.street
allinActions.add(it)
}
else -> {
Timber.d("unmanaged action type: ${it.type}")
// throw PAIllegalStateException("unmanaged action type: ${it.type}")
}
}
} else { // Allin situation
if (it.street != allinAtStreet) {
allinAtStreet = null
allinActions.clear()
} else {
when {
it.type == Action.Type.FOLD -> {
positions.remove(it.position)
}
it.type == Action.Type.CALL -> {
currentPot += it.effectiveAmount
allinActions.add(it)
}
it.type == Action.Type.CALL_ALLIN -> {
}
}
}
}
}
if (currentPot > 0.0) {
pots.add(Pot(currentPot, positions))
}
return pots
}
data class Pot(var amount: Double, var level: Double, var positions: MutableSet<Int> = mutableSetOf())
// fun pots(): List<Pot> {
//
// var currentPot = 0.0
// val positions = this.positionIndexes.toMutableSet()
// val pots = mutableListOf<Pot>()
// var allinAtStreet: Street? = null
// val allinActions = mutableListOf<Action>()
// this.sortedActions.forEach {
//
// if (allinAtStreet == null) {
//
// when {
// it.type == Action.Type.FOLD -> {
// positions.remove(it.position)
// }
// it.type?.isPullOut == false -> {
// currentPot += it.effectiveAmount
// }
// it.type == Action.Type.CALL_ALLIN -> {
// allinAtStreet = it.street
// // TODO create pot
// }
// it.type?.isAllin == true -> {
// currentPot += it.effectiveAmount
// allinAtStreet = it.street
// allinActions.add(it)
// }
// else -> {
// Timber.d("unmanaged action type: ${it.type}")
//// throw PAIllegalStateException("unmanaged action type: ${it.type}")
// }
// }
//
// } else { // Allin situation
//
// if (it.street != allinAtStreet) {
// allinAtStreet = null
// allinActions.clear()
// } else {
// when {
// it.type == Action.Type.FOLD -> {
// positions.remove(it.position)
// }
// it.type == Action.Type.CALL -> {
// currentPot += it.effectiveAmount
// allinActions.add(it)
// }
// it.type == Action.Type.CALL_ALLIN -> {
//
// }
//
// }
//
//
//
// }
//
// }
//
// }
//
// if (currentPot > 0.0) {
//// pots.add(Pot(currentPot, positions))
// }
//
// return pots
// }
/***
* Defines which positions win the hand
@ -567,54 +568,144 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
pot.amount = potSizeForStreet(Street.SUMMARY)
listOf(pot)
}
else -> this.getWinningsByPosition(activePositions) // Several players remains, typically BET/FOLD or CHECKS
else -> { // Several players remains, typically BET/FOLD or CHECKS
this.wonPots(getPots(activePositions))
}
}
this.winnerPots.clear()
this.winnerPots.addAll(wonPots)
}
private fun getPots(eligiblePositions: List<Int>): List<Pot> {
var runningPotAmount = 0.0
val pots = mutableListOf<Pot>()
Street.values().forEach { street ->
val streetActions = this.actions.filter { it.street == street }
val allinPositions = streetActions.filter { it.type?.isAllin == true }.map { it.position }
if (allinPositions.isEmpty()) {
runningPotAmount += streetActions.sumByDouble { it.effectiveAmount }
} else {
val amountsPerPosition = mutableListOf<PositionAmount>()
this.positionIndexes.map { position ->
val playerActions = streetActions.filter { it.position == position }
val sum = playerActions.sumByDouble { it.effectiveAmount }
amountsPerPosition.add(PositionAmount(position, sum, allinPositions.contains(position)))
}
amountsPerPosition.sortWith(this) // sort by value, then allin. Allin must be first of equal values sequence
val streetPots = mutableListOf<Pot>()
amountsPerPosition.forEach { positionAmount ->
val amount = positionAmount.amount
val position = positionAmount.position
val isAllin = allinPositions.contains(position)
if (!isAllin) {
if (streetPots.isEmpty()) {
runningPotAmount += amount
} else {
var rest = amount
streetPots.forEach { pot ->
pot.amount += pot.level
if (eligiblePositions.contains(position)) {
pot.positions.add(position)
}
rest -= pot.level
}
runningPotAmount += rest
}
} else {
runningPotAmount += amount
streetPots.add(Pot(runningPotAmount, amount, mutableSetOf(position)))
runningPotAmount = 0.0
}
}
pots.addAll(streetPots)
}
}
if (runningPotAmount > 0.0) {
pots.add(Pot(runningPotAmount, 0.0, eligiblePositions.toMutableSet()))
}
return pots
}
/***
* Compares the hands of the players at the given [positions]
* Returns the list of winning hands by position + chips won
*/
private fun getWinningsByPosition(positions: List<Int>): Collection<WonPot> {
// get the total committed amounts for each position, same order
val committedAmounts = this.positionIndexes.map { position ->
this.actions.filter { it.position == position }.sumByDouble { it.effectiveAmount }
}.toMutableList()
// get the various committed levels, ascendly sorted
val sortedPotLevels = committedAmounts.toSet().toList().sorted()
// private fun getWinningsByPosition(positions: List<Int>): Collection<WonPot> {
//
// // get the total committed amounts for each position, same order
// val committedAmounts = this.positionIndexes.map { position ->
// this.actions.filter { it.position == position }.sumByDouble { it.effectiveAmount }
// }.toMutableList()
//
// // get the various committed levels, ascendly sorted
// val sortedPotLevels = committedAmounts.toSet().toList().sorted()
//
// var previousPotLevel = 0.0 // previous pot level, to remove from the next level
//
// // Iterate on each pot
// sortedPotLevels.forEach { potLevel ->
//
// val potEligiblePositions = mutableListOf<Int>()
// var pot = 0.0
// val asked = potLevel - previousPotLevel
// committedAmounts.forEachIndexed { index, playerAmount ->
//
// // if the player has the asked amount, he becomes eligible for the pot
// if (playerAmount >= asked) {
// potEligiblePositions.add(positions[index])
// committedAmounts[index] = playerAmount - asked
// pot += asked
// } else if (playerAmount != 0.0) {
// throw PAIllegalStateException("Issue in pot calculations")
// }
// }
// previousPotLevel = potLevel
//
// // get the winning positions
// val winningPositions = compareHands(potEligiblePositions)
//
// // Distributes the pot for each winners
// val share = pot / winningPositions.size
// winningPositions.forEach { p ->
// val wp = wonPots[p]
// if (wp == null) {
// val wonPot = WonPot()
// wonPot.position = p
// wonPot.amount = share
// wonPots[p] = wonPot
// } else {
// wp.amount += share
// }
// }
// }
//
// return wonPots.values
// }
private fun wonPots(pots: List<Pot>): Collection<WonPot> {
val wonPots = hashMapOf<Int, WonPot>()
var previousPotLevel = 0.0 // previous pot level, to remove from the next level
// Iterate on each pot
sortedPotLevels.forEach { potLevel ->
val potEligiblePositions = mutableListOf<Int>()
var pot = 0.0
val asked = potLevel - previousPotLevel
committedAmounts.forEachIndexed { index, playerAmount ->
// if the player has the asked amount, he becomes eligible for the pot
if (playerAmount >= asked) {
potEligiblePositions.add(positions[index])
committedAmounts[index] = playerAmount - asked
pot += asked
} else if (playerAmount != 0.0) {
throw PAIllegalStateException("Issue in pot calculations")
}
}
previousPotLevel = potLevel
// get the winning positions
val winningPositions = compareHands(potEligiblePositions)
pots.forEach { pot ->
val winningPositions = compareHands(pot.positions.toList())
// Distributes the pot for each winners
val share = pot / winningPositions.size
val share = pot.amount / winningPositions.size
winningPositions.forEach { p ->
val wp = wonPots[p]
if (wp == null) {
@ -626,8 +717,8 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
wp.amount += share
}
}
}
}
return wonPots.values
}
@ -661,4 +752,17 @@ open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable,
}
/***
* return a negative integer, zero, or a positive integer as the
* first argument is less than, equal to, or greater than the
* second.
*/
override fun compare(o1: PositionAmount, o2: PositionAmount): Int {
return if (o1.amount == o2.amount) {
if (o1.isAllin) -1 else 1
} else {
(o1.amount - o2.amount).toInt()
}
}
}

@ -0,0 +1,162 @@
package net.pokeranalytics.android
import net.pokeranalytics.android.model.handhistory.HandSetup
import net.pokeranalytics.android.model.handhistory.Street
import net.pokeranalytics.android.model.realm.handhistory.Action
import net.pokeranalytics.android.model.realm.handhistory.Card
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.model.realm.handhistory.PlayerSetup
import org.junit.Assert
import org.junit.Test
fun HandHistory.addAction(street: Street, position: Int, type: Action.Type, effectiveAmount: Double = 0.0) {
val action = Action()
action.street = street
action.position = position
action.type = type
// action.amount = amount
action.effectiveAmount = effectiveAmount
action.index = this.actions.size
this.actions.add(action)
}
class HandHistoryTest {
private fun handHistoryInstance(players: Int): HandHistory {
val hh = HandHistory()
val hs = HandSetup()
hs.smallBlind = 1.0
hs.bigBlind = 2.0
hs.tableSize = players
hh.configure(hs)
return hh
}
private fun playerSetupInstance(position: Int, cards: List<Card>): PlayerSetup {
val ps = PlayerSetup()
ps.position = position
ps.cards.addAll(cards)
return ps
}
private fun card(value: Int, suit: Int): Card {
return Card.newInstance(value, Card.Suit.valueOf(suit))
}
private fun board(values: List<Int>, suits: List<Int>): List<Card> {
val cards = mutableListOf<Card>()
(0 until 5).forEach { index ->
cards.add(card(values[index], suits[index]))
}
return cards
}
private val delta = 0.0001
@Test
fun potTest1() { // A simple BET / FOLD
val hh = handHistoryInstance(2)
hh.addAction(Street.PREFLOP, 0, Action.Type.CHECK)
hh.addAction(Street.PREFLOP, 1, Action.Type.BET, 6.0)
hh.addAction(Street.PREFLOP, 0, Action.Type.FOLD)
hh.defineWinnerPositions()
val winnerPots = hh.winnerPots
assert(winnerPots.size == 1)
val wp = winnerPots.first()!!
Assert.assertEquals(9.0, wp.amount, delta)
Assert.assertEquals(1, wp.position)
}
@Test
fun potTest2() { // A simple BET / CALL
val hh = handHistoryInstance(2)
hh.addAction(Street.PREFLOP, 0, Action.Type.BET, 6.0)
hh.addAction(Street.PREFLOP, 1, Action.Type.CALL, 6.0)
val ps1 = playerSetupInstance(0, listOf(card(3, 0), card(3, 1)))
val ps2 = playerSetupInstance(1, listOf(card(2, 0), card(2, 1)))
hh.playerSetups.addAll(listOf(ps1, ps2))
hh.board.addAll(board(listOf(4, 5, 6, 7, 9), listOf(0, 1, 2, 1, 2)))
hh.defineWinnerPositions()
val winnerPots = hh.winnerPots
Assert.assertEquals(1, winnerPots.size)
val wp = winnerPots.first()!!
Assert.assertEquals(15.0, wp.amount, delta)
Assert.assertEquals(0, wp.position)
}
@Test
fun potTest3() { // A split pot with a BET / CALL with same hand, a flush
val hh = handHistoryInstance(2)
hh.addAction(Street.PREFLOP, 0, Action.Type.BET, 6.0)
hh.addAction(Street.PREFLOP, 1, Action.Type.CALL, 6.0)
val ps1 = playerSetupInstance(0, listOf(card(3, 0), card(3, 1)))
val ps2 = playerSetupInstance(1, listOf(card(2, 0), card(2, 1)))
hh.playerSetups.addAll(listOf(ps1, ps2))
hh.board.addAll(board(listOf(4, 5, 6, 7, 9), listOf(3, 3, 3, 3, 3)))
hh.defineWinnerPositions()
val winnerPots = hh.winnerPots
Assert.assertEquals(2, winnerPots.size)
winnerPots.forEach {
Assert.assertEquals(7.5, it.amount, delta)
}
}
@Test
fun potTest4() { // A multi allin pot
val hh = handHistoryInstance(3) // 3
hh.addAction(Street.PREFLOP, 0, Action.Type.BET, 6.0)
hh.addAction(Street.PREFLOP, 1, Action.Type.CALL, 6.0)
hh.addAction(Street.PREFLOP, 2, Action.Type.CALL, 6.0) // 21
hh.addAction(Street.FLOP, 0, Action.Type.BET, 10.0)
hh.addAction(Street.FLOP, 1, Action.Type.CALL, 10.0)
hh.addAction(Street.FLOP, 2, Action.Type.RAISE_ALLIN, 100.0)
hh.addAction(Street.FLOP, 0, Action.Type.CALL, 90.0)
hh.addAction(Street.FLOP, 1, Action.Type.CALL, 90.0) // main 321
hh.addAction(Street.TURN, 0, Action.Type.BET, 100.0)
hh.addAction(Street.TURN, 1, Action.Type.RAISE_ALLIN, 200.0)
hh.addAction(Street.TURN, 0, Action.Type.CALL, 100.0) // side 400
val ps0 = playerSetupInstance(0, listOf(card(10, 3), card(2, 1)))
val ps1 = playerSetupInstance(1, listOf(card(2, 3), card(3, 1)))
val ps2 = playerSetupInstance(2, listOf(card(12, 3), card(2, 1)))
hh.playerSetups.addAll(listOf(ps0, ps1, ps2))
hh.board.addAll(board(listOf(4, 5, 6, 7, 9), listOf(3, 3, 3, 3, 3)))
hh.defineWinnerPositions()
val winnerPots = hh.winnerPots
Assert.assertEquals(2, winnerPots.size)
val mainPot = winnerPots.first { it.position == 2 }
Assert.assertEquals(321.0, mainPot.amount, delta)
val sidePot = winnerPots.first { it.position == 0 }
Assert.assertEquals(400.0, sidePot.amount, delta)
}
}
Loading…
Cancel
Save