Big Refactoring: Splitting HHBuilder into HHBuilder + ActionList

hh
Laurent 6 years ago
parent f8ab6489c5
commit b203763882
  1. 791
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/HHBuilder.kt
  2. 2
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/HandHistoryAdapter.kt
  3. 26
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/HandHistoryFragment.kt
  4. 14
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/HandHistoryViewModel.kt
  5. 500
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/model/ActionList.kt
  6. 13
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/model/ComputedAction.kt
  7. 390
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/model/HHBuilder.kt
  8. 5
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/views/CardsRow.kt

@ -1,791 +0,0 @@
package net.pokeranalytics.android.ui.modules.handhistory
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.handhistory.*
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.ui.modules.handhistory.views.CardCentralizer
import net.pokeranalytics.android.ui.modules.handhistory.views.CardsRow
import net.pokeranalytics.android.ui.modules.handhistory.views.PlayerCardsRow
import net.pokeranalytics.android.ui.modules.handhistory.views.StreetCardsRow
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable
import net.pokeranalytics.android.util.extensions.formatted
import timber.log.Timber
import kotlin.math.max
enum class HHKeyboard {
ACTION,
AMOUNT,
CARD;
}
interface ActionManager {
fun getStreetLastSignificantAction(street: Street, index: Int): ComputedAction?
fun getPreviouslyCommittedAmount(index: Int): Double?
fun selectAction(index: Int, actionType: Action.Type) : List<Int>?
fun getStreetNextCalls(index: Int): List<ComputedAction>
fun getPlayerNextStreetActions(index: Int): List<ComputedAction>
fun dropNextActions(index: Int): Boolean
}
class ActionList : ArrayList<ComputedAction>() {
/***
* Keep the first [n] elements
*/
fun keepFirst(n: Int) {
val cut = this.take(n)
this.clear()
this.addAll(cut)
}
/***
* Returns the first action of the given [street]
*/
fun firstStreetAction(street: Street): ComputedAction {
return this.first { it.street == street }
}
/***
* Returns the last action index of the street, for the action at [index]
*/
fun lastIndexOfStreet(index: Int): Int {
val street = this[index].street
return this.last { it.street == street }.action.index
}
}
class HHBuilder : CardCentralizer, ActionManager {
/***
* The hand history
*/
private var handHistory: HandHistory
set(value) {
field = value
setNumberOfPlayers(value.numberOfPlayers)
load()
}
/***
* All actions sorted by index
*/
private var sortedActions: ActionList = ActionList()
/***
* The board cards sorted by position
*/
// private lateinit var boardManager: BoardManager
/***
* A LinkedHashSet containing the sorted positions at the table
*/
var positions: LinkedHashSet<Position> = linkedSetOf()
/***
* The maximum number of cards in a player's hand
*/
private var playerHandMaxCards: Int? = null
set(value) {
field = value
this.rowRepresentables.filterIsInstance<PlayerCardsRow>().forEach {
it.maxCards = value
}
}
/***
* Creates a builder using a [handSetup]
* Also creates a new Hand History and configures it according to the [handSetup]
*/
constructor(handSetup: HandSetup) {
val handHistory = HandHistory()
handHistory.configure(handSetup)
this.playerHandMaxCards = handSetup.game?.playerHandMaxCards
this.handHistory = handHistory
}
/***
* Creates a builder using the parameter [handHistory]
*/
constructor(handHistory: HandHistory) {
this.handHistory = handHistory
}
/***
* Returns the action
*/
private fun actionForIndex(index: Int) : ComputedAction {
return this.sortedActions[index]
}
/***
* Fills the [sortedActions] variable with the proper actions
* Pre-computes the potsizes for the video export
*/
private fun load() {
var totalPotSize = 0.0
// sorted actions
val computedActions = ActionList()
val sortedActions = this.handHistory.actions.sortedBy { it.index }
sortedActions.forEach { action ->
totalPotSize += action.effectiveAmount
val position = this.positions.elementAt(action.position)
val ca = ComputedAction(
this,
action,
totalPotSize,
action.positionRemainingStack,
position
)
computedActions.add(ca)
}
this.sortedActions = computedActions
// this.boardManager = BoardManager(this.handHistory.board, this)
this.createRowRepresentation()
}
/***
* Returns the list of available user actions at [index]
*/
fun availableActions(index: Int) : Set<Action.Type> {
val computedAction = this.actionForIndex(index)
val position = computedAction.position
val lastSignificantAction: ComputedAction? = getStreetLastSignificantAction(computedAction.street, index)
return if (lastSignificantAction == null) {
setOf(Action.Type.CHECK, Action.Type.BET, Action.Type.UNDEFINED_ALLIN)
} else {
val remainingStack = getLastPlayerAction(index)?.playerRemainingStack
val actionAmount = lastSignificantAction.action.amount
when (lastSignificantAction.action.type) {
Action.Type.POST_SB, Action.Type.POST_BB, Action.Type.STRADDLE -> {
if (position == lastSignificantAction.position) {
setOf(Action.Type.FOLD, Action.Type.CHECK, Action.Type.BET, Action.Type.UNDEFINED_ALLIN)
} else {
setOf(Action.Type.STRADDLE, Action.Type.FOLD, Action.Type.CALL, Action.Type.BET, Action.Type.UNDEFINED_ALLIN)
}
}
Action.Type.BET, Action.Type.RAISE -> {
if (remainingStack != null && actionAmount != null && remainingStack <= actionAmount) {
setOf(Action.Type.FOLD, Action.Type.CALL_ALLIN)
} else {
setOf(Action.Type.FOLD, Action.Type.CALL, Action.Type.RAISE, Action.Type.UNDEFINED_ALLIN)
}
}
Action.Type.RAISE_ALLIN, Action.Type.BET_ALLIN -> {
if (remainingStack != null && actionAmount != null && remainingStack <= actionAmount) {
setOf(Action.Type.FOLD, Action.Type.CALL_ALLIN)
} else if (activePositions(index).size == 2 && remainingStack != null && actionAmount != null && remainingStack > actionAmount) {
setOf(Action.Type.FOLD, Action.Type.CALL)
} else {
setOf(Action.Type.FOLD, Action.Type.CALL, Action.Type.RAISE, Action.Type.UNDEFINED_ALLIN)
}
}
else -> {
throw PAIllegalStateException("We should not handle this action: ${lastSignificantAction.action.type}")
}
}
}
}
/***
* Selects an action type for the action at the provided [index]
* In case of UNDEFINED_ALLIN, we define which type of allin it is.
* If the user changes the current action,
* for convenience we remove all the following actions to avoid managing complex cases
* Also calculates the player effective amounts in proper cases
* Returns either:
* - null when actions have been deleted, requiring a whole table refresh, because streets might me lost
* - the list of modified action indexes that are not the action at [index]
*/
override fun selectAction(index: Int, actionType: Action.Type) : List<Int>? {
var type = actionType
val computedAction = this.actionForIndex(index)
// define allin type
if (type == Action.Type.UNDEFINED_ALLIN) {
val significant = getStreetLastSignificantAction(computedAction.street, index - 1)
if (significant != null) {
val betAmount = significant.action.amount
val remainingStack = computedAction.playerRemainingStack
type = if (remainingStack != null && betAmount != null && remainingStack < betAmount) {
Action.Type.CALL_ALLIN
} else {
Action.Type.RAISE_ALLIN
}
} else {
type = Action.Type.BET_ALLIN
}
}
Timber.d(">>> Sets $type at index: $index")
var structureModified = computedAction.setType(type) // false
// Automatically sets action for the previous empty actions
val modifiedActions = mutableListOf<ComputedAction>()
getPreviousEmptyActions(index).forEach {
modifiedActions.add(it)
val lastSignificant = getStreetLastSignificantAction(computedAction.street, index - 1)
it.action.type = if (lastSignificant != null) {
Action.Type.FOLD
} else {
Action.Type.CHECK
}
}
structureModified = this.updateFollowupActions(index) || structureModified
if (structureModified) return null
return modifiedActions.map { this.rowRepresentables.indexOf(it) }
}
/***
* Sets the amount for the action at the provided [index]
*/
fun setAmount(index: Int, amount: Double) {
val computedAction = this.actionForIndex(index)
Timber.d(">>> Sets $amount at index: $index, for action ${computedAction.action.type}")
computedAction.setAmount(amount)
}
/***
* Adds, if necessary, new ComputedAction for players that needs to act
* Also adds, if necessary, the Street separators and board selectors
* Returns true if the action list has been modified
*/
private fun updateFollowupActions(index: Int) : Boolean {
val computedAction = this.actionForIndex(index)
val type = computedAction.action.type
var actionsChanged = addsFollowupActionsIfNecessary(index)
when (type?.isSignificant) {
false -> { // closes the action, pass to next street if necessary
if (type == Action.Type.CALL_ALLIN) { // sometimes we can go from RAISE_ALLIN to CALL_IN, changing the actions required
removeActionsIfNecessary(index)
}
actionsChanged = createNextStreetIfNecessary(index) || actionsChanged
}
else -> {}
}
return actionsChanged
}
/***
* Recreates the actions after [index]
*/
private fun removeActionsIfNecessary(index: Int) {
this.sortedActions.keepFirst(index + 1)
addsFollowupActionsIfNecessary(index)
}
/***
* Check if some positions are still required to play,
* and adds new actions if none exists after [index]
* Returns true if actions have been added
*/
private fun addsFollowupActionsIfNecessary(index: Int) : Boolean {
val street = this.actionForIndex(index).street
val refAction = getStreetLastSignificantAction(street, index)
?: this.sortedActions.firstStreetAction(street)
val refIndex = refAction.action.index
val refIndexPosition = refAction.position
val activePositions = activePositions(refIndex)
activePositions.remove(refIndexPosition)
// We want to remove positions that already have an action after [refIndex]
for (i in refIndex + 1 until this.sortedActions.size) {
val ca = this.actionForIndex(i)
activePositions.remove(ca.position)
}
// Circularly adds an action for missing positions
val firstPositionAfterCurrent = max(0, activePositions.indexOfFirst { it.ordinal > refIndexPosition.ordinal })
for (i in 0 until activePositions.size) {
val position = activePositions[(firstPositionAfterCurrent + i) % activePositions.size]
this.addNewEmptyAction(position, refAction.street, refAction.totalPotSize, lastRemainingStack(position, index))
}
return activePositions.isNotEmpty()
}
/***
* Returns the last remaining stack of the player, if available
*/
private fun lastRemainingStack(position: Position, index: Int): Double? {
return this.sortedActions.take(index).lastOrNull { it.position == position }?.playerRemainingStack
}
/***
* Creates a new action at the end of the action stack, and in the HH representation
*/
private fun addNewEmptyAction(position: Position, street: Street, currentPotSize: Double, remainingStack: Double?) {
val action = Action()
action.index = this.sortedActions.size
action.position = position.ordinal
action.street = street
val computedAction =
ComputedAction(this,
action,
currentPotSize,
remainingStack,
position
)
this.sortedActions.add(computedAction)
this.rowRepresentables.add(computedAction)
}
/***
* Returns the list of position still in play before the given [index]
*/
private fun activePositions(index: Int, showDown: Boolean = false): MutableList<Position> {
val oustedPositions = this.sortedActions.take(index + 1)
.filter {
if (showDown) {
it.action.type == Action.Type.FOLD
} else {
it.action.type?.isPullOut ?: false
}
}
.map { it.position }
val allPositions = this.positions.clone() as LinkedHashSet<Position>
allPositions.removeAll(oustedPositions)
return allPositions.toMutableList()
}
/***
* Creates a new street if no player needs to act
* Returns true if a street has been created
*/
private fun createNextStreetIfNecessary(index: Int) : Boolean {
val nextStreet = isStreetActionClosed(index)
if (nextStreet != null && this.sortedActions.firstOrNull { it.street == nextStreet } == null) {
createStreet(nextStreet)
return true
}
return false
}
/***
* Creates a new street:
* - Adds a Street Header
* - Adds a Board if necessary
* - Adds empty actions for the remaining players
*/
private fun createStreet(street: Street) {
val lastComputedAction = this.sortedActions.last()
val totalPotSize = lastComputedAction.totalPotSize
addStreetHeader(this.rowRepresentables, street)
val lastActionIndex = lastComputedAction.action.index
val isShowDown = street == Street.SUMMARY
activePositions(lastActionIndex, isShowDown).sortedBy { it.ordinal }.forEach {
when (street) {
Street.SUMMARY -> {
this.rowRepresentables.add(PlayerCardsRow(it))
}
else -> {
addNewEmptyAction(it, street, totalPotSize, lastRemainingStack(it, lastActionIndex))
}
}
}
}
/***
* Returns the list of empty actions before the action at the given [index]
*/
private fun getPreviousEmptyActions(index: Int) : List<ComputedAction> {
val street = this.actionForIndex(index).street
return this.sortedActions.take(index).filter { it.street == street && it.action.type == null }
}
/***
* Removes the actions, not the automatically created ones,
* following an action change.
* We want drop all non-auto added rows after the index
*/
override fun dropNextActions(index: Int): Boolean {
val sizeBefore = this.sortedActions.size
this.sortedActions.keepFirst(index + 1)
this.updateFollowupActions(index)
this.createRowRepresentation()
val sizeAfter = this.sortedActions.size
return sizeAfter != sizeBefore
}
override fun getPlayerNextStreetActions(index: Int): List<ComputedAction> {
val computedAction = this.sortedActions[index]
return this.sortedActions.drop(index + 1).filter {
it.street == computedAction.street && it.position == computedAction.position
}
}
/***
* Returns the committed amount by the player for the street at the current [index]
*/
override fun getPreviouslyCommittedAmount(index: Int): Double? {
val computedAction = this.actionForIndex(index)
val position = computedAction.position
val street = computedAction.action.street
val previousActions = this.sortedActions.take(index)
val previousComputedAction = previousActions.lastOrNull {
it.position == position && it.street == street
}
previousComputedAction?.action?.let { previousAction ->
return when (previousAction.type) {
Action.Type.POST_BB, Action.Type.POST_SB, Action.Type.STRADDLE, Action.Type.BET, Action.Type.RAISE -> previousAction.amount
Action.Type.CALL -> getStreetLastSignificantAction(previousAction.street, previousAction.index)?.action?.amount
else -> null
}
}
return null
}
/***
* Returns the last significant player action, if any, for the action at the provided [index]
*/
override fun getStreetLastSignificantAction(street: Street, index: Int): ComputedAction? {
val previousActions = this.sortedActions.take(index + 1).filter { it.street == street }
return previousActions.lastOrNull { it.action.isActionSignificant }
}
/***
* Returns all "CALL" ComputedAction between the [index] and the next significant action
*/
override fun getStreetNextCalls(index: Int): List<ComputedAction> {
val streetNextSignificantIndex = getStreetNextSignificantAction(index)?.action?.index ?: this.sortedActions.lastIndexOfStreet(index)
return this.sortedActions.filter {
it.action.index in ((index + 1) until streetNextSignificantIndex)
&& (it.action.type?.isCall ?: false)
}
}
/***
* Returns the last user action, if any, for the action at the provided [index]
*/
private fun getLastPlayerAction(index: Int): ComputedAction? {
val action = this.actionForIndex(index).action
val previousActions = this.sortedActions.take(index)
return previousActions.lastOrNull { it.action.position == action.position }
}
/***
* Returns the next significant player action in the street, if any, for the action at the provided [index]
*/
private fun getStreetNextSignificantAction(index: Int): ComputedAction? {
val street = this.actionForIndex(index).street
val nextActions = this.sortedActions.drop(index + 1).filter { it.street == street }
return nextActions.firstOrNull { it.action.isActionSignificant }
}
/***
* Returns a list of positions at the provided [index]
*/
fun positionsAtIndex(index: Int): List<Position> {
val currentStreet = this.actionForIndex(index).street
val streetActions = this.sortedActions.filter { it.street == currentStreet }
return streetActions.drop(index + 1).map { it.position }
}
/***
* Sets the number of players playing the hand
* Defines the appropriate positions for this player count
*/
fun setNumberOfPlayers(playerCount: Int) {
this.handHistory.numberOfPlayers = playerCount
this.positions = Position.positionsPerPlayers(playerCount)
}
/***
* Returns the index of a ComputedAction given its [rowRepresentableIndex]
*/
fun indexOfComputedAction(rowRepresentableIndex: Int) : Int {
val computedAction = this.rowRepresentables[rowRepresentableIndex] as ComputedAction
return computedAction.action.index
}
/***
* Finds the index of the first incomplete action, if existing
* If the same selection is the same than current, pass to the next row
*/
fun findIndexForEdition(startIndex: Int): HHSelection? {
this.rowRepresentables.forEachIndexed { index, rowRepresentable ->
if (index >= startIndex && rowRepresentable is HandHistoryRow) {
val foundKeyboard = rowRepresentable.keyboardForCompletion()
if (foundKeyboard != null) {
return HHSelection(index, foundKeyboard)
}
}
}
return null
}
/***
* Returns the index, strictly superior to [actionIndexForSelection],
* of the next action of the given [position]
*/
fun nextActionIndex(actionIndexForSelection: Int, position: Position): Int {
var i = actionIndexForSelection + 1
while (true) {
val computedAction = this.actionForIndex(i)
if (computedAction.position == position) {
return this.rowRepresentables.indexOf(computedAction)
}
i++
if (i > this.sortedActions.size) throw PAIllegalStateException("algo sucks")
}
}
/***
* Adds a card with the selected [value]
*/
fun cardValueSelected(value: Card.Value, currentSelection: HHSelection) {
this.lastValue = value
val row = this.rowRepresentables[currentSelection.index] as CardsRow
row.valueSelected(value)
}
/***
* Either affect the [suit] to the current card,
* or create a Card with an empty value and the [suit]
*/
fun cardSuitSelected(suit: Card.Suit, currentSelection: HHSelection) {
val row = this.rowRepresentables[currentSelection.index] as CardsRow
row.suitSelected(suit)
// TODO do we want to store the information right now ?
}
/***
* Deletes all the card of the selected street
*/
fun clearCards(currentSelection: HHSelection) {
this.lastValue = null
val row = this.rowRepresentables[currentSelection.index] as CardsRow
row.clear()
}
/***
* Delete the last property of the last Card of the selected street
* We don't want empty Card (value + suit),
* so we delete the whole card if both information are null
*/
fun deleteLastCardProperty(currentSelection: HHSelection) {
val row = this.rowRepresentables[currentSelection.index] as CardsRow
row.deleteLastCardProperty()
}
/***
* When a card selection is ended,
* sets the playerHandMaxCards with the number of cards set to the player
*/
fun cardSelectionEnded(index: Int) {
val cardsRow = this.rowRepresentables[index] as CardsRow
if (cardsRow is PlayerCardsRow) {
this.playerHandMaxCards = cardsRow.cards.size
}
}
// BoardChangedListener
/***
* Called when the board has changed
*/
fun boardChanged(cards: List<Card>) {
this.rowRepresentables.filterIsInstance<StreetCardsRow>().forEach {
// it.cards = cards
}
}
/***
* Saves the current hand state in the database
*/
private fun save() {
}
/***
* The list of row representables, generated from the hand history actions
*/
var rowRepresentables = mutableListOf<RowRepresentable>()
private set
private fun createRowRepresentation() {
val rows: MutableList<RowRepresentable> = mutableListOf()
rows.add(CustomizableRowRepresentable(customViewType = HandRowType.HEADER, resId = R.string.settings, value = ""))
Street.values().forEach { street ->
val actions = this.sortedActions.filter { it.street == street }
when (street) {
Street.SUMMARY -> {
val lastActionIndex = this.sortedActions.size - 1
if (activePositions(lastActionIndex).size < 2 || isStreetActionClosed(lastActionIndex) == Street.SUMMARY) {
addStreetHeader(rows, street)
activePositions(lastActionIndex).forEach {
val positionIndex = this.positions.indexOf(it)
val playerCardsRow = PlayerCardsRow(it, this.handHistory.cardsForPosition(positionIndex), this.playerHandMaxCards)
this.rowRepresentables.add(playerCardsRow)
}
}
}
else -> {
if (actions.isNotEmpty()) {
addStreetHeader(rows, street)
rows.addAll(actions)
}
}
}
}
this.rowRepresentables = rows
}
/***
* This function evaluates whether the street, identified by an action [index],
* has its action closed, meaning no more player are required to play.
* In case of closing, the function returns the next logical Street
*/
private fun isStreetActionClosed(index: Int) : Street? {
val currentStreet = this.actionForIndex(index).street
getStreetLastSignificantAction(currentStreet, index)?.let { significantAction ->
val activePositions = activePositions(index)
val activePlayerCount = activePositions.size // don't move this line because of removes
// Blinds must act
if (!significantAction.action.type!!.isBlind) {
activePositions.remove(significantAction.position)
}
val significantIndex = significantAction.action.index
for (i in significantIndex + 1 until this.sortedActions.size) {
val ca = this.sortedActions[i]
val type = ca.action.type
if (type != null && !type.isSignificant) { // Calls and folds
activePositions.remove(ca.position)
}
}
if (activePositions.isEmpty()) {
return if (activePlayerCount >= 2 && currentStreet != Street.RIVER) {
currentStreet.next
} else {
Street.SUMMARY
}
}
}
val allPassive = this.sortedActions
.filter { it.street == currentStreet }
.all { it.action.type?.isPassive ?: false }
if (allPassive) {
return if (currentStreet != Street.RIVER) {
currentStreet.next
} else {
Street.SUMMARY
}
}
return null
}
/***
* Adds a [street] header to a [rowRepresentables]
*/
private fun addStreetHeader(rowRepresentables: MutableList<RowRepresentable>, street: Street) {
val firstIndexOfStreet = this.sortedActions.firstOrNull { it.street == street }?.action?.index
?: this.sortedActions.size
val potSize = this.sortedActions.take(firstIndexOfStreet).sumByDouble { it.action.effectiveAmount }
val potString = if (potSize > 0) potSize.formatted() else "" // "" required otherwise random values come up
val headerView = CustomizableRowRepresentable(customViewType = HandRowType.HEADER, resId = street.resId, value = potString)
rowRepresentables.add(headerView)
if (street.totalBoardCards > 0) {
// get board from last street
val lastBoardRow = this.rowRepresentables.lastOrNull { it is StreetCardsRow } as? StreetCardsRow
val cards = lastBoardRow?.cards ?: listOf<Card>()
// create new StreetCardsRow
val boardView = StreetCardsRow(street, cards)
rowRepresentables.add(boardView)
}
}
// Card Centralizer
private val usedCards: List<Card>
get() {
// TODO is my list always stored in the handHistory, or not?
return listOf()
}
private var lastValue: Card.Value? = null
override fun isValueAvailable(value: Card.Value): Boolean {
val usedValues = this.usedCards.filter { it.value == value.value }
return usedValues.size < 4
}
override fun isSuitAvailable(suit: Card.Suit, value: Card.Value?): Boolean {
val usedValues = this.usedCards.filter { it.value == value?.value }
return !usedValues.map { it.suit }.contains(suit)
}
}

@ -12,7 +12,7 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.handhistory.ComputedAction import net.pokeranalytics.android.ui.modules.handhistory.model.ComputedAction
import net.pokeranalytics.android.model.handhistory.Street import net.pokeranalytics.android.model.handhistory.Street
import net.pokeranalytics.android.model.realm.handhistory.formatted import net.pokeranalytics.android.model.realm.handhistory.formatted
import net.pokeranalytics.android.ui.adapter.BindableHolder import net.pokeranalytics.android.ui.adapter.BindableHolder

@ -10,7 +10,6 @@ import kotlinx.android.synthetic.main.fragment_hand_history.*
import kotlinx.android.synthetic.main.fragment_settings.recyclerView import kotlinx.android.synthetic.main.fragment_settings.recyclerView
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.handhistory.ComputedAction
import net.pokeranalytics.android.model.handhistory.HandSetup import net.pokeranalytics.android.model.handhistory.HandSetup
import net.pokeranalytics.android.model.handhistory.Position import net.pokeranalytics.android.model.handhistory.Position
import net.pokeranalytics.android.model.realm.handhistory.Action import net.pokeranalytics.android.model.realm.handhistory.Action
@ -18,6 +17,9 @@ import net.pokeranalytics.android.model.realm.handhistory.Card
import net.pokeranalytics.android.model.realm.handhistory.HandHistory import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.ui.modules.handhistory.model.BuilderListener
import net.pokeranalytics.android.ui.modules.handhistory.model.ComputedAction
import net.pokeranalytics.android.ui.modules.handhistory.model.HHBuilder
import net.pokeranalytics.android.ui.modules.handhistory.views.KeyboardListener import net.pokeranalytics.android.ui.modules.handhistory.views.KeyboardListener
import net.pokeranalytics.android.ui.modules.handhistory.views.PlayerCardsRow import net.pokeranalytics.android.ui.modules.handhistory.views.PlayerCardsRow
import net.pokeranalytics.android.ui.modules.handhistory.views.StreetCardsRow import net.pokeranalytics.android.ui.modules.handhistory.views.StreetCardsRow
@ -27,7 +29,7 @@ import net.pokeranalytics.android.util.extensions.findById
import net.pokeranalytics.android.util.extensions.noGroupingFormatted import net.pokeranalytics.android.util.extensions.noGroupingFormatted
import timber.log.Timber import timber.log.Timber
class HandHistoryFragment : RealmFragment(), RowRepresentableDelegate, KeyboardListener { class HandHistoryFragment : RealmFragment(), RowRepresentableDelegate, KeyboardListener, BuilderListener {
private lateinit var model: HandHistoryViewModel private lateinit var model: HandHistoryViewModel
@ -80,7 +82,9 @@ class HandHistoryFragment : RealmFragment(), RowRepresentableDelegate, KeyboardL
handHistory handHistory
) )
} ?: run { } ?: run {
HHBuilder(HandSetup()) HHBuilder(
HandSetup()
)
} }
this.model.setBuilder(builder) this.model.setBuilder(builder)
@ -238,13 +242,13 @@ class HandHistoryFragment : RealmFragment(), RowRepresentableDelegate, KeyboardL
override fun actionSelected(action: Action.Type) { override fun actionSelected(action: Action.Type) {
Timber.d(">>> action $action selected") Timber.d(">>> action $action selected")
val indexesToRefresh = this.model.actionSelected(action) this.model.actionSelected(action)
indexesToRefresh?.let { indexes -> // indexesToRefresh?.let { indexes ->
indexes.forEach { this.handHistoryAdapter.notifyItemChanged(it) } // indexes.forEach { this.handHistoryAdapter.notifyItemChanged(it) }
} ?: run { // } ?: run {
this.handHistoryAdapter.notifyDataSetChanged() // this.handHistoryAdapter.notifyDataSetChanged()
} // }
this.findNextActionToEdit() this.findNextActionToEdit()
} }
@ -321,4 +325,8 @@ class HandHistoryFragment : RealmFragment(), RowRepresentableDelegate, KeyboardL
} }
} }
override fun rowRepresentablesCountChanged() {
this.handHistoryAdapter.notifyDataSetChanged()
}
} }

@ -7,10 +7,18 @@ import net.pokeranalytics.android.model.handhistory.Position
import net.pokeranalytics.android.model.realm.handhistory.Action import net.pokeranalytics.android.model.realm.handhistory.Action
import net.pokeranalytics.android.model.realm.handhistory.Card import net.pokeranalytics.android.model.realm.handhistory.Card
import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource
import net.pokeranalytics.android.ui.modules.handhistory.model.BuilderListener
import net.pokeranalytics.android.ui.modules.handhistory.model.HHBuilder
import net.pokeranalytics.android.ui.modules.handhistory.views.CardCentralizer import net.pokeranalytics.android.ui.modules.handhistory.views.CardCentralizer
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import timber.log.Timber import timber.log.Timber
enum class HHKeyboard {
ACTION,
AMOUNT,
CARD;
}
class HHSelection(var index: Int, var keyboard: HHKeyboard) class HHSelection(var index: Int, var keyboard: HHKeyboard)
class HandHistoryViewModel : ViewModel(), RowRepresentableDataSource { class HandHistoryViewModel : ViewModel(), RowRepresentableDataSource {
@ -47,7 +55,7 @@ class HandHistoryViewModel : ViewModel(), RowRepresentableDataSource {
// Action // Action
fun actionSelected(action: Action.Type): List<Int>? { fun actionSelected(action: Action.Type) {
return this.builder.selectAction(this.actionIndexForSelection, action) return this.builder.selectAction(this.actionIndexForSelection, action)
} }
@ -138,4 +146,8 @@ class HandHistoryViewModel : ViewModel(), RowRepresentableDataSource {
this.builder.cardSelectionEnded(this.currentSelection.index) this.builder.cardSelectionEnded(this.currentSelection.index)
} }
fun setBuilderListener(builderListener: BuilderListener) {
this.builder.setBuilderListener(builderListener)
}
} }

@ -0,0 +1,500 @@
package net.pokeranalytics.android.ui.modules.handhistory.model
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.handhistory.Position
import net.pokeranalytics.android.model.handhistory.Street
import net.pokeranalytics.android.model.realm.handhistory.Action
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import timber.log.Timber
interface ActionManager {
fun selectAction(index: Int, actionType: Action.Type)
fun getStreetLastSignificantAction(street: Street, index: Int): ComputedAction?
fun getPreviouslyCommittedAmount(index: Int): Double?
fun getStreetNextCalls(index: Int): List<ComputedAction>
fun getPlayerNextStreetActions(index: Int): List<ComputedAction>
fun dropNextActions(index: Int)
}
interface ActionListListener {
fun actionCountChanged()
}
class ActionList(var listener: ActionListListener) : ArrayList<ComputedAction>(), ActionManager {
private var countChanged: Boolean = false
fun load(handHistory: HandHistory) {
this.positions = Position.positionsPerPlayers(handHistory.numberOfPlayers)
var totalPotSize = 0.0
// sorted actions
val computedActions = mutableListOf<ComputedAction>()
val sortedActions = handHistory.actions.sortedBy { it.index }
sortedActions.forEach { action ->
totalPotSize += action.effectiveAmount
val position = this.positions.elementAt(action.position)
val ca =
ComputedAction(
this,
action,
totalPotSize,
action.positionRemainingStack,
position
)
computedActions.add(ca)
}
this.addAll(computedActions)
}
/***
* A LinkedHashSet containing the sorted positions at the table
*/
var positions: LinkedHashSet<Position> = linkedSetOf()
/***
* Selects an action type for the action at the provided [index]
* In case of UNDEFINED_ALLIN, we define which type of allin it is.
* If the user changes the current action,
* for convenience we remove all the following actions to avoid managing complex cases
* Also calculates the player effective amounts in proper cases
*/
override fun selectAction(index: Int, actionType: Action.Type) {
var type = actionType
val computedAction = this[index]
// define allin type
if (type == Action.Type.UNDEFINED_ALLIN) {
val significant = getStreetLastSignificantAction(computedAction.street, index - 1)
type = if (significant != null) {
val betAmount = significant.action.amount
val remainingStack = computedAction.playerRemainingStack
if (remainingStack != null && betAmount != null && remainingStack < betAmount) {
Action.Type.CALL_ALLIN
} else {
Action.Type.RAISE_ALLIN
}
} else {
Action.Type.BET_ALLIN
}
}
Timber.d(">>> Sets $type at index: $index")
computedAction.setType(type) // false
// Automatically sets action for the previous empty actions
val modifiedActions = mutableListOf<ComputedAction>()
getPreviousEmptyActions(index).forEach {
modifiedActions.add(it)
val lastSignificant = getStreetLastSignificantAction(computedAction.street, index - 1)
it.action.type = if (lastSignificant != null) {
Action.Type.FOLD
} else {
Action.Type.CHECK
}
}
this.updateFollowupActions(index)
fireListener()
}
/***
* Fires the listener if the list count has changed
*/
private fun fireListener() {
if (this.countChanged) {
this.listener.actionCountChanged()
this.countChanged = false
}
}
/***
* Returns the list of available user actions at [index]
*/
fun availableActions(index: Int) : Set<Action.Type> {
val computedAction = this[index]
val position = computedAction.position
val lastSignificantAction: ComputedAction? = getStreetLastSignificantAction(computedAction.street, index - 1)
return if (lastSignificantAction == null) {
setOf(Action.Type.FOLD, Action.Type.CHECK, Action.Type.BET, Action.Type.UNDEFINED_ALLIN)
} else {
val remainingStack = getLastPlayerAction(index)?.playerRemainingStack
val actionAmount = lastSignificantAction.action.amount
when (lastSignificantAction.action.type) {
Action.Type.POST_SB, Action.Type.POST_BB, Action.Type.STRADDLE -> {
if (position == lastSignificantAction.position) {
setOf(Action.Type.FOLD, Action.Type.CHECK, Action.Type.BET, Action.Type.UNDEFINED_ALLIN)
} else {
setOf(Action.Type.STRADDLE, Action.Type.FOLD, Action.Type.CALL, Action.Type.BET, Action.Type.UNDEFINED_ALLIN)
}
}
Action.Type.BET, Action.Type.RAISE -> {
if (remainingStack != null && actionAmount != null && remainingStack <= actionAmount) {
setOf(Action.Type.FOLD, Action.Type.CALL_ALLIN)
} else {
setOf(Action.Type.FOLD, Action.Type.CALL, Action.Type.RAISE, Action.Type.UNDEFINED_ALLIN)
}
}
Action.Type.RAISE_ALLIN, Action.Type.BET_ALLIN -> {
if (remainingStack != null && actionAmount != null && remainingStack <= actionAmount) {
setOf(Action.Type.FOLD, Action.Type.CALL_ALLIN)
} else if (activePositions(index).size == 2 && remainingStack != null && actionAmount != null && remainingStack > actionAmount) {
setOf(Action.Type.FOLD, Action.Type.CALL)
} else {
setOf(Action.Type.FOLD, Action.Type.CALL, Action.Type.RAISE, Action.Type.UNDEFINED_ALLIN)
}
}
else -> {
throw PAIllegalStateException("We should not handle this action: ${lastSignificantAction.action.type}")
}
}
}
}
/***
* Returns the list of position still in play before the given [index]
*/
fun activePositions(index: Int, showDown: Boolean = false): MutableList<Position> {
val oustedPositions = this.take(index + 1)
.filter {
if (showDown) {
it.action.type == Action.Type.FOLD
} else {
it.action.type?.isPullOut ?: false
}
}
.map { it.position }
val allPositions = this.positions.clone() as LinkedHashSet<Position>
allPositions.removeAll(oustedPositions)
return allPositions.toMutableList()
}
/***
* Keep the first [n] elements
*/
private fun keepFirst(n: Int) {
val sizeBefore = this.size
val cut = this.take(n)
this.clear()
this.addAll(cut)
val sizeAfter = this.size
if (sizeBefore != sizeAfter) {
this.countChanged = true
}
}
/***
* Returns the first action of the given [street]
*/
private fun firstStreetAction(street: Street): ComputedAction {
return this.first { it.street == street }
}
/***
* Returns the last action index of the street, for the action at [index]
*/
private fun lastIndexOfStreet(index: Int): Int {
val street = this[index].street
return this.last { it.street == street }.action.index
}
/***
* Returns the list of empty actions before the action at the given [index]
*/
private fun getPreviousEmptyActions(index: Int) : List<ComputedAction> {
val street = this[index].street
return this.take(index).filter { it.street == street && it.action.type == null }
}
/***
* Returns the last user action, if any, for the action at the provided [index]
*/
private fun getLastPlayerAction(index: Int): ComputedAction? {
val action = this[index].action
val previousActions = this.take(index)
return previousActions.lastOrNull { it.action.position == action.position }
}
/***
* Returns the last significant player action, if any, for the action at the provided [index]
*/
override fun getStreetLastSignificantAction(street: Street, index: Int): ComputedAction? {
val previousActions = this.take(index + 1).filter { it.street == street }
return previousActions.lastOrNull { it.action.isActionSignificant }
}
/***
* Check if some positions are still required to play,
* and adds new actions if none exists after [index]
*/
private fun addsFollowupActionsIfNecessary(index: Int) {
val street = this[index].street
val refAction = getStreetLastSignificantAction(street, index)
?: this.firstStreetAction(street)
val refIndex = refAction.action.index
val refIndexPosition = refAction.position
val activePositions = activePositions(refIndex)
activePositions.remove(refIndexPosition)
// We want to remove positions that already have an action after [refIndex]
for (i in refIndex + 1 until this.size) {
val ca = this[i]
activePositions.remove(ca.position)
}
// Circularly adds an action for missing positions
val firstPositionAfterCurrent = kotlin.math.max(
0,
activePositions.indexOfFirst { it.ordinal > refIndexPosition.ordinal })
for (i in 0 until activePositions.size) {
val position = activePositions[(firstPositionAfterCurrent + i) % activePositions.size]
this.addNewEmptyAction(position, refAction.street, refAction.totalPotSize, lastRemainingStack(position, index))
}
if (activePositions.isNotEmpty()) {
this.listener.actionCountChanged()
}
}
/***
* Creates a new action at the end of the action stack, and in the HH representation
*/
private fun addNewEmptyAction(position: Position, street: Street, currentPotSize: Double, remainingStack: Double?) {
if (street == Street.SUMMARY) {
throw PAIllegalStateException("Summary cannot have player actions")
}
val action = Action()
action.index = this.size
action.position = position.ordinal
action.street = street
val computedAction =
ComputedAction(
this,
action,
currentPotSize,
remainingStack,
position
)
this.add(computedAction)
this.countChanged = true
}
/***
* Returns the last remaining stack of the player, if available
*/
private fun lastRemainingStack(position: Position, index: Int): Double? {
return this.take(index).lastOrNull { it.position == position }?.playerRemainingStack
}
/***
* Adds, if necessary, new ComputedAction for players that needs to act
* Also adds, if necessary, the Street separators and board selectors
* Returns true if the action list has been modified
*/
private fun updateFollowupActions(index: Int) {
val computedAction = this[index]
val type = computedAction.action.type
addsFollowupActionsIfNecessary(index)
when (type?.isSignificant) {
false -> { // closes the action, pass to next street if necessary
if (type == Action.Type.CALL_ALLIN) { // sometimes we can go from RAISE_ALLIN to CALL_IN, changing the actions required
removeActionsIfNecessary(index)
}
createNextStreetIfNecessary(index)
}
else -> {}
}
}
/***
* Recreates the actions after [index]
*/
private fun removeActionsIfNecessary(index: Int) {
this.keepFirst(index + 1)
addsFollowupActionsIfNecessary(index)
}
/***
* Creates a new street if no player needs to act
* Returns true if a street has been created
*/
private fun createNextStreetIfNecessary(index: Int) {
val nextStreet = isStreetActionClosed(index)
if (nextStreet != null && this.firstOrNull { it.street == nextStreet } == null) {
if (nextStreet == Street.SUMMARY) {
this.listener.actionCountChanged() // force recreation of builder rows
} else {
createStreet(nextStreet)
}
}
}
/***
* This function evaluates whether the street, identified by an action [index],
* has its action closed, meaning no more player are required to play.
* In case of closing, the function returns the next logical Street
*/
fun isStreetActionClosed(index: Int) : Street? {
val currentStreet = this[index].street
getStreetLastSignificantAction(currentStreet, index)?.let { significantAction ->
val activePositions = activePositions(index)
val activePlayerCount = activePositions.size // don't move this line because of removes
// Blinds must act
if (!significantAction.action.type!!.isBlind) {
activePositions.remove(significantAction.position)
}
val significantIndex = significantAction.action.index
for (i in significantIndex + 1 until this.size) {
val ca = this[i]
val type = ca.action.type
if (type != null && !type.isSignificant) { // Calls and folds
activePositions.remove(ca.position)
}
}
if (activePositions.isEmpty()) {
return if (activePlayerCount >= 2 && currentStreet != Street.RIVER) {
currentStreet.next
} else {
Street.SUMMARY
}
}
}
val allPassive = this.filter { it.street == currentStreet }
.all { it.action.type?.isPassive ?: false }
if (allPassive) {
return if (currentStreet != Street.RIVER) {
currentStreet.next
} else {
Street.SUMMARY
}
}
return null
}
/***
* Removes the actions, not the automatically created ones,
* following an action change.
* We want drop all non-auto added rows after the index
*/
override fun dropNextActions(index: Int) {
this.keepFirst(index + 1)
this.updateFollowupActions(index)
fireListener()
}
override fun getPlayerNextStreetActions(index: Int): List<ComputedAction> {
val computedAction = this[index]
return this.drop(index + 1).filter {
it.street == computedAction.street && it.position == computedAction.position
}
}
/***
* Returns the committed amount by the player for the street at the current [index]
*/
override fun getPreviouslyCommittedAmount(index: Int): Double? {
val computedAction = this[index]
val position = computedAction.position
val street = computedAction.action.street
val previousActions = this.take(index)
val previousComputedAction = previousActions.lastOrNull {
it.position == position && it.street == street
}
previousComputedAction?.action?.let { previousAction ->
return when (previousAction.type) {
Action.Type.POST_BB, Action.Type.POST_SB, Action.Type.STRADDLE, Action.Type.BET, Action.Type.RAISE -> previousAction.amount
Action.Type.CALL -> getStreetLastSignificantAction(previousAction.street, previousAction.index)?.action?.amount
else -> null
}
}
return null
}
/***
* Returns all "CALL" ComputedAction between the [index] and the next significant action
*/
override fun getStreetNextCalls(index: Int): List<ComputedAction> {
val streetNextSignificantIndex = getStreetNextSignificantAction(index)?.action?.index
?: this.lastIndexOfStreet(index)
return this.filter {
it.action.index in ((index + 1) until streetNextSignificantIndex)
&& (it.action.type?.isCall ?: false)
}
}
/***
* Returns the next significant player action in the street, if any, for the action at the provided [index]
*/
private fun getStreetNextSignificantAction(index: Int): ComputedAction? {
val street = this[index].street
val nextActions = this.drop(index + 1).filter { it.street == street }
return nextActions.firstOrNull { it.action.isActionSignificant }
}
/***
* Creates a new street:
* - Adds a Street Header
* - Adds a Board if necessary
* - Adds empty actions for the remaining players
*/
private fun createStreet(street: Street) {
val lastComputedAction = this.last()
val totalPotSize = lastComputedAction.totalPotSize
val lastActionIndex = lastComputedAction.action.index
val isShowDown = street == Street.SUMMARY
activePositions(lastActionIndex, isShowDown).sortedBy { it.ordinal }.forEach {
addNewEmptyAction(it, street, totalPotSize, lastRemainingStack(it, lastActionIndex))
}
}
/***
* Returns a list of positions at the provided [index]
*/
fun positionsAtIndex(index: Int): List<Position> {
val currentStreet = this[index].street
val streetActions = this.filter { it.street == currentStreet }
return streetActions.drop(index + 1).map { it.position }
}
}

@ -1,8 +1,9 @@
package net.pokeranalytics.android.model.handhistory package net.pokeranalytics.android.ui.modules.handhistory.model
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.handhistory.Position
import net.pokeranalytics.android.model.handhistory.Street
import net.pokeranalytics.android.model.realm.handhistory.Action import net.pokeranalytics.android.model.realm.handhistory.Action
import net.pokeranalytics.android.ui.modules.handhistory.ActionManager
import net.pokeranalytics.android.ui.modules.handhistory.HHKeyboard import net.pokeranalytics.android.ui.modules.handhistory.HHKeyboard
import net.pokeranalytics.android.ui.modules.handhistory.HandRowType import net.pokeranalytics.android.ui.modules.handhistory.HandRowType
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
@ -21,7 +22,8 @@ class ComputedAction(var manager: ActionManager,
var action: Action, var action: Action,
var totalPotSize: Double = 0.0, var totalPotSize: Double = 0.0,
var playerRemainingStack: Double? = null, var playerRemainingStack: Double? = null,
var position: Position) : HandHistoryRow { var position: Position
) : HandHistoryRow {
/*** /***
* Returns whether the action requires the user to enter an amount for the selected action * Returns whether the action requires the user to enter an amount for the selected action
@ -36,7 +38,7 @@ class ComputedAction(var manager: ActionManager,
} }
} }
fun setType(type: Action.Type): Boolean { fun setType(type: Action.Type) {
val typeChange = (this.action.type != null) val typeChange = (this.action.type != null)
@ -63,10 +65,9 @@ class ComputedAction(var manager: ActionManager,
} }
if (typeChange) { if (typeChange) {
return this.manager.dropNextActions(this.action.index) this.manager.dropNextActions(this.action.index)
} }
return false
} }
/*** /***

@ -0,0 +1,390 @@
package net.pokeranalytics.android.ui.modules.handhistory.model
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.handhistory.HandSetup
import net.pokeranalytics.android.model.handhistory.Position
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.ui.modules.handhistory.HHSelection
import net.pokeranalytics.android.ui.modules.handhistory.HandRowType
import net.pokeranalytics.android.ui.modules.handhistory.views.CardCentralizer
import net.pokeranalytics.android.ui.modules.handhistory.views.CardsRow
import net.pokeranalytics.android.ui.modules.handhistory.views.PlayerCardsRow
import net.pokeranalytics.android.ui.modules.handhistory.views.StreetCardsRow
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable
import net.pokeranalytics.android.util.extensions.formatted
import timber.log.Timber
interface BuilderListener {
fun rowRepresentablesCountChanged()
}
class HHBuilder : CardCentralizer, ActionListListener {
/***
* The list of row representables, generated from the hand history actions
*/
var rowRepresentables = mutableListOf<RowRepresentable>()
private set
/***
* The hand history
*/
private var handHistory: HandHistory
set(value) {
field = value
setNumberOfPlayers(value.numberOfPlayers)
load()
}
/***
* All actions sorted by index
*/
private var sortedActions: ActionList = ActionList(this)
/***
* The board cards sorted by position
*/
// private lateinit var boardManager: BoardManager
/***
* The maximum number of cards in a player's hand
*/
private var playerHandMaxCards: Int? = null
set(value) {
field = value
this.rowRepresentables.filterIsInstance<PlayerCardsRow>().forEach {
it.maxCards = value
}
}
/***
* Creates a builder using a [handSetup]
* Also creates a new Hand History and configures it according to the [handSetup]
*/
constructor(handSetup: HandSetup) {
val handHistory = HandHistory()
handHistory.configure(handSetup)
this.playerHandMaxCards = handSetup.game?.playerHandMaxCards
this.handHistory = handHistory
}
/***
* Creates a builder using the parameter [handHistory]
*/
constructor(handHistory: HandHistory) {
this.handHistory = handHistory
}
private var listener: BuilderListener? = null
fun setBuilderListener(builderListener: BuilderListener) {
this.listener = builderListener
}
/***
* Returns the action
*/
private fun actionForIndex(index: Int) : ComputedAction {
return this.sortedActions[index]
}
/***
* Fills the [sortedActions] variable with the proper actions
* Pre-computes the potsizes for the video export
*/
private fun load() {
// this.boardManager = BoardManager(this.handHistory.board, this)
this.sortedActions.load(this.handHistory)
this.createRowRepresentation()
}
/***
* Sets the number of players playing the hand
* Defines the appropriate positions for this player count
*/
fun setNumberOfPlayers(playerCount: Int) {
this.handHistory.numberOfPlayers = playerCount
this.sortedActions.positions = Position.positionsPerPlayers(playerCount)
}
fun availableActions(actionIndexForSelection: Int): Set<Action.Type> {
return this.sortedActions.availableActions(actionIndexForSelection)
}
fun selectAction(index: Int, actionType: Action.Type) {
this.sortedActions.selectAction(index, actionType)
}
/***
* Sets the amount for the action at the provided [index]
*/
fun setAmount(index: Int, amount: Double) {
val computedAction = this.actionForIndex(index)
Timber.d(">>> Sets $amount at index: $index, for action ${computedAction.action.type}")
computedAction.setAmount(amount)
}
// /***
// * Creates a new street:
// * - Adds a Street Header
// * - Adds a Board if necessary
// * - Adds empty actions for the remaining players
// */
// private fun createStreet(street: Street) {
//
// val lastComputedAction = this.sortedActions.last()
// val totalPotSize = lastComputedAction.totalPotSize
//
// addStreetHeader(this.rowRepresentables, street)
//
// val lastActionIndex = lastComputedAction.action.index
// val isShowDown = street == Street.SUMMARY
// activePositions(lastActionIndex, isShowDown).sortedBy { it.ordinal }.forEach {
//
// when (street) {
// Street.SUMMARY -> {
// this.rowRepresentables.add(PlayerCardsRow(it))
// }
// else -> {
// addNewEmptyAction(it, street, totalPotSize, lastRemainingStack(it, lastActionIndex))
// }
// }
//
// }
// }
/***
* Returns a list of positions at the provided [index]
*/
fun positionsAtIndex(index: Int): List<Position> {
return this.sortedActions.positionsAtIndex(index)
}
/***
* Returns the index of a ComputedAction given its [rowRepresentableIndex]
*/
fun indexOfComputedAction(rowRepresentableIndex: Int) : Int {
val computedAction = this.rowRepresentables[rowRepresentableIndex] as ComputedAction
return computedAction.action.index
}
/***
* Finds the index of the first incomplete action, if existing
* If the same selection is the same than current, pass to the next row
*/
fun findIndexForEdition(startIndex: Int): HHSelection? {
this.rowRepresentables.forEachIndexed { index, rowRepresentable ->
if (index >= startIndex && rowRepresentable is HandHistoryRow) {
val foundKeyboard = rowRepresentable.keyboardForCompletion()
if (foundKeyboard != null) {
return HHSelection(
index,
foundKeyboard
)
}
}
}
return null
}
/***
* Returns the index, strictly superior to [actionIndexForSelection],
* of the next action of the given [position]
*/
fun nextActionIndex(actionIndexForSelection: Int, position: Position): Int {
var i = actionIndexForSelection + 1
while (true) {
val computedAction = this.actionForIndex(i)
if (computedAction.position == position) {
return this.rowRepresentables.indexOf(computedAction)
}
i++
if (i > this.sortedActions.size) throw PAIllegalStateException("algo sucks")
}
}
/***
* Adds a card with the selected [value]
*/
fun cardValueSelected(value: Card.Value, currentSelection: HHSelection) {
this.lastValue = value
val row = this.rowRepresentables[currentSelection.index] as CardsRow
row.valueSelected(value)
}
/***
* Either affect the [suit] to the current card,
* or create a Card with an empty value and the [suit]
*/
fun cardSuitSelected(suit: Card.Suit, currentSelection: HHSelection) {
val row = this.rowRepresentables[currentSelection.index] as CardsRow
row.suitSelected(suit)
// TODO do we want to store the information right now ?
}
/***
* Deletes all the card of the selected street
*/
fun clearCards(currentSelection: HHSelection) {
this.lastValue = null
val row = this.rowRepresentables[currentSelection.index] as CardsRow
row.clear()
}
/***
* Delete the last property of the last Card of the selected street
* We don't want empty Card (value + suit),
* so we delete the whole card if both information are null
*/
fun deleteLastCardProperty(currentSelection: HHSelection) {
val row = this.rowRepresentables[currentSelection.index] as CardsRow
row.deleteLastCardProperty()
}
/***
* When a card selection is ended,
* sets the playerHandMaxCards with the number of cards set to the player
*/
fun cardSelectionEnded(index: Int) {
val cardsRow = this.rowRepresentables[index] as CardsRow
if (cardsRow is PlayerCardsRow) {
this.playerHandMaxCards = cardsRow.cards.size
}
}
// BoardChangedListener
/***
* Called when the board has changed
*/
fun boardChanged(cards: List<Card>) {
this.rowRepresentables.filterIsInstance<StreetCardsRow>().forEach {
// it.cards = cards
}
}
/***
* Saves the current hand state in the database
*/
private fun save() {
}
private fun createRowRepresentation() {
val rows: MutableList<RowRepresentable> = mutableListOf()
rows.add(CustomizableRowRepresentable(customViewType = HandRowType.HEADER, resId = R.string.settings, value = ""))
Street.values().forEach { street ->
val actions = this.sortedActions.filter { it.street == street }
when (street) {
Street.SUMMARY -> {
val lastActionIndex = this.sortedActions.size - 1
if (this.sortedActions.activePositions(lastActionIndex).size < 2 || this.sortedActions.isStreetActionClosed(lastActionIndex) == Street.SUMMARY) {
addStreetHeader(rows, street)
val positions = this.sortedActions.activePositions(lastActionIndex, true)
positions.forEach {
val positionIndex = this.sortedActions.positions.indexOf(it)
val playerCardsRow = PlayerCardsRow(it, this.handHistory.cardsForPosition(positionIndex), this.playerHandMaxCards)
rows.add(playerCardsRow)
}
}
}
else -> {
if (actions.isNotEmpty()) {
addStreetHeader(rows, street)
rows.addAll(actions)
}
}
}
}
this.rowRepresentables = rows
this.listener?.rowRepresentablesCountChanged()
}
/***
* Adds a [street] header to a [rowRepresentables]
*/
private fun addStreetHeader(rowRepresentables: MutableList<RowRepresentable>, street: Street) {
val firstIndexOfStreet = this.sortedActions.firstOrNull { it.street == street }?.action?.index
?: this.sortedActions.size
val potSize = this.sortedActions.take(firstIndexOfStreet).sumByDouble { it.action.effectiveAmount }
val potString = if (potSize > 0) potSize.formatted() else "" // "" required otherwise random values come up
val headerView = CustomizableRowRepresentable(customViewType = HandRowType.HEADER, resId = street.resId, value = potString)
rowRepresentables.add(headerView)
if (street.totalBoardCards > 0) {
// get board from last street
val lastBoardRow = this.rowRepresentables.lastOrNull { it is StreetCardsRow } as? StreetCardsRow
val cards = lastBoardRow?.cards ?: listOf<Card>()
// create new StreetCardsRow
val boardView = StreetCardsRow(street, cards)
rowRepresentables.add(boardView)
}
}
// Card Centralizer
/***
* The list of card already used, for the board or players
*/
private val usedCards: List<Card>
get() {
// TODO is my list always stored in the handHistory, or not?
return listOf()
}
/***
* The value of the last chosen card
*/
private var lastValue: Card.Value? = null
/***
* Returns true if the card [value] is available
*/
override fun isValueAvailable(value: Card.Value): Boolean {
val usedValues = this.usedCards.filter { it.value == value.value }
return usedValues.size < 4
}
/***
* Returns true if the [suit] is available for the given [value]
*/
override fun isSuitAvailable(suit: Card.Suit, value: Card.Value?): Boolean {
val usedValues = this.usedCards.filter { it.value == value?.value }
return !usedValues.map { it.suit }.contains(suit)
}
// Action List Listener
override fun actionCountChanged() {
this.createRowRepresentation()
}
}

@ -1,14 +1,15 @@
package net.pokeranalytics.android.ui.modules.handhistory.views package net.pokeranalytics.android.ui.modules.handhistory.views
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.handhistory.HandHistoryRow
import net.pokeranalytics.android.model.handhistory.Position import net.pokeranalytics.android.model.handhistory.Position
import net.pokeranalytics.android.model.handhistory.Street import net.pokeranalytics.android.model.handhistory.Street
import net.pokeranalytics.android.model.realm.handhistory.Card import net.pokeranalytics.android.model.realm.handhistory.Card
import net.pokeranalytics.android.ui.modules.handhistory.HHKeyboard import net.pokeranalytics.android.ui.modules.handhistory.HHKeyboard
import net.pokeranalytics.android.ui.modules.handhistory.HandRowType import net.pokeranalytics.android.ui.modules.handhistory.HandRowType
import net.pokeranalytics.android.ui.modules.handhistory.model.HandHistoryRow
abstract class CardsRow(cards: List<Card>) : HandHistoryRow { abstract class CardsRow(cards: List<Card>) :
HandHistoryRow {
var cards: MutableList<Card> = cards.toMutableList() var cards: MutableList<Card> = cards.toMutableList()
private set private set

Loading…
Cancel
Save