diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Action.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Action.kt index 50cbd9b9..1fcd8b05 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Action.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Action.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 /*** diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Card.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Card.kt index a1b94e2e..4224a146 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Card.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Card.kt @@ -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 diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/HandHistory.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/HandHistory.kt index 0cd44a07..01d4f5cd 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/HandHistory.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/HandHistory.kt @@ -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 { @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, var level: Double? = null) - fun pots(): List { - - var currentPot = 0.0 - val positions = this.positionIndexes.toMutableSet() - val pots = mutableListOf() - var allinAtStreet: Street? = null - val allinActions = mutableListOf() - 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 = mutableSetOf()) +// fun pots(): List { +// +// var currentPot = 0.0 +// val positions = this.positionIndexes.toMutableSet() +// val pots = mutableListOf() +// var allinAtStreet: Street? = null +// val allinActions = mutableListOf() +// 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): List { + + + var runningPotAmount = 0.0 + val pots = mutableListOf() + 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() + 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() + 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): Collection { - - // 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): Collection { +// +// // 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() +// 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): Collection { val wonPots = hashMapOf() - var previousPotLevel = 0.0 // previous pot level, to remove from the next level - - // Iterate on each pot - sortedPotLevels.forEach { potLevel -> - - val potEligiblePositions = mutableListOf() - 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() + } + } + } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/WonPot.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/WonPot.kt index ee445be5..f4494d59 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/WonPot.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/WonPot.kt @@ -14,4 +14,4 @@ open class WonPot: RealmObject() { */ var amount: Double = 0.0 -} \ No newline at end of file +} diff --git a/app/src/test/java/net/pokeranalytics/android/HandHistoryTest.kt b/app/src/test/java/net/pokeranalytics/android/HandHistoryTest.kt new file mode 100644 index 00000000..21456a50 --- /dev/null +++ b/app/src/test/java/net/pokeranalytics/android/HandHistoryTest.kt @@ -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): 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, suits: List): List { + val cards = mutableListOf() + (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) + + } + +} \ No newline at end of file