commit
1ce9ee3ee3
@ -1,80 +0,0 @@ |
||||
package net.pokeranalytics.android.api |
||||
|
||||
import android.content.Context |
||||
import net.pokeranalytics.android.BuildConfig |
||||
import net.pokeranalytics.android.R |
||||
import net.pokeranalytics.android.model.retrofit.CurrencyConverterValue |
||||
import net.pokeranalytics.android.util.URL |
||||
import okhttp3.Interceptor |
||||
import okhttp3.OkHttpClient |
||||
import okhttp3.logging.HttpLoggingInterceptor |
||||
import retrofit2.Call |
||||
import retrofit2.Retrofit |
||||
import retrofit2.converter.gson.GsonConverterFactory |
||||
import retrofit2.http.GET |
||||
import retrofit2.http.Query |
||||
import java.util.concurrent.TimeUnit |
||||
|
||||
/** |
||||
* CurrencyCode Converter API |
||||
*/ |
||||
interface CurrencyConverterApi { |
||||
|
||||
companion object { |
||||
|
||||
private var currencyConverterApi: CurrencyConverterApi? = null |
||||
|
||||
fun getApi(context: Context): CurrencyConverterApi? { |
||||
|
||||
if (currencyConverterApi == null) { |
||||
|
||||
var serviceEndpoint = URL.API_CURRENCY_CONVERTER |
||||
|
||||
val httpClient = OkHttpClient.Builder() |
||||
|
||||
// Logging interceptor |
||||
if (BuildConfig.DEBUG) { |
||||
val interceptor = HttpLoggingInterceptor() |
||||
interceptor.level = HttpLoggingInterceptor.Level.BASIC |
||||
httpClient.addInterceptor(interceptor) |
||||
} |
||||
|
||||
// Add headers |
||||
val interceptor = Interceptor { chain -> |
||||
val original = chain.request() |
||||
val originalHttpUrl = original.url() |
||||
|
||||
val url = originalHttpUrl.newBuilder() |
||||
.addQueryParameter("apiKey", context.getString(R.string.currency_converter_api)) |
||||
.build() |
||||
|
||||
val requestBuilder = original.newBuilder() |
||||
.url(url) |
||||
|
||||
chain.proceed(requestBuilder.build()) |
||||
} |
||||
httpClient.addInterceptor(interceptor) |
||||
|
||||
val client = httpClient |
||||
.readTimeout(60, TimeUnit.SECONDS) |
||||
.connectTimeout(60, TimeUnit.SECONDS) |
||||
.build() |
||||
|
||||
val retrofit = Retrofit.Builder() |
||||
.addConverterFactory(GsonConverterFactory.create()) |
||||
.baseUrl(serviceEndpoint.value) |
||||
.client(client) |
||||
.build() |
||||
|
||||
currencyConverterApi = retrofit.create(CurrencyConverterApi::class.java) |
||||
} |
||||
|
||||
return currencyConverterApi |
||||
} |
||||
|
||||
} |
||||
|
||||
@GET("convert") |
||||
fun convert(@Query("q") currencies: String, @Query("compact") compact: String = "y"): Call<Map<String, CurrencyConverterValue>> |
||||
|
||||
} |
||||
@ -0,0 +1,47 @@ |
||||
package net.pokeranalytics.android.api |
||||
|
||||
import android.content.Context |
||||
import com.android.volley.Request |
||||
import com.android.volley.Response |
||||
import com.android.volley.toolbox.StringRequest |
||||
import com.android.volley.toolbox.Volley |
||||
import kotlinx.serialization.json.Json |
||||
import kotlinx.serialization.json.JsonConfiguration |
||||
import timber.log.Timber |
||||
|
||||
class FreeConverterApi { |
||||
|
||||
companion object { |
||||
|
||||
fun currencyRate(pair: String, context: Context, callback: (Double) -> (Unit)) { |
||||
|
||||
val queue = Volley.newRequestQueue(context) |
||||
val url = "https://free.currconv.com/api/v7/convert?q=${pair}&compact=ultra&apiKey=5ba8d38995282fe8b1c8" |
||||
|
||||
// https://free.currconv.com/api/v7/convert?q=GBP_USD&compact=ultra&apiKey=5ba8d38995282fe8b1c8 |
||||
// { "USD_PHP": 44.1105, "PHP_USD": 0.0227 } |
||||
|
||||
val stringRequest = StringRequest( |
||||
Request.Method.GET, url, |
||||
Response.Listener { response -> |
||||
|
||||
val json = Json(JsonConfiguration.Stable) |
||||
val f = json.parseJson(response) |
||||
f.jsonObject[pair]?.primitive?.double?.let { rate -> |
||||
callback(rate) |
||||
} ?: run { |
||||
Timber.d("no rate: $response") |
||||
} |
||||
|
||||
}, |
||||
Response.ErrorListener { |
||||
Timber.d("Api call failed: ${it.message}") |
||||
}) |
||||
|
||||
queue.add(stringRequest) |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,170 @@ |
||||
package net.pokeranalytics.android.calculus.optimalduration |
||||
|
||||
import io.realm.Realm |
||||
import net.pokeranalytics.android.calculus.Calculator |
||||
import net.pokeranalytics.android.calculus.Stat |
||||
import net.pokeranalytics.android.model.filter.Query |
||||
import net.pokeranalytics.android.model.filter.QueryCondition |
||||
import net.pokeranalytics.android.model.realm.Session |
||||
import org.apache.commons.math3.fitting.PolynomialCurveFitter |
||||
import org.apache.commons.math3.fitting.WeightedObservedPoints |
||||
import timber.log.Timber |
||||
import java.util.* |
||||
import kotlin.math.pow |
||||
import kotlin.math.round |
||||
|
||||
/*** |
||||
* This class attempts to find the optimal game duration, |
||||
* meaning the duration where the player will maximize its results, based on his history. |
||||
* The results stands for cash game, and are separated between live and online. |
||||
* Various reasons can prevent the algorithm to find a duration, see below. |
||||
*/ |
||||
class CashGameOptimalDurationCalculator { |
||||
|
||||
companion object { |
||||
|
||||
private const val bucket = 60 * 60 * 1000L // the duration of bucket |
||||
private const val bucketInterval = 4 // number of duration tests inside the bucket to find the best duration |
||||
private const val minimumValidityCount = 10 // the number of sessions inside a bucket to start having a reasonable average |
||||
private const val intervalValidity = 3 // the minimum number of unit between the shortest & longest valid buckets |
||||
private const val polynomialDegree = 7 // the degree of the computed polynomial |
||||
|
||||
/*** |
||||
* Starts the calculation |
||||
* [isLive] is a boolean to indicate if we're looking at live or online games |
||||
* return a duration or null if it could not be computed |
||||
*/ |
||||
fun start(isLive: Boolean): Double? { |
||||
|
||||
val realm = Realm.getDefaultInstance() |
||||
|
||||
val query = Query().add(QueryCondition.IsCash) // cash game |
||||
query.add(if (isLive) { QueryCondition.IsLive } else { QueryCondition.IsOnline }) // live / online |
||||
query.add(QueryCondition.EndDateNotNull) // ended |
||||
query.add(QueryCondition.BigBlindNotNull) // has BB value |
||||
|
||||
val sessions = query.queryWith(realm.where(Session::class.java)).findAll() |
||||
val sessionsByDuration = sessions.groupBy { |
||||
val dur = round((it.netDuration / bucket).toDouble()) * bucket |
||||
Timber.d("Stop notif > key: $dur") |
||||
dur |
||||
} |
||||
|
||||
// define validity interval |
||||
var start: Double? = null |
||||
var end: Double? = null |
||||
var validBuckets = 0 |
||||
|
||||
val hkeys = sessionsByDuration.keys.map { it / 3600 / 1000.0 }.sorted() |
||||
Timber.d("Stop notif > keys: $hkeys ") |
||||
for (key in sessionsByDuration.keys.sorted()) { |
||||
val sessionCount = sessionsByDuration[key]?.size ?: 0 |
||||
if (start == null && sessionCount >= minimumValidityCount) { |
||||
start = key |
||||
} |
||||
if (sessionCount >= minimumValidityCount) { |
||||
end = key |
||||
validBuckets++ |
||||
} |
||||
} |
||||
Timber.d("Stop notif > validBuckets: $validBuckets ") |
||||
if (!(start != null && end != null && (end - start) >= intervalValidity)) { |
||||
Timber.d("Stop notif > invalid setup: $start / $end ") |
||||
return null |
||||
} |
||||
|
||||
// define if we have enough sessions |
||||
if (sessions.size < 50) { |
||||
Timber.d("Stop notif > not enough sessions: ${sessions.size} ") |
||||
return null |
||||
} |
||||
|
||||
val options = Calculator.Options() |
||||
options.query = query |
||||
val report = Calculator.computeStats(realm, options) |
||||
val stdBB = report.results.firstOrNull()?.computedStat(Stat.STANDARD_DEVIATION_BB)?.value |
||||
|
||||
val p = polynomialRegression(sessions, stdBB) |
||||
|
||||
var bestAverage = 0.0 |
||||
var bestHourlyRate = 0.0 |
||||
var bestDuration = 0.0 |
||||
var maxDuration = 0.0 |
||||
|
||||
val keys = sessionsByDuration.keys.filter { it >= start && it <= end }.sorted() |
||||
|
||||
for (key in keys) { |
||||
|
||||
val sessionCount = sessionsByDuration[key]?.size ?: 0 |
||||
|
||||
if (sessionCount < minimumValidityCount / 2) continue // if too few sessions we don't consider the duration valid |
||||
|
||||
for (i in 0 until bucketInterval) { |
||||
|
||||
val duration = key + i * bucket / bucketInterval |
||||
|
||||
val averageResult = getBB(duration, p) |
||||
val hourly = averageResult / duration |
||||
if (averageResult > bestAverage && hourly > 2 / 3 * bestHourlyRate) { |
||||
bestAverage = averageResult |
||||
bestDuration = duration |
||||
} |
||||
|
||||
if (duration > 0 && hourly > bestHourlyRate) { |
||||
bestHourlyRate = hourly |
||||
} |
||||
if (duration > maxDuration){ |
||||
maxDuration = duration |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
if (bestDuration > 0.0) { |
||||
return bestDuration |
||||
} |
||||
|
||||
Timber.d("Stop notif > not found, best duration: $bestDuration") |
||||
realm.close() |
||||
return null |
||||
} |
||||
|
||||
private fun getBB(netDuration: Double, polynomial: DoubleArray): Double { |
||||
var y = 0.0 |
||||
for (i in polynomial.indices) { |
||||
y += polynomial[i] * netDuration.pow(i) |
||||
} |
||||
return y |
||||
} |
||||
|
||||
private fun polynomialRegression(sessions: List<Session>, bbStandardDeviation: Double?): DoubleArray { |
||||
|
||||
val stdBB = bbStandardDeviation ?: Double.MAX_VALUE |
||||
|
||||
val points = WeightedObservedPoints() |
||||
val now = Date().time |
||||
|
||||
sessions.forEach { |
||||
var weight = 5.0 |
||||
|
||||
val endTime = it.endDate?.time ?: 0L |
||||
|
||||
val age = now - endTime |
||||
if (age > 2 * 365 * 24 * 3600 * 1000L) { // if more than 2 years loses 1 point |
||||
weight -= 1.0 |
||||
} |
||||
if (it.bbNet > 2 * stdBB) { // if very big result loses 3 points |
||||
weight -= 3.0 |
||||
} |
||||
|
||||
points.add(weight, it.netDuration.toDouble(), it.bbNet) |
||||
|
||||
} |
||||
|
||||
// polynomial of 7 degree, same as iOS |
||||
return PolynomialCurveFitter.create(polynomialDegree).fit(points.toList()) |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,80 @@ |
||||
package net.pokeranalytics.android.model.handhistory |
||||
|
||||
import net.pokeranalytics.android.exceptions.PAIllegalStateException |
||||
import net.pokeranalytics.android.model.realm.handhistory.Card |
||||
|
||||
/*** |
||||
* An interface used for board changes notifications |
||||
*/ |
||||
interface BoardChangedListener { |
||||
fun boardChanged() |
||||
} |
||||
|
||||
/*** |
||||
* The BoardManager purpose is to manage the cards from a hand history board |
||||
* and notify its listener when a change occurs |
||||
*/ |
||||
class BoardManager(cards: List<Card>, var listener: BoardChangedListener) { |
||||
|
||||
/*** |
||||
* The sorted list of cards |
||||
*/ |
||||
private var sortedBoardCards: MutableList<Card> = mutableListOf() |
||||
|
||||
/*** |
||||
* All cards |
||||
*/ |
||||
val allCards: List<Card> |
||||
get() { |
||||
return this.sortedBoardCards |
||||
} |
||||
|
||||
init { |
||||
this.sortedBoardCards = cards.sortedBy { it.index }.toMutableList() |
||||
} |
||||
|
||||
/*** |
||||
* Adds a card to the board, notifies the listener |
||||
*/ |
||||
fun add(card: Card) { |
||||
|
||||
this.sortedBoardCards.lastOrNull()?.let { |
||||
if (it.suit == null) { |
||||
it.suit = Card.Suit.UNDEFINED |
||||
} |
||||
} |
||||
|
||||
if (this.sortedBoardCards.size == 5) { |
||||
throw PAIllegalStateException("Can't add anymore cards") |
||||
} |
||||
|
||||
card.index = this.sortedBoardCards.size |
||||
|
||||
this.sortedBoardCards.add(card) |
||||
this.listener.boardChanged() |
||||
} |
||||
|
||||
/*** |
||||
* Clears the street's cards, notifies the listener |
||||
*/ |
||||
fun clearStreet(street: Street) { |
||||
this.sortedBoardCards.removeAll { it.street == street } |
||||
this.listener.boardChanged() |
||||
} |
||||
|
||||
/*** |
||||
* Returns the last card of a given [street] |
||||
*/ |
||||
fun lastCard(street: Street) : Card? { |
||||
return this.sortedBoardCards.lastOrNull { it.street == street } |
||||
} |
||||
|
||||
/*** |
||||
* Remove the given [card], notifies the listener |
||||
*/ |
||||
fun remove(card: Card) { |
||||
this.sortedBoardCards.remove(card) |
||||
this.listener.boardChanged() |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,104 @@ |
||||
package net.pokeranalytics.android.model.handhistory |
||||
|
||||
import io.realm.Realm |
||||
import net.pokeranalytics.android.model.realm.Game |
||||
import net.pokeranalytics.android.model.realm.Session |
||||
import net.pokeranalytics.android.model.realm.handhistory.HandHistory |
||||
import net.pokeranalytics.android.util.extensions.findById |
||||
import timber.log.Timber |
||||
import java.util.* |
||||
|
||||
class HandSetup { |
||||
|
||||
companion object { |
||||
|
||||
/*** |
||||
* Returns a HandSetup instance using a [configurationId], the id of a Realm object, |
||||
* Session or HandHistory, used to later configure the HandHistory |
||||
*/ |
||||
fun from(configurationId: String?, attached: Boolean, realm: Realm): HandSetup { |
||||
|
||||
return if (configurationId != null) { |
||||
val handSetup = HandSetup() |
||||
val hh = realm.findById(HandHistory::class.java, configurationId) |
||||
if (hh != null) { |
||||
handSetup.configure(hh) |
||||
} |
||||
val session = realm.findById(Session::class.java, configurationId) |
||||
if (session != null) { |
||||
handSetup.configure(session, attached) |
||||
} |
||||
handSetup |
||||
} else { |
||||
HandSetup() |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
private fun configure(handHistory: HandHistory) { |
||||
this.smallBlind = handHistory.smallBlind |
||||
this.bigBlind = handHistory.bigBlind |
||||
this.bigBlindAnte = handHistory.bigBlindAnte |
||||
this.ante = handHistory.ante |
||||
this.tableSize = handHistory.numberOfPlayers |
||||
} |
||||
|
||||
/*** |
||||
* Configures the Hand Setup with a [session] |
||||
* [attached] denotes if the HandHistory must be directly linked to the session |
||||
*/ |
||||
private fun configure(session: Session, attached: Boolean) { |
||||
if (attached) { |
||||
this.session = session |
||||
} |
||||
if (session.endDate == null) { |
||||
this.game = session.game // we don't want to force the max number of cards if unsure |
||||
} |
||||
this.type = session.sessionType |
||||
this.smallBlind = session.cgSmallBlind |
||||
this.bigBlind = session.cgBigBlind |
||||
this.tableSize = session.tableSize |
||||
} |
||||
|
||||
var type: Session.Type? = null |
||||
|
||||
var smallBlind: Double? = null |
||||
|
||||
var bigBlind: Double? = null |
||||
|
||||
var ante: Double? = null |
||||
|
||||
var tableSize: Int? = null |
||||
|
||||
var bigBlindAnte: Boolean = false |
||||
|
||||
var game: Game? = null |
||||
|
||||
var session: Session? = null |
||||
|
||||
var straddlePositions: MutableList<Position> = mutableListOf() |
||||
private set |
||||
|
||||
fun clearStraddles() { |
||||
this.straddlePositions.clear() |
||||
} |
||||
|
||||
/*** |
||||
* This method sorts the straddle positions in their natural order |
||||
* If the straddle position contains the button, we're usually in a Mississipi straddle, |
||||
* meaning the BUT straddles, then CO, then HJ... |
||||
* Except if it goes to UTG, in which case we don't know if we're in standard straddle, or Mississipi |
||||
* We use the first straddled position to sort out this case |
||||
*/ |
||||
fun setStraddlePositions(firstStraddlePosition: Position, positions: LinkedHashSet<Position>) { |
||||
var sortedPosition = positions.sortedBy { it.ordinal } |
||||
if (positions.contains(Position.BUT) && firstStraddlePosition != Position.UTG) { |
||||
sortedPosition = sortedPosition.reversed() |
||||
} |
||||
Timber.d("sortedPosition = $sortedPosition") |
||||
this.straddlePositions = sortedPosition.toMutableList() |
||||
Timber.d("this.straddlePositions = ${this.straddlePositions}") |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,53 @@ |
||||
package net.pokeranalytics.android.model.handhistory |
||||
|
||||
import android.content.Context |
||||
import net.pokeranalytics.android.exceptions.PAIllegalStateException |
||||
import net.pokeranalytics.android.ui.view.RowRepresentable |
||||
import java.util.* |
||||
|
||||
enum class Position(var value: String) : RowRepresentable { |
||||
SB("SB"), |
||||
BB("BB"), |
||||
UTG("UTG"), |
||||
UTG1("UTG+1"), |
||||
UTG2("UTG+2"), |
||||
UTG3("UTG+3"), |
||||
MP("MP"), |
||||
HJ("HJ"), |
||||
CO("CO"), |
||||
BUT("BUT"); |
||||
|
||||
companion object { |
||||
|
||||
fun positionsPerPlayers(playerCount: Int) : LinkedHashSet<Position> { |
||||
return when(playerCount) { |
||||
2 -> linkedSetOf(SB, BB) |
||||
3 -> linkedSetOf(SB, BB, BUT) |
||||
4 -> linkedSetOf(SB, BB, UTG, BUT) |
||||
5 -> linkedSetOf(SB, BB, UTG, CO, BUT) |
||||
6 -> linkedSetOf(SB, BB, UTG, HJ, CO, BUT) |
||||
7 -> linkedSetOf(SB, BB, UTG, MP, HJ, CO, BUT) |
||||
8 -> linkedSetOf(SB, BB, UTG, UTG1, MP, HJ, CO, BUT) |
||||
9 -> linkedSetOf(SB, BB, UTG, UTG1, UTG2, MP, HJ, CO, BUT) |
||||
10 -> linkedSetOf(SB, BB, UTG, UTG1, UTG2, UTG3, MP, HJ, CO, BUT) |
||||
else -> throw PAIllegalStateException("Unmanaged number of players") |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
val shortValue: String |
||||
get() { |
||||
return when (this) { |
||||
UTG1 -> "+1" |
||||
UTG2 -> "+2" |
||||
UTG3 -> "+3" |
||||
else -> this.value |
||||
} |
||||
} |
||||
|
||||
override fun getDisplayName(context: Context): String { |
||||
return this.value |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,50 @@ |
||||
package net.pokeranalytics.android.model.handhistory |
||||
|
||||
import net.pokeranalytics.android.R |
||||
import net.pokeranalytics.android.ui.modules.handhistory.replayer.HandStep |
||||
|
||||
enum class Street : HandStep { |
||||
PREFLOP, |
||||
FLOP, |
||||
TURN, |
||||
RIVER, |
||||
SUMMARY; |
||||
|
||||
override val street: Street = this |
||||
|
||||
val totalBoardCards: Int |
||||
get() { |
||||
return when (this) { |
||||
PREFLOP -> 0 |
||||
FLOP -> 3 |
||||
TURN -> 4 |
||||
RIVER, SUMMARY -> 5 |
||||
} |
||||
} |
||||
|
||||
val resId: Int |
||||
get() { |
||||
return when (this) { |
||||
PREFLOP -> R.string.street_preflop |
||||
FLOP -> R.string.street_flop |
||||
TURN -> R.string.street_turn |
||||
RIVER -> R.string.street_river |
||||
SUMMARY -> R.string.summary |
||||
} |
||||
} |
||||
|
||||
val next: Street |
||||
get() { |
||||
return values()[this.ordinal + 1] |
||||
} |
||||
|
||||
val previous: Street? |
||||
get() { |
||||
return when (this) { |
||||
PREFLOP -> null |
||||
else -> values()[this.ordinal - 1] |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
@ -1,16 +0,0 @@ |
||||
package net.pokeranalytics.android.model.realm |
||||
|
||||
import io.realm.RealmObject |
||||
import io.realm.annotations.PrimaryKey |
||||
import java.util.* |
||||
|
||||
|
||||
open class HandHistory : RealmObject() { |
||||
|
||||
@PrimaryKey |
||||
var id = UUID.randomUUID().toString() |
||||
|
||||
// the date of the hand history |
||||
var date: Date = Date() |
||||
|
||||
} |
||||
@ -0,0 +1,252 @@ |
||||
package net.pokeranalytics.android.model.realm.handhistory |
||||
|
||||
import io.realm.RealmObject |
||||
import net.pokeranalytics.android.R |
||||
import net.pokeranalytics.android.exceptions.PAIllegalStateException |
||||
import net.pokeranalytics.android.model.handhistory.Position |
||||
import net.pokeranalytics.android.model.handhistory.Street |
||||
import net.pokeranalytics.android.ui.modules.handhistory.model.ActionReadRow |
||||
import net.pokeranalytics.android.ui.view.RowRepresentable |
||||
import net.pokeranalytics.android.ui.view.RowViewType |
||||
import net.pokeranalytics.android.util.extensions.formatted |
||||
|
||||
|
||||
/*** |
||||
* An extension to transform a list of ComputedAction into |
||||
* a more compact and read-friendly list of ActionReadRow |
||||
*/ |
||||
fun List<Action>.compact(positions: LinkedHashSet<Position>, heroIndex: Int?): List<ActionReadRow> { |
||||
val rows = mutableListOf<ActionReadRow>() |
||||
this.forEach { |
||||
if (it.type == Action.Type.FOLD && rows.lastOrNull()?.action == Action.Type.FOLD) { |
||||
rows.lastOrNull()?.positions?.add(positions.elementAt(it.position)) |
||||
} else { |
||||
rows.add(it.toReadRow(positions, heroIndex, null)) // TODO stack. The method is used for text export only atm |
||||
} |
||||
} |
||||
return rows |
||||
} |
||||
|
||||
fun Action.toReadRow(positions: LinkedHashSet<Position>, heroIndex: Int?, stack: Double?): ActionReadRow { |
||||
val pos = positions.elementAt(this.position) |
||||
val isHero = (heroIndex == this.position) |
||||
|
||||
var amount = this.amount |
||||
if (this.type?.isCall == true) { |
||||
amount = this.effectiveAmount |
||||
} |
||||
|
||||
return ActionReadRow(mutableListOf(pos), this.position, this.type, amount, stack, isHero) |
||||
} |
||||
|
||||
open class Action : RealmObject() { |
||||
|
||||
enum class Type(override var resId: Int) : RowRepresentable { |
||||
|
||||
POST_SB(R.string.posts_sb), |
||||
POST_BB(R.string.post_bb), |
||||
STRADDLE(R.string.straddle), |
||||
FOLD(R.string.fold), |
||||
CHECK(R.string.check), |
||||
CALL(R.string.call), |
||||
BET(R.string.bet), |
||||
POT(R.string.pot), |
||||
RAISE(R.string.raise), |
||||
UNDEFINED_ALLIN(R.string.allin), |
||||
CALL_ALLIN(R.string.callin), |
||||
BET_ALLIN(R.string.ballin), |
||||
RAISE_ALLIN(R.string.rallin); |
||||
|
||||
val isBlind: Boolean |
||||
get() { |
||||
return when (this) { |
||||
POST_SB, POST_BB -> true |
||||
else -> false |
||||
} |
||||
} |
||||
|
||||
val isSignificant: Boolean |
||||
get() { |
||||
return when (this) { |
||||
POST_SB, POST_BB, STRADDLE, BET, POT, RAISE, BET_ALLIN, RAISE_ALLIN -> true |
||||
UNDEFINED_ALLIN -> throw PAIllegalStateException("Can't ask for UNDEFINED_ALLIN") |
||||
else -> false |
||||
} |
||||
} |
||||
|
||||
/*** |
||||
* Tells if the action pulls the player out from any new decision |
||||
*/ |
||||
val isPullOut: Boolean |
||||
get() { |
||||
return when (this) { |
||||
FOLD, BET_ALLIN, RAISE_ALLIN, CALL_ALLIN, UNDEFINED_ALLIN -> true |
||||
else -> false |
||||
} |
||||
} |
||||
|
||||
/*** |
||||
* Returns if the action is passive |
||||
*/ |
||||
val isPassive: Boolean |
||||
get() { |
||||
return when (this) { |
||||
FOLD, CHECK -> true |
||||
else -> false |
||||
} |
||||
} |
||||
|
||||
val isCall: Boolean |
||||
get() { |
||||
return when (this) { |
||||
CALL, CALL_ALLIN -> true |
||||
else -> false |
||||
} |
||||
} |
||||
|
||||
val isAllin: Boolean |
||||
get() { |
||||
return when (this) { |
||||
UNDEFINED_ALLIN, BET_ALLIN, RAISE_ALLIN, CALL_ALLIN -> true |
||||
else -> false |
||||
} |
||||
} |
||||
|
||||
val color: Int |
||||
get() { |
||||
return when (this) { |
||||
POST_SB, POST_BB, CALL, CHECK, CALL_ALLIN -> R.color.kaki_lighter |
||||
FOLD -> R.color.red |
||||
else -> R.color.green |
||||
} |
||||
} |
||||
|
||||
val background: Int |
||||
get() { |
||||
return when (this) { |
||||
POST_SB, POST_BB, CALL, CHECK, CALL_ALLIN -> R.drawable.rounded_kaki_medium_rect |
||||
FOLD -> R.drawable.rounded_red_rect |
||||
else -> R.drawable.rounded_green_rect |
||||
} |
||||
} |
||||
|
||||
override val viewType: Int = RowViewType.TITLE_GRID.ordinal |
||||
|
||||
companion object { |
||||
|
||||
val defaultTypes: List<Type> by lazy { |
||||
listOf(FOLD, CHECK, BET, POT, CALL, RAISE, UNDEFINED_ALLIN) |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
/*** |
||||
* The street of the action |
||||
*/ |
||||
private var streetIdentifier: Int = 0 |
||||
var street: Street |
||||
set(value) { |
||||
this.streetIdentifier = value.ordinal |
||||
} |
||||
get() { |
||||
return streetIdentifier.let { Street.values()[it] } |
||||
} |
||||
|
||||
/*** |
||||
* The index of the action |
||||
*/ |
||||
var index: Int = 0 |
||||
|
||||
/*** |
||||
* The position of the user making the action |
||||
*/ |
||||
var position: Int = 0 |
||||
|
||||
/*** |
||||
* The type of action: check, fold, raise... |
||||
*/ |
||||
private var typeIdentifier: Int? = null |
||||
|
||||
var type: Type? |
||||
set(value) { |
||||
this.typeIdentifier = value?.ordinal |
||||
} |
||||
get() { |
||||
return typeIdentifier?.let { Type.values()[it] } |
||||
} |
||||
|
||||
/*** |
||||
* The amount linked for a bet, raise... |
||||
*/ |
||||
var amount: Double? = null |
||||
set(value) { |
||||
field = value |
||||
// Timber.d("/// set value = $value") |
||||
} |
||||
|
||||
var effectiveAmount: Double = 0.0 |
||||
|
||||
var positionRemainingStack: Double? = null |
||||
|
||||
val isActionSignificant: Boolean |
||||
get() { |
||||
return this.type?.isSignificant ?: false |
||||
} |
||||
|
||||
val formattedAmount: String? |
||||
get() { |
||||
val amount = when (this.type) { |
||||
Type.CALL, Type.CALL_ALLIN -> this.effectiveAmount |
||||
else -> this.amount |
||||
} |
||||
return amount?.formatted |
||||
} |
||||
|
||||
fun toggleType(remainingStack: Double) { |
||||
|
||||
when (this.type) { |
||||
Type.BET -> { |
||||
if (remainingStack == 0.0) { |
||||
this.type = Type.BET_ALLIN |
||||
} |
||||
} |
||||
Type.BET_ALLIN -> { |
||||
if (remainingStack > 0.0) { |
||||
this.type = Type.BET |
||||
} |
||||
} |
||||
Type.RAISE -> { |
||||
if (remainingStack == 0.0) { |
||||
this.type = Type.RAISE_ALLIN |
||||
} |
||||
} |
||||
Type.RAISE_ALLIN -> { |
||||
if (remainingStack > 0.0) { |
||||
this.type = Type.RAISE |
||||
} |
||||
} |
||||
Type.CALL -> { |
||||
if (remainingStack == 0.0) { |
||||
this.type = Type.CALL_ALLIN |
||||
} |
||||
} |
||||
Type.CALL_ALLIN -> { |
||||
if (remainingStack > 0.0) { |
||||
this.type = Type.CALL |
||||
} |
||||
} |
||||
else -> {} |
||||
} |
||||
} |
||||
|
||||
val displayedAmount: Double? |
||||
get() { |
||||
if (this.type?.isCall == true) { |
||||
return this.effectiveAmount |
||||
} |
||||
return this.amount |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,662 @@ |
||||
package net.pokeranalytics.android.model.realm.handhistory |
||||
|
||||
import android.content.Context |
||||
import android.view.LayoutInflater |
||||
import android.view.View |
||||
import android.view.ViewGroup |
||||
import androidx.appcompat.widget.AppCompatTextView |
||||
import io.realm.Realm |
||||
import io.realm.RealmList |
||||
import io.realm.RealmObject |
||||
import io.realm.annotations.Ignore |
||||
import io.realm.annotations.PrimaryKey |
||||
import net.pokeranalytics.android.R |
||||
import net.pokeranalytics.android.model.filter.Filterable |
||||
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.interfaces.Deletable |
||||
import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus |
||||
import net.pokeranalytics.android.model.interfaces.Identifiable |
||||
import net.pokeranalytics.android.model.interfaces.TimeFilterable |
||||
import net.pokeranalytics.android.model.realm.Session |
||||
import net.pokeranalytics.android.ui.modules.handhistory.evaluator.EvaluatorBridge |
||||
import net.pokeranalytics.android.ui.modules.handhistory.model.ActionReadRow |
||||
import net.pokeranalytics.android.ui.modules.handhistory.model.CardHolder |
||||
import net.pokeranalytics.android.ui.view.RowRepresentable |
||||
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 java.util.* |
||||
import kotlin.math.max |
||||
|
||||
data class PositionAmount(var position: Int, var amount: Double, var isAllin: Boolean) |
||||
|
||||
open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable, TimeFilterable, |
||||
CardHolder, Comparator<PositionAmount> { |
||||
|
||||
@PrimaryKey |
||||
override var id = UUID.randomUUID().toString() |
||||
|
||||
@Ignore |
||||
override val realmObjectClass: Class<out Identifiable> = HandHistory::class.java |
||||
|
||||
/*** |
||||
* The date of the hand history |
||||
*/ |
||||
var date: Date = Date() |
||||
set(value) { |
||||
field = value |
||||
this.updateTimeParameter(value) |
||||
} |
||||
|
||||
init { |
||||
this.date = Date() // force the custom setter call |
||||
} |
||||
|
||||
/*** |
||||
* The session whose hand was played |
||||
*/ |
||||
var session: Session? = null |
||||
|
||||
/*** |
||||
* The small blind |
||||
*/ |
||||
var smallBlind: Double? = null |
||||
set(value) { |
||||
field = value |
||||
if (this.bigBlind == null && value != null) { |
||||
this.bigBlind = value * 2 |
||||
} |
||||
} |
||||
|
||||
/*** |
||||
* The big blind |
||||
*/ |
||||
var bigBlind: Double? = null |
||||
set(value) { |
||||
field = value |
||||
if (this.smallBlind == null && value != null) { |
||||
this.smallBlind = value / 2 |
||||
} |
||||
} |
||||
|
||||
/*** |
||||
* The ante |
||||
*/ |
||||
var ante: Double = 0.0 |
||||
|
||||
/*** |
||||
* Big blind ante |
||||
*/ |
||||
var bigBlindAnte: Boolean = false |
||||
|
||||
/*** |
||||
* Number of players in the hand |
||||
*/ |
||||
var numberOfPlayers: Int = 9 |
||||
|
||||
/*** |
||||
* Number of players in the hand |
||||
*/ |
||||
var comment: String? = null |
||||
|
||||
/*** |
||||
* The position index of the hero |
||||
*/ |
||||
var heroIndex: Int? = null |
||||
|
||||
/*** |
||||
* Indicates if the hero wins the hand |
||||
*/ |
||||
var winnerPots: RealmList<WonPot> = RealmList() |
||||
|
||||
/*** |
||||
* The board |
||||
*/ |
||||
var board: RealmList<Card> = RealmList() |
||||
|
||||
/*** |
||||
* The players actions |
||||
*/ |
||||
var actions: RealmList<Action> = RealmList() |
||||
|
||||
/*** |
||||
* A list of players initial data: user, position, hand, stack |
||||
*/ |
||||
var playerSetups: RealmList<PlayerSetup> = RealmList() |
||||
|
||||
// Timed interface |
||||
override var dayOfWeek: Int? = null |
||||
override var month: Int? = null |
||||
override var year: Int? = null |
||||
override var dayOfMonth: Int? = null |
||||
|
||||
/*** |
||||
* Returns the indexes of all players |
||||
*/ |
||||
val positionIndexes: IntRange |
||||
get() { return (0 until this.numberOfPlayers) } |
||||
|
||||
// Deletable |
||||
|
||||
override fun isValidForDelete(realm: Realm): Boolean { |
||||
return true |
||||
} |
||||
|
||||
override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int { |
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates. |
||||
} |
||||
|
||||
override fun deleteDependencies(realm: Realm) { |
||||
this.board.deleteAllFromRealm() |
||||
this.playerSetups.deleteAllFromRealm() |
||||
this.actions.deleteAllFromRealm() |
||||
} |
||||
|
||||
/*** |
||||
* Configures a hand history with a [handSetup] |
||||
*/ |
||||
fun configure(handSetup: HandSetup) { |
||||
|
||||
this.playerSetups.removeAll(this.playerSetups) |
||||
|
||||
handSetup.tableSize?.let { this.numberOfPlayers = it } |
||||
handSetup.smallBlind?.let { this.smallBlind = it } |
||||
handSetup.bigBlind?.let { this.bigBlind = it } |
||||
|
||||
this.session = handSetup.session |
||||
this.date = this.session?.handHistoryAutomaticDate ?: Date() |
||||
|
||||
this.createActions(handSetup) |
||||
} |
||||
|
||||
/*** |
||||
* Creates the initial actions of the hand history |
||||
*/ |
||||
private fun createActions(handSetup: HandSetup) { |
||||
|
||||
this.actions.clear() |
||||
|
||||
this.addAction(0, Action.Type.POST_SB, this.smallBlind) |
||||
this.addAction(1, Action.Type.POST_BB, this.bigBlind) |
||||
|
||||
// var lastStraddler: Int? = null |
||||
|
||||
val positions = Position.positionsPerPlayers(this.numberOfPlayers) |
||||
handSetup.straddlePositions.forEach { position -> // position are sorted here |
||||
val positionIndex = positions.indexOf(position) |
||||
this.addAction(positionIndex, Action.Type.STRADDLE) |
||||
// lastStraddler = positionIndex |
||||
} |
||||
|
||||
// val totalActions = this.actions.size |
||||
// val startingPosition = lastStraddler?.let { it + 1 } ?: totalActions |
||||
|
||||
// for (i in this.positionIndexes - 1) { // we don't add the BB / straddler by default in case of a walk |
||||
// this.addAction((startingPosition + i) % this.numberOfPlayers) |
||||
// } |
||||
|
||||
} |
||||
|
||||
/*** |
||||
* Adds an action with the given [position], [type] and [amount] to the actions list |
||||
*/ |
||||
private fun addAction(position: Int, type: Action.Type? = null, amount: Double? = null) { |
||||
val action = Action() |
||||
action.index = this.actions.size |
||||
action.position = position |
||||
action.type = type |
||||
action.amount = amount |
||||
action.effectiveAmount = amount ?: 0.0 |
||||
this.actions.add(action) |
||||
} |
||||
|
||||
/*** |
||||
* Returns the board cards for a given [street] |
||||
*/ |
||||
fun cardsForStreet(street: Street): MutableList<Card> { |
||||
return this.board.sortedBy { it.index }.take(street.totalBoardCards).toMutableList() |
||||
} |
||||
|
||||
/*** |
||||
* Returns the optional PlayerSetup object at the [position] |
||||
*/ |
||||
fun playerSetupForPosition(position: Int): PlayerSetup? { |
||||
return this.playerSetups.firstOrNull { it.position == position } |
||||
} |
||||
|
||||
override val cards: RealmList<Card> |
||||
get() { return this.board } |
||||
|
||||
/*** |
||||
* Returns the ante sum |
||||
*/ |
||||
val anteSum: Double |
||||
get() { |
||||
return if (bigBlindAnte) { |
||||
this.bigBlind ?: 0.0 |
||||
} else { |
||||
this.ante * this.numberOfPlayers |
||||
} |
||||
} |
||||
|
||||
/*** |
||||
* Returns the sorted list of actions by index |
||||
*/ |
||||
private val sortedActions: List<Action> |
||||
get() { |
||||
return this.actions.sortedBy { it.index } |
||||
} |
||||
|
||||
/*** |
||||
* Returns the list of undefined positions, |
||||
* meaning the positions where no PlayerSetup has been created |
||||
*/ |
||||
fun undefinedPositions(): List<Position> { |
||||
val positions = Position.positionsPerPlayers(this.numberOfPlayers) |
||||
val copy = positions.clone() as LinkedHashSet<Position> |
||||
this.playerSetups.forEach { |
||||
copy.remove(positions.elementAt(it.position)) |
||||
} |
||||
return copy.toList() |
||||
} |
||||
|
||||
/*** |
||||
* Creates and affect a PlayerSetup at the given [positionIndex] |
||||
*/ |
||||
fun createPlayerSetup(positionIndex: Int): PlayerSetup { |
||||
|
||||
val playerSetup = if (this.realm != null) { |
||||
this.realm.createObject(PlayerSetup::class.java) } |
||||
else { |
||||
PlayerSetup() |
||||
} |
||||
|
||||
playerSetup.position = positionIndex |
||||
this.playerSetups.add(playerSetup) |
||||
return playerSetup |
||||
} |
||||
|
||||
/*** |
||||
* Returns the pot size at the start of the given [street] |
||||
*/ |
||||
fun potSizeForStreet(street: Street): Double { |
||||
val sortedActions = this.sortedActions |
||||
val firstIndexOfStreet = sortedActions.firstOrNull { it.street == street }?.index |
||||
?: sortedActions.size |
||||
return this.anteSum + sortedActions.take(firstIndexOfStreet).sumByDouble { it.effectiveAmount } |
||||
} |
||||
|
||||
@Ignore |
||||
override val viewType: Int = RowViewType.HAND_HISTORY.ordinal |
||||
|
||||
override fun localizedString(context: Context): CharSequence { |
||||
|
||||
val positions = Position.positionsPerPlayers(this.numberOfPlayers) |
||||
|
||||
var string = "" |
||||
|
||||
// Settings |
||||
val players = "${this.numberOfPlayers} ${context.getString(R.string.players)}" |
||||
val firstLineComponents = mutableListOf(this.date.fullDate(), players) |
||||
|
||||
this.smallBlind?.let { sb -> |
||||
this.bigBlind?.let { bb -> |
||||
firstLineComponents.add("${sb.formatted}/${bb.formatted}") |
||||
} |
||||
} |
||||
if (this.ante > 0.0) { |
||||
firstLineComponents.add("ante ${this.ante}") |
||||
} |
||||
string = string.plus(firstLineComponents.joinToString(" - ")) |
||||
string = string.addLineReturn(2) |
||||
|
||||
// Comment |
||||
this.comment?.let { comment -> |
||||
string = string.plus(comment) |
||||
string = string.addLineReturn(2) |
||||
} |
||||
|
||||
// Players |
||||
this.playerSetups.sortedBy { it.position }.forEach { |
||||
string = string.plus(localizedPlayerSetup(it, positions, context)) |
||||
string = string.addLineReturn() |
||||
} |
||||
|
||||
// Actions per street |
||||
val sortedActions = this.actions.sortedBy { it.index } |
||||
|
||||
// val actionList = ActionList() |
||||
// actionList.load(this) |
||||
|
||||
Street.values().forEach { street -> |
||||
|
||||
string = string.addLineReturn(2) |
||||
|
||||
val streetActions = sortedActions.filter { it.street == street }.compact(positions, this.heroIndex) |
||||
if (streetActions.isNotEmpty()) { |
||||
|
||||
val streetItems = mutableListOf<CharSequence>(context.getString(street.resId)) |
||||
|
||||
val potSize = this.potSizeForStreet(street) |
||||
if (potSize > 0) { |
||||
// streetItems.add(context.getString(R.string.pot_size)) |
||||
streetItems.add("(" + potSize.formatted + ")") |
||||
} |
||||
string = string.plus(streetItems.joinToString(" ")) |
||||
|
||||
val streetCards = this.cardsForStreet(street) |
||||
if (streetCards.isNotEmpty()) { |
||||
string = string.addLineReturn() |
||||
string = string.plus(streetCards.formatted(context) ?: "") |
||||
} |
||||
string = string.addLineReturn() |
||||
string = string.plus("-----------") |
||||
|
||||
string = string.addLineReturn() |
||||
|
||||
streetActions.forEach { action -> |
||||
string = string.plus(localizedAction(action, context)) |
||||
string = string.addLineReturn() |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
return string |
||||
} |
||||
|
||||
fun anteForPosition(position: Position): Double { |
||||
return if (this.bigBlindAnte) { |
||||
if (position == Position.BB) { |
||||
this.bigBlind ?: 0.0 |
||||
} else { |
||||
0.0 |
||||
} |
||||
} else { |
||||
this.ante |
||||
} |
||||
} |
||||
|
||||
/*** |
||||
* Returns a string representation of the [playerSetup] |
||||
*/ |
||||
private fun localizedPlayerSetup(playerSetup: PlayerSetup, positions: LinkedHashSet<Position>, context: Context): String { |
||||
|
||||
val playerItems = mutableListOf(positions.elementAt(playerSetup.position).value) |
||||
|
||||
val isHero = (playerSetup.position == this.heroIndex) |
||||
if (isHero) { |
||||
val heroString = context.getString(R.string.hero) |
||||
playerItems.add("- $heroString") |
||||
} |
||||
playerItems.add("[${playerSetup.cards.formatted(context)}]") |
||||
playerSetup.stack?.let { stack -> |
||||
playerItems.add("- $stack") |
||||
} |
||||
return playerItems.joinToString(" ") |
||||
} |
||||
|
||||
/*** |
||||
* Returns a string representation of the [actionReadRow] |
||||
*/ |
||||
private fun localizedAction(actionReadRow: ActionReadRow, context: Context): String { |
||||
val formattedPositions = actionReadRow.positions.joinToString(", ") { it.value } |
||||
val actionItems = mutableListOf(formattedPositions) |
||||
actionReadRow.action?.let { type -> |
||||
actionItems.add(context.getString(type.resId)) |
||||
} |
||||
actionReadRow.amount?.let { amount -> |
||||
actionItems.add(amount.formatted) |
||||
} |
||||
return actionItems.joinToString(" ") |
||||
} |
||||
|
||||
/*** |
||||
* Returns if the hero has won the hand, or part of the pot |
||||
*/ |
||||
val heroWins: Boolean? |
||||
get() { |
||||
return this.heroIndex?.let { heroIndex -> |
||||
this.winnerPots.any { it.position == heroIndex } |
||||
} ?: run { |
||||
null |
||||
} |
||||
} |
||||
|
||||
/*** |
||||
* Creates a list of cards for the hand history to give a representation of the hand |
||||
* We will try to add a minimum of 5 cards using by priority: |
||||
* - the hero hand |
||||
* - the opponents hands |
||||
* - the board |
||||
*/ |
||||
fun cardViews(context: Context, viewGroup: ViewGroup): List<View> { |
||||
|
||||
val views = mutableListOf<View>() |
||||
val layoutInflater = LayoutInflater.from(context) |
||||
|
||||
// Create all the possible cards list: hero, opponents, board |
||||
val cardSets = mutableListOf<List<Card>>() |
||||
|
||||
// Hero |
||||
this.heroIndex?.let { hIndex -> |
||||
this.playerSetupForPosition(hIndex)?.cards?.let { |
||||
cardSets.add(it) |
||||
} |
||||
} |
||||
// Opponents |
||||
this.playerSetups.filter { it.cards.isNotEmpty() && it.position != this.heroIndex }.forEach { |
||||
cardSets.add(it.cards) |
||||
} |
||||
// Board |
||||
cardSets.add(this.board) |
||||
|
||||
// Try to add cards but not too much |
||||
while (views.size < 5 && cardSets.isNotEmpty()) { |
||||
|
||||
val cardSet = cardSets.removeAt(0) |
||||
|
||||
if (views.isNotEmpty() && cardSet.isNotEmpty()) { // add separator with previous set of cards |
||||
val view = layoutInflater.inflate(R.layout.view_card_separator, viewGroup, false) as AppCompatTextView |
||||
views.add(view) |
||||
} |
||||
cardSet.forEach { views.add(it.view(context, layoutInflater, viewGroup)) } |
||||
} |
||||
|
||||
// Add 5 blank cards if no card has been added |
||||
if (views.isEmpty()) { |
||||
val blankCard = Card() |
||||
(1..5).forEach { _ -> |
||||
val view = blankCard.view(context, layoutInflater, viewGroup, true) |
||||
view.setBackgroundResource(R.drawable.rounded_kaki_medium_rect) |
||||
views.add(view) |
||||
} |
||||
} |
||||
return views |
||||
} |
||||
|
||||
data class Pot(var amount: Double, var level: Double, var positions: MutableSet<Int> = mutableSetOf()) |
||||
|
||||
/*** |
||||
* Defines which positions win the hand |
||||
*/ |
||||
fun defineWinnerPositions() { |
||||
|
||||
val folds = this.sortedActions.filter { it.type == Action.Type.FOLD }.map { it.position } |
||||
val activePositions = this.positionIndexes.toMutableList() |
||||
activePositions.removeAll(folds) |
||||
|
||||
val wonPots = when (activePositions.size) { |
||||
0 -> listOf() // no winner, everyone has fold. Should not happen |
||||
1 -> { // One player has not fold, typically BET / FOLD |
||||
val pot = WonPot() |
||||
pot.position = activePositions.first() |
||||
pot.amount = potSizeForStreet(Street.SUMMARY) |
||||
listOf(pot) |
||||
} |
||||
else -> { // Several players remains, typically BET/FOLD or CHECKS |
||||
this.wonPots(getPots(activePositions)) |
||||
} |
||||
} |
||||
|
||||
this.winnerPots.clear() |
||||
this.winnerPots.addAll(wonPots) |
||||
} |
||||
|
||||
/*** |
||||
* Returns a list with all the different pots with the appropriate eligible players |
||||
*/ |
||||
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>() |
||||
|
||||
// get all committed amounts for the street by player, by allin |
||||
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 |
||||
|
||||
// for each player |
||||
val streetPots = mutableListOf<Pot>() |
||||
amountsPerPosition.forEach { positionAmount -> |
||||
val amount = positionAmount.amount |
||||
val position = positionAmount.position |
||||
|
||||
var rest = amount |
||||
var lastPotLevel = 0.0 |
||||
// put invested amount in smaller pots |
||||
streetPots.forEach { pot -> |
||||
val added = pot.level - lastPotLevel |
||||
pot.amount += added |
||||
if (eligiblePositions.contains(position)) { |
||||
pot.positions.add(position) |
||||
} |
||||
rest -= added |
||||
lastPotLevel = pot.level |
||||
} |
||||
// Adds remaining chips to the running Pot |
||||
runningPotAmount += rest |
||||
|
||||
// If the player is allin, create a new pot for the relevant amount |
||||
val isAllin = allinPositions.contains(position) |
||||
if (isAllin) { |
||||
streetPots.add(Pot(runningPotAmount, amount, mutableSetOf(position))) |
||||
runningPotAmount = 0.0 |
||||
} |
||||
|
||||
} |
||||
pots.addAll(streetPots) |
||||
} |
||||
|
||||
} |
||||
|
||||
// Create a pot with the remaining chips |
||||
if (runningPotAmount > 0.0) { |
||||
pots.add(Pot(runningPotAmount, 0.0, eligiblePositions.toMutableSet())) |
||||
} |
||||
|
||||
return pots |
||||
} |
||||
|
||||
private fun wonPots(pots: List<Pot>): Collection<WonPot> { |
||||
|
||||
val wonPots = hashMapOf<Int, WonPot>() |
||||
|
||||
pots.forEach { pot -> |
||||
|
||||
val winningPositions = compareHands(pot.positions.toList()) |
||||
|
||||
// Distributes the pot for each winners |
||||
val share = pot.amount / 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 |
||||
} |
||||
|
||||
/*** |
||||
* Compares the hands of the players at the given [activePositions] |
||||
* Returns the list of winning hands by position |
||||
*/ |
||||
private fun compareHands(activePositions: List<Int>): List<Int> { |
||||
|
||||
// Evaluate all hands |
||||
val results = activePositions.map { |
||||
this.playerSetupForPosition(it)?.cards?.let { hand -> |
||||
EvaluatorBridge.playerHand(hand, this.board) |
||||
} ?: run { |
||||
Int.MAX_VALUE |
||||
} |
||||
} |
||||
|
||||
// Check who has best score (EvaluatorBridge gives a lowest score for a better hand) |
||||
return results.min()?.let { best -> |
||||
val winners = mutableListOf<Int>() |
||||
results.forEachIndexed { index, i -> |
||||
if (i == best && i != Int.MAX_VALUE) { |
||||
winners.add(activePositions[index]) |
||||
} |
||||
} |
||||
winners |
||||
} ?: run { |
||||
listOf<Int>() // type needed for build |
||||
} |
||||
|
||||
} |
||||
|
||||
/*** |
||||
* 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() |
||||
} |
||||
} |
||||
|
||||
val maxPlayerCards: Int |
||||
get() { |
||||
var max = 0 |
||||
this.playerSetups.forEach { |
||||
max = max(it.cards.size, max) |
||||
} |
||||
return max |
||||
} |
||||
|
||||
val usesWildcards: Boolean |
||||
get() { |
||||
val boardHasWildCard = this.cards.any { it.isWildCard } |
||||
val playerCardHasWildCard = this.playerSetups.any { it.cards.any { it.isWildCard } } |
||||
return boardHasWildCard || playerCardHasWildCard |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,31 @@ |
||||
package net.pokeranalytics.android.model.realm.handhistory |
||||
|
||||
import io.realm.RealmList |
||||
import io.realm.RealmObject |
||||
import net.pokeranalytics.android.model.realm.Player |
||||
import net.pokeranalytics.android.ui.modules.handhistory.model.CardHolder |
||||
import net.pokeranalytics.android.ui.view.Localizable |
||||
|
||||
open class PlayerSetup : RealmObject(), CardHolder, Localizable { |
||||
|
||||
/*** |
||||
* The player |
||||
*/ |
||||
var player: Player? = null |
||||
|
||||
/*** |
||||
* The position at the table |
||||
*/ |
||||
var position: Int = 0 |
||||
|
||||
/*** |
||||
* The initial stack of the player |
||||
*/ |
||||
var stack: Double? = null |
||||
|
||||
/*** |
||||
* The cards of the player |
||||
*/ |
||||
override var cards: RealmList<Card> = RealmList() |
||||
|
||||
} |
||||
@ -0,0 +1,17 @@ |
||||
package net.pokeranalytics.android.model.realm.handhistory |
||||
|
||||
import io.realm.RealmObject |
||||
|
||||
open class WonPot: RealmObject() { |
||||
|
||||
/*** |
||||
* The position of the player |
||||
*/ |
||||
var position: Int = 0 |
||||
|
||||
/*** |
||||
* The amount won |
||||
*/ |
||||
var amount: Double = 0.0 |
||||
|
||||
} |
||||
@ -1,58 +0,0 @@ |
||||
package net.pokeranalytics.android.ui.activity |
||||
|
||||
import android.content.Context |
||||
import android.content.Intent |
||||
import android.os.Bundle |
||||
import androidx.fragment.app.Fragment |
||||
import kotlinx.android.synthetic.main.activity_data_list.* |
||||
import net.pokeranalytics.android.R |
||||
import net.pokeranalytics.android.ui.activity.components.BaseActivity |
||||
import net.pokeranalytics.android.ui.fragment.DataListFragment |
||||
import net.pokeranalytics.android.ui.interfaces.FilterActivityRequestCode |
||||
|
||||
class DataListActivity : BaseActivity() { |
||||
|
||||
enum class IntentKey(val keyName: String) { |
||||
DATA_TYPE("DATA_TYPE"), |
||||
LIVE_DATA_TYPE("LIVE_DATA_TYPE"), |
||||
ITEM_DELETED("ITEM_DELETED"), |
||||
SHOW_ADD_BUTTON("SHOW_ADD_BUTTON"), |
||||
} |
||||
|
||||
companion object { |
||||
fun newInstance(context: Context, dataType: Int) { |
||||
context.startActivity(getIntent(context, dataType)) |
||||
} |
||||
|
||||
fun newSelectInstance(fragment: Fragment, dataType: Int, showAddButton: Boolean = true) { |
||||
val context = fragment.requireContext() |
||||
fragment.startActivityForResult(getIntent(context, dataType, showAddButton), FilterActivityRequestCode.SELECT_FILTER.ordinal) |
||||
} |
||||
|
||||
private fun getIntent(context: Context, dataType: Int, showAddButton: Boolean = true): Intent { |
||||
val intent = Intent(context, DataListActivity::class.java) |
||||
intent.putExtra(IntentKey.DATA_TYPE.keyName, dataType) |
||||
intent.putExtra(IntentKey.SHOW_ADD_BUTTON.keyName, showAddButton) |
||||
return intent |
||||
} |
||||
} |
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
||||
super.onCreate(savedInstanceState) |
||||
setContentView(R.layout.activity_data_list) |
||||
|
||||
initUI() |
||||
} |
||||
|
||||
/** |
||||
* Init UI |
||||
*/ |
||||
private fun initUI() { |
||||
val dataType = intent.getIntExtra(IntentKey.DATA_TYPE.keyName, 0) |
||||
val showAddButton = intent.getBooleanExtra(IntentKey.SHOW_ADD_BUTTON.keyName, true) |
||||
val fragment = dataListFragment as DataListFragment |
||||
fragment.setData(dataType) |
||||
fragment.updateUI(showAddButton) |
||||
} |
||||
|
||||
} |
||||
@ -1,71 +0,0 @@ |
||||
package net.pokeranalytics.android.ui.activity |
||||
|
||||
import android.content.Context |
||||
import android.content.Intent |
||||
import android.os.Bundle |
||||
import androidx.fragment.app.Fragment |
||||
import net.pokeranalytics.android.R |
||||
import net.pokeranalytics.android.model.realm.Filter |
||||
import net.pokeranalytics.android.ui.activity.components.BaseActivity |
||||
import net.pokeranalytics.android.ui.fragment.FilterDetailsFragment |
||||
|
||||
class FilterDetailsActivity : BaseActivity() { |
||||
|
||||
enum class IntentKey(val keyName: String) { |
||||
FILTER_ID("FILTER_ID"), |
||||
FILTER_CATEGORY_ORDINAL("FILTER_CATEGORY_ORDINAL") |
||||
} |
||||
|
||||
companion object { |
||||
|
||||
/** |
||||
* Default constructor |
||||
*/ |
||||
fun newInstance(context: Context, filterId: String, filterCategoryOrdinal: Int) { |
||||
val intent = Intent(context, FilterDetailsActivity::class.java) |
||||
intent.putExtra(IntentKey.FILTER_ID.keyName, filterId) |
||||
intent.putExtra(IntentKey.FILTER_CATEGORY_ORDINAL.keyName, filterCategoryOrdinal) |
||||
context.startActivity(intent) |
||||
} |
||||
|
||||
/** |
||||
* Create a new instance for result |
||||
*/ |
||||
fun newInstanceForResult(fragment: Fragment, filterId: String, filterCategoryOrdinal: Int, requestCode: Int, filter: Filter? = null) { |
||||
|
||||
val intent = Intent(fragment.requireContext(), FilterDetailsActivity::class.java) |
||||
intent.putExtra(IntentKey.FILTER_ID.keyName, filterId) |
||||
intent.putExtra(IntentKey.FILTER_CATEGORY_ORDINAL.keyName, filterCategoryOrdinal) |
||||
fragment.startActivityForResult(intent, requestCode) |
||||
} |
||||
} |
||||
|
||||
private lateinit var fragment: FilterDetailsFragment |
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
||||
super.onCreate(savedInstanceState) |
||||
setContentView(R.layout.activity_filter_details) |
||||
initUI() |
||||
} |
||||
|
||||
override fun onBackPressed() { |
||||
fragment.onBackPressed() |
||||
} |
||||
|
||||
/** |
||||
* Init UI |
||||
*/ |
||||
private fun initUI() { |
||||
|
||||
val fragmentManager = supportFragmentManager |
||||
val fragmentTransaction = fragmentManager.beginTransaction() |
||||
val filterId = intent.getStringExtra(IntentKey.FILTER_ID.keyName) |
||||
val filterCategoryOrdinal = intent.getIntExtra(IntentKey.FILTER_CATEGORY_ORDINAL.keyName, 0) |
||||
|
||||
fragment = FilterDetailsFragment() |
||||
fragmentTransaction.add(R.id.container, fragment) |
||||
fragmentTransaction.commit() |
||||
fragment.setData(filterId, filterCategoryOrdinal) |
||||
} |
||||
|
||||
} |
||||
@ -1,74 +0,0 @@ |
||||
package net.pokeranalytics.android.ui.activity |
||||
|
||||
import android.content.Context |
||||
import android.content.Intent |
||||
import android.os.Bundle |
||||
import androidx.fragment.app.Fragment |
||||
import net.pokeranalytics.android.R |
||||
import net.pokeranalytics.android.ui.activity.components.BaseActivity |
||||
import net.pokeranalytics.android.ui.fragment.FiltersFragment |
||||
import net.pokeranalytics.android.ui.interfaces.FilterActivityRequestCode |
||||
import net.pokeranalytics.android.ui.interfaces.FilterableType |
||||
|
||||
class FiltersActivity : BaseActivity() { |
||||
|
||||
enum class IntentKey(val keyName: String) { |
||||
FILTER_ID("FILTER_ID"), |
||||
FILTERABLE_TYPE("FILTERABLE_TYPE"), |
||||
HIDE_MOST_USED_FILTERS("HIDE_MOST_USED_FILTERS"), |
||||
; |
||||
} |
||||
|
||||
private lateinit var fragment: FiltersFragment |
||||
|
||||
companion object { |
||||
/** |
||||
* Create a new instance for result |
||||
*/ |
||||
fun newInstanceForResult(fragment: Fragment, filterId: String? = null, currentFilterable: FilterableType, hideMostUsedFilters: Boolean = false) { |
||||
val intent = getIntent(fragment.requireContext(), filterId, currentFilterable, hideMostUsedFilters) |
||||
fragment.startActivityForResult(intent, FilterActivityRequestCode.CREATE_FILTER.ordinal) |
||||
} |
||||
|
||||
private fun getIntent(context: Context, filterId: String?, currentFilterable: FilterableType, hideMostUsedFilters: Boolean = false): Intent { |
||||
val intent = Intent(context, FiltersActivity::class.java) |
||||
intent.putExtra(IntentKey.FILTERABLE_TYPE.keyName, currentFilterable.uniqueIdentifier) |
||||
intent.putExtra(IntentKey.HIDE_MOST_USED_FILTERS.keyName, hideMostUsedFilters) |
||||
filterId?.let { |
||||
intent.putExtra(IntentKey.FILTER_ID.keyName, it) |
||||
} |
||||
return intent |
||||
} |
||||
} |
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
||||
super.onCreate(savedInstanceState) |
||||
setContentView(R.layout.activity_filters) |
||||
initUI() |
||||
} |
||||
|
||||
override fun onBackPressed() { |
||||
fragment.onBackPressed() |
||||
} |
||||
|
||||
/** |
||||
* Init UI |
||||
*/ |
||||
private fun initUI() { |
||||
|
||||
val fragmentManager = supportFragmentManager |
||||
val fragmentTransaction = fragmentManager.beginTransaction() |
||||
val filterId = intent.getStringExtra(IntentKey.FILTER_ID.keyName) |
||||
val uniqueIdentifier= intent.getIntExtra(IntentKey.FILTERABLE_TYPE.keyName, 0) |
||||
val hideMostUsedFilters = intent.getBooleanExtra(IntentKey.HIDE_MOST_USED_FILTERS.keyName, false) |
||||
val filterableType = FilterableType.valueByIdentifier(uniqueIdentifier) |
||||
|
||||
fragment = FiltersFragment() |
||||
fragment.setData(filterId, filterableType) |
||||
fragmentTransaction.add(R.id.container, fragment) |
||||
fragmentTransaction.commit() |
||||
fragment.updateMostUsedFiltersVisibility(!hideMostUsedFilters) |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -1,58 +0,0 @@ |
||||
package net.pokeranalytics.android.ui.activity |
||||
|
||||
import android.content.Context |
||||
import android.content.Intent |
||||
import android.os.Bundle |
||||
import androidx.fragment.app.Fragment |
||||
import kotlinx.android.synthetic.main.activity_filters_list.* |
||||
import net.pokeranalytics.android.R |
||||
import net.pokeranalytics.android.ui.activity.components.BaseActivity |
||||
import net.pokeranalytics.android.ui.fragment.FiltersListFragment |
||||
import net.pokeranalytics.android.ui.interfaces.FilterActivityRequestCode |
||||
|
||||
class FiltersListActivity : BaseActivity() { |
||||
|
||||
enum class IntentKey(val keyName: String) { |
||||
DATA_TYPE("DATA_TYPE"), |
||||
LIVE_DATA_TYPE("LIVE_DATA_TYPE"), |
||||
ITEM_DELETED("ITEM_DELETED"), |
||||
SHOW_ADD_BUTTON("SHOW_ADD_BUTTON"), |
||||
} |
||||
|
||||
companion object { |
||||
fun newInstance(context: Context, dataType: Int) { |
||||
context.startActivity(getIntent(context, dataType)) |
||||
} |
||||
|
||||
fun newSelectInstance(fragment: Fragment, dataType: Int, showAddButton: Boolean = true) { |
||||
val context = fragment.requireContext() |
||||
fragment.startActivityForResult(getIntent(context, dataType, showAddButton), FilterActivityRequestCode.SELECT_FILTER.ordinal) |
||||
} |
||||
|
||||
private fun getIntent(context: Context, dataType: Int, showAddButton: Boolean = true): Intent { |
||||
val intent = Intent(context, FiltersListActivity::class.java) |
||||
intent.putExtra(IntentKey.DATA_TYPE.keyName, dataType) |
||||
intent.putExtra(IntentKey.SHOW_ADD_BUTTON.keyName, showAddButton) |
||||
return intent |
||||
} |
||||
} |
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
||||
super.onCreate(savedInstanceState) |
||||
setContentView(R.layout.activity_filters_list) |
||||
|
||||
initUI() |
||||
} |
||||
|
||||
/** |
||||
* Init UI |
||||
*/ |
||||
private fun initUI() { |
||||
val dataType = intent.getIntExtra(IntentKey.DATA_TYPE.keyName, 0) |
||||
val showAddButton = intent.getBooleanExtra(IntentKey.SHOW_ADD_BUTTON.keyName, true) |
||||
val fragment = filtersListFragment as FiltersListFragment |
||||
fragment.setData(dataType) |
||||
fragment.updateUI(showAddButton) |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,240 @@ |
||||
package net.pokeranalytics.android.ui.adapter |
||||
|
||||
import android.view.LayoutInflater |
||||
import android.view.View |
||||
import android.view.ViewGroup |
||||
import androidx.appcompat.widget.AppCompatTextView |
||||
import androidx.recyclerview.widget.RecyclerView |
||||
import io.realm.RealmModel |
||||
import io.realm.RealmQuery |
||||
import io.realm.RealmResults |
||||
import net.pokeranalytics.android.R |
||||
import net.pokeranalytics.android.exceptions.PAIllegalStateException |
||||
import net.pokeranalytics.android.model.filter.Filterable |
||||
import net.pokeranalytics.android.model.realm.Filter |
||||
import net.pokeranalytics.android.ui.modules.feed.FeedTransactionRowRepresentableAdapter |
||||
import net.pokeranalytics.android.ui.view.RowRepresentable |
||||
import net.pokeranalytics.android.ui.view.RowViewType |
||||
import net.pokeranalytics.android.util.NULL_TEXT |
||||
import net.pokeranalytics.android.util.extensions.getMonthAndYear |
||||
import java.util.* |
||||
import kotlin.collections.HashMap |
||||
|
||||
interface DateModel : RowRepresentable, RealmModel, Filterable { |
||||
var date: Date |
||||
} |
||||
|
||||
|
||||
|
||||
class TransactionED : EntityDescriptor { |
||||
override fun bindableHolder(view: View): RecyclerView.ViewHolder { |
||||
TODO() |
||||
} |
||||
|
||||
override val layout: Int = R.layout.row_transaction |
||||
|
||||
override val viewType: Int = RowViewType.ROW_TRANSACTION.ordinal |
||||
|
||||
override val sortFieldName: String = "date" |
||||
|
||||
override fun distinctHeaders(realmQuery: RealmQuery<out DateModel>): RealmQuery<out DateModel> { |
||||
return realmQuery.distinct("year", "month") |
||||
} |
||||
|
||||
} |
||||
|
||||
interface EntityDescriptor { |
||||
|
||||
fun bindableHolder(view: View): RecyclerView.ViewHolder |
||||
|
||||
val layout: Int |
||||
|
||||
val viewType: Int |
||||
|
||||
val sortFieldName: String |
||||
|
||||
fun distinctHeaders(realmQuery: RealmQuery<out DateModel>): RealmQuery<out DateModel> |
||||
|
||||
// val clazz: Class<T> |
||||
} |
||||
|
||||
/** |
||||
* An adapter capable of displaying a list of RowRepresentables |
||||
* @param dataSource the datasource providing rows |
||||
* @param delegate the delegate, notified of UI actions |
||||
*/ |
||||
class FilterSectionAdapter( |
||||
override var delegate: RowRepresentableDelegate? = null, |
||||
override var dataSource: RowRepresentableDataSource, |
||||
var descriptor: EntityDescriptor, |
||||
var realmQuery: RealmQuery<out DateModel> |
||||
// var distinctTransactionsHeaders: RealmResults<Transaction> |
||||
) : |
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>(), RecyclerAdapter { |
||||
|
||||
private var headersPositions = HashMap<Int, Date?>() |
||||
private lateinit var sortedHeaders: SortedMap<Int, Date?> |
||||
|
||||
private var realmEntities: RealmResults<out DateModel> |
||||
private var realmHeaders: RealmResults<out DateModel> |
||||
|
||||
var filter: Filter? = null |
||||
// |
||||
// companion object { |
||||
// |
||||
// inline fun <reified T : DateModel> build(delegate: RowRepresentableDelegate?, dataSource: RowRepresentableDataSource, descriptor: EntityDescriptor<T>, realmQuery: RealmQuery<T>) : FilterSectionAdapter<T> { |
||||
// val adapter = FilterSectionAdapter(delegate, dataSource, descriptor, realmQuery) |
||||
// adapter.load() |
||||
// return adapter |
||||
// } |
||||
// |
||||
// } |
||||
|
||||
init { |
||||
|
||||
// this.realmEntities = this.realmQuery.findAll().sort(this.descriptor.sortFieldName) |
||||
this.realmEntities = this.filter?.results() ?: this.realmQuery.findAll().sort(this.descriptor.sortFieldName) |
||||
this.realmHeaders = this.descriptor.distinctHeaders(this.realmQuery).findAll() |
||||
|
||||
|
||||
refreshData() |
||||
} |
||||
|
||||
// fun load() { |
||||
// |
||||
// val f = this.filter?.results()?: this.realmQuery.findAll().sort(this.descriptor.sortFieldName) |
||||
// this.realmEntities = f |
||||
// |
||||
// } |
||||
|
||||
|
||||
// /** |
||||
// * Display a transaction view |
||||
// */ |
||||
// inner class RowEntityViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder { |
||||
// |
||||
// fun bind(position: Int, row: DateModel?, adapter: FilterSectionAdapter) { |
||||
// |
||||
//// itemView.transactionRow.setData(row as Transaction) |
||||
//// val listener = View.OnClickListener { |
||||
//// adapter.delegate?.onRowSelected(position, row) |
||||
//// } |
||||
//// itemView.transactionRow.setOnClickListener(listener) |
||||
// } |
||||
// } |
||||
|
||||
/** |
||||
* Display a header |
||||
*/ |
||||
inner class HeaderTitleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder { |
||||
|
||||
fun bind(title: String) { |
||||
// Title |
||||
itemView.findViewById<AppCompatTextView>(R.id.title)?.let { |
||||
it.text = title |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { |
||||
val inflater = LayoutInflater.from(parent.context) |
||||
return if (viewType == this.descriptor.viewType) { // TODO layout |
||||
val layout = inflater.inflate(this.descriptor.layout, parent, false) |
||||
this.descriptor.bindableHolder(layout) |
||||
// RowEntityViewHolder(layout) |
||||
} else { |
||||
val layout = inflater.inflate(R.layout.row_header_title, parent, false) |
||||
HeaderTitleViewHolder(layout) |
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
override fun getItemViewType(position: Int): Int { |
||||
return if (sortedHeaders.containsKey(position)) { |
||||
RowViewType.HEADER_TITLE.ordinal |
||||
} else { |
||||
this.descriptor.viewType // RowViewType.ROW_TRANSACTION.ordinal |
||||
} |
||||
} |
||||
|
||||
|
||||
override fun getItemCount(): Int { |
||||
return realmEntities.size + realmHeaders.size |
||||
} |
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { |
||||
|
||||
if (holder is BindableHolder) { |
||||
holder.onBind(position, getEntityForPosition(position), this) |
||||
} else if (holder is FeedTransactionRowRepresentableAdapter.HeaderTitleViewHolder) { |
||||
holder.bind(getHeaderForPosition(position)) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Return the header |
||||
*/ |
||||
private fun getHeaderForPosition(position: Int): String { |
||||
if (sortedHeaders.containsKey(position)) { |
||||
val realmHeaderPosition = sortedHeaders.keys.indexOf(position) |
||||
return realmHeaders[realmHeaderPosition]?.date?.getMonthAndYear() ?: "" |
||||
} |
||||
return NULL_TEXT |
||||
} |
||||
|
||||
/** |
||||
* Get real index |
||||
*/ |
||||
private fun getEntityForPosition(position: Int): DateModel { |
||||
|
||||
// Row position |
||||
var headersBefore = 0 |
||||
for (key in sortedHeaders.keys) { |
||||
if (position > key) { |
||||
headersBefore++ |
||||
} else { |
||||
break |
||||
} |
||||
} |
||||
|
||||
return realmEntities[position - headersBefore] ?: throw PAIllegalStateException("Nasty problem!") |
||||
} |
||||
|
||||
/** |
||||
* Refresh headers positions |
||||
*/ |
||||
fun refreshData() { |
||||
|
||||
headersPositions.clear() |
||||
|
||||
var previousYear = Int.MAX_VALUE |
||||
var previousMonth = Int.MAX_VALUE |
||||
|
||||
val calendar = Calendar.getInstance() |
||||
|
||||
// Add headers if the date doesn't exist yet |
||||
for ((index, transaction) in realmEntities.withIndex()) { |
||||
calendar.time = transaction.date |
||||
if (checkHeaderCondition(calendar, previousYear, previousMonth)) { |
||||
headersPositions[index + headersPositions.size] = transaction.date |
||||
previousYear = calendar.get(Calendar.YEAR) |
||||
previousMonth = calendar.get(Calendar.MONTH) |
||||
} |
||||
} |
||||
|
||||
sortedHeaders = headersPositions.toSortedMap() |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Check if we need to add a header |
||||
* Can be change to manage different condition |
||||
*/ |
||||
private fun checkHeaderCondition(currentCalendar: Calendar, previousYear: Int, previousMonth: Int): Boolean { |
||||
return currentCalendar.get(Calendar.YEAR) == previousYear && currentCalendar.get(Calendar.MONTH) < previousMonth || (currentCalendar.get(Calendar.YEAR) < previousYear) |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,26 @@ |
||||
package net.pokeranalytics.android.ui.adapter |
||||
|
||||
import android.view.View |
||||
import net.pokeranalytics.android.ui.view.RowRepresentable |
||||
|
||||
interface RecyclerAdapter { |
||||
var dataSource: RowRepresentableDataSource |
||||
var delegate: RowRepresentableDelegate? |
||||
} |
||||
|
||||
interface RowRepresentableDelegate { |
||||
fun onRowSelected(position: Int, row: RowRepresentable, tag: Int = 0) {} |
||||
fun onRowDeselected(position: Int, row: RowRepresentable) {} |
||||
fun onRowValueChanged(value: Any?, row: RowRepresentable) {} |
||||
fun onRowDeleted(row: RowRepresentable) {} |
||||
fun onRowLongClick(itemView: View, row: RowRepresentable, position: Int) {} |
||||
fun onItemClick(position: Int, row: RowRepresentable, tag: Int = 0) {} |
||||
} |
||||
|
||||
/** |
||||
* An interface used to factor the configuration of RecyclerView.ViewHolder |
||||
*/ |
||||
interface BindableHolder { |
||||
fun onBind(position: Int, row: RowRepresentable, adapter: RecyclerAdapter) { |
||||
} |
||||
} |
||||
@ -1,201 +0,0 @@ |
||||
package net.pokeranalytics.android.ui.fragment |
||||
|
||||
import android.app.Activity |
||||
import android.content.Intent |
||||
import android.os.Bundle |
||||
import android.view.* |
||||
import androidx.appcompat.widget.SearchView |
||||
import androidx.core.view.isVisible |
||||
import androidx.recyclerview.widget.ItemTouchHelper |
||||
import androidx.recyclerview.widget.LinearLayoutManager |
||||
import io.realm.Realm |
||||
import io.realm.RealmResults |
||||
import kotlinx.android.synthetic.main.fragment_data_list.* |
||||
import net.pokeranalytics.android.R |
||||
import net.pokeranalytics.android.exceptions.PAIllegalStateException |
||||
import net.pokeranalytics.android.model.LiveData |
||||
import net.pokeranalytics.android.model.interfaces.Deletable |
||||
import net.pokeranalytics.android.model.interfaces.Identifiable |
||||
import net.pokeranalytics.android.model.realm.Filter |
||||
import net.pokeranalytics.android.ui.activity.EditableDataActivity |
||||
import net.pokeranalytics.android.ui.activity.FiltersActivity |
||||
import net.pokeranalytics.android.ui.adapter.LiveRowRepresentableDataSource |
||||
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter |
||||
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate |
||||
import net.pokeranalytics.android.ui.extensions.removeMargins |
||||
import net.pokeranalytics.android.ui.fragment.components.DeletableItemFragment |
||||
import net.pokeranalytics.android.ui.helpers.SwipeToDeleteCallback |
||||
import net.pokeranalytics.android.ui.view.RowRepresentable |
||||
import net.pokeranalytics.android.ui.view.RowViewType |
||||
import net.pokeranalytics.android.util.extensions.find |
||||
import net.pokeranalytics.android.util.extensions.sorted |
||||
|
||||
|
||||
open class DataListFragment : DeletableItemFragment(), LiveRowRepresentableDataSource, RowRepresentableDelegate { |
||||
|
||||
companion object { |
||||
const val REQUEST_CODE_DETAILS = 1000 |
||||
} |
||||
|
||||
private lateinit var identifiableClass: Class<out Deletable> |
||||
|
||||
private lateinit var dataType: LiveData |
||||
private lateinit var items: RealmResults<out Deletable> |
||||
private var dataListMenu: Menu? = null |
||||
private var searchView: SearchView? = null |
||||
|
||||
var isSearchable: Boolean = false |
||||
set(value) { |
||||
field = value |
||||
val searchMenuItem = dataListMenu?.findItem(R.id.action_search) |
||||
searchMenuItem?.isVisible = value |
||||
} |
||||
|
||||
/** |
||||
* Set fragment data |
||||
*/ |
||||
open fun setData(dataType: Int) { |
||||
|
||||
this.dataType = LiveData.values()[dataType] |
||||
this.identifiableClass = this.dataType.relatedEntity |
||||
setToolbarTitle(this.dataType.pluralLocalizedTitle(requireContext())) |
||||
|
||||
this.items = this.retrieveItems(getRealm()) |
||||
|
||||
this.isSearchable = when (this.dataType) { |
||||
LiveData.PLAYER, LiveData.LOCATION -> true |
||||
else -> false |
||||
} |
||||
} |
||||
|
||||
open fun retrieveItems(realm: Realm): RealmResults<out Deletable> { |
||||
return realm.sorted(this.identifiableClass, editableOnly = true, filterableTypeUniqueIdentifier = dataType.subType) |
||||
} |
||||
|
||||
override fun deletableItems() : List<Deletable> { |
||||
return this.items |
||||
} |
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { |
||||
super.onCreateView(inflater, container, savedInstanceState) |
||||
return inflater.inflate(R.layout.fragment_data_list, container, false) |
||||
} |
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
||||
super.onViewCreated(view, savedInstanceState) |
||||
initUI() |
||||
} |
||||
|
||||
/** |
||||
* Init UI |
||||
*/ |
||||
private fun initUI() { |
||||
|
||||
setDisplayHomeAsUpEnabled(true) |
||||
|
||||
val viewManager = LinearLayoutManager(requireContext()) |
||||
dataListAdapter = RowRepresentableAdapter(this, this) |
||||
|
||||
val swipeToDelete = SwipeToDeleteCallback(dataListAdapter) { position -> |
||||
val item = this.items[position] |
||||
if (item != null) { |
||||
val itemId = item.id |
||||
deleteItem(dataListAdapter, items, itemId) |
||||
} else { |
||||
throw PAIllegalStateException("Item with position $position not found") |
||||
} |
||||
} |
||||
|
||||
val itemTouchHelper = ItemTouchHelper(swipeToDelete) |
||||
|
||||
recyclerView.apply { |
||||
setHasFixedSize(true) |
||||
layoutManager = viewManager |
||||
adapter = dataListAdapter |
||||
itemTouchHelper.attachToRecyclerView(this) |
||||
} |
||||
|
||||
this.addButton.setOnClickListener { |
||||
EditableDataActivity.newInstance( |
||||
requireContext(), |
||||
dataType = this.dataType.ordinal, |
||||
primaryKey = null |
||||
) |
||||
} |
||||
} |
||||
|
||||
override fun onResume() { |
||||
super.onResume() |
||||
this.recyclerView?.adapter?.notifyDataSetChanged() |
||||
} |
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { |
||||
|
||||
menu.clear() |
||||
inflater.inflate(R.menu.toolbar_data_list, menu) |
||||
this.dataListMenu = menu |
||||
|
||||
val searchMenuItem = menu.findItem(R.id.action_search) |
||||
searchMenuItem.isVisible = isSearchable |
||||
|
||||
searchView = searchMenuItem.actionView as SearchView? |
||||
searchView?.removeMargins() |
||||
searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { |
||||
override fun onQueryTextSubmit(query: String?): Boolean { |
||||
return false |
||||
} |
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean { |
||||
filterItemsWithSearch(newText) |
||||
return false |
||||
} |
||||
}) |
||||
|
||||
super.onCreateOptionsMenu(menu, inflater) |
||||
} |
||||
|
||||
override fun rowRepresentableForPosition(position: Int): RowRepresentable? { |
||||
return this.items[position] as RowRepresentable |
||||
} |
||||
|
||||
override fun numberOfRows(): Int { |
||||
return this.items.size |
||||
} |
||||
|
||||
override fun viewTypeForPosition(position: Int): Int { |
||||
val viewType = (this.items[position] as RowRepresentable).viewType |
||||
return if (viewType != -1) viewType else RowViewType.DATA.ordinal |
||||
} |
||||
|
||||
override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { |
||||
|
||||
when (this.dataType) { |
||||
LiveData.FILTER -> { |
||||
val intent = Intent() |
||||
intent.putExtra(FiltersActivity.IntentKey.FILTER_ID.keyName, (row as Filter).id) |
||||
activity?.setResult(Activity.RESULT_OK, intent) |
||||
activity?.finish() |
||||
} |
||||
else -> { |
||||
val identifier = (row as Identifiable).id |
||||
EditableDataActivity.newInstanceForResult(this, this.dataType, identifier, REQUEST_CODE_DETAILS) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Update UI |
||||
*/ |
||||
fun updateUI(showAddButton: Boolean) { |
||||
this.addButton.isVisible = showAddButton |
||||
} |
||||
|
||||
/** |
||||
* Filter the items list with the given search content |
||||
*/ |
||||
private fun filterItemsWithSearch(searchContent: String?) { |
||||
this.items = getRealm().find(this.identifiableClass, searchContent) |
||||
dataListAdapter.notifyDataSetChanged() |
||||
} |
||||
|
||||
} |
||||
@ -1,122 +0,0 @@ |
||||
package net.pokeranalytics.android.ui.fragment |
||||
|
||||
import android.app.Activity |
||||
import android.content.Context |
||||
import android.content.Intent |
||||
import io.realm.RealmResults |
||||
import net.pokeranalytics.android.model.LiveData |
||||
import net.pokeranalytics.android.model.interfaces.Deletable |
||||
import net.pokeranalytics.android.model.interfaces.Identifiable |
||||
import net.pokeranalytics.android.model.realm.Filter |
||||
import net.pokeranalytics.android.ui.activity.EditableDataActivity |
||||
import net.pokeranalytics.android.ui.activity.FiltersActivity |
||||
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetFragment |
||||
import net.pokeranalytics.android.ui.interfaces.FilterHandler.Companion.INTENT_FILTER_UPDATE_FILTER_UI |
||||
import net.pokeranalytics.android.ui.view.RowRepresentable |
||||
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor |
||||
import net.pokeranalytics.android.ui.view.RowViewType |
||||
import net.pokeranalytics.android.util.Preferences |
||||
import timber.log.Timber |
||||
|
||||
|
||||
open class FiltersListFragment : DataListFragment() { |
||||
|
||||
private var identifiableClass: Class<out Deletable> = Filter::class.java |
||||
private var dataType: LiveData = LiveData.FILTER |
||||
private lateinit var items: RealmResults<Filter> |
||||
|
||||
/** |
||||
* Set fragment data |
||||
*/ |
||||
override fun setData(dataType: Int) { |
||||
super.setData(dataType) |
||||
|
||||
this.dataType = LiveData.FILTER |
||||
this.identifiableClass = Filter::class.java |
||||
setToolbarTitle(this.dataType.pluralLocalizedTitle(requireContext())) |
||||
this.items = this.retrieveItems(getRealm()) as RealmResults<Filter> |
||||
} |
||||
|
||||
override fun rowRepresentableForPosition(position: Int): RowRepresentable? { |
||||
Timber.d("rowRepresentableForPosition: ${this.items[position] as RowRepresentable}") |
||||
return this.items[position] as RowRepresentable |
||||
} |
||||
|
||||
override fun numberOfRows(): Int { |
||||
return this.items.size |
||||
} |
||||
|
||||
override fun adapterRows(): List<RowRepresentable>? { |
||||
return items |
||||
} |
||||
|
||||
override fun viewTypeForPosition(position: Int): Int { |
||||
val viewType = (this.items[position] as RowRepresentable).viewType |
||||
return if (viewType != -1) viewType else RowViewType.DATA.ordinal |
||||
} |
||||
|
||||
override fun editDescriptors(row: RowRepresentable): ArrayList<RowRepresentableEditDescriptor>? { |
||||
return when (row) { |
||||
is Filter -> row.editingDescriptors(mapOf("defaultValue" to row.name)) |
||||
else -> super.editDescriptors(row) |
||||
} |
||||
} |
||||
|
||||
override fun onRowValueChanged(value: Any?, row: RowRepresentable) { |
||||
when (row) { |
||||
is Filter -> { |
||||
row.updateValue(value, row) |
||||
dataListAdapter.refreshRow(row) |
||||
updateFilterUIIfNecessary(requireContext(), row.id) |
||||
} |
||||
else -> super.onRowValueChanged(value, row) |
||||
} |
||||
} |
||||
|
||||
override fun onRowDeleted(row: RowRepresentable) { |
||||
when (row) { |
||||
is Filter -> { |
||||
val filterId = row.id |
||||
deleteItem(dataListAdapter, items, filterId) |
||||
if (filterId == Preferences.getActiveFilterId(requireContext())) { |
||||
Preferences.setActiveFilterId("", requireContext()) |
||||
updateFilterUIIfNecessary(requireContext(), "") |
||||
} |
||||
} |
||||
else -> super.onRowDeleted(row) |
||||
} |
||||
} |
||||
|
||||
override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) { |
||||
when (row) { |
||||
is Filter -> { |
||||
if (fromAction) { |
||||
val data = row.editingDescriptors(mapOf("defaultValue" to row.name)) |
||||
BottomSheetFragment.create(fragmentManager, row, this, data, false, isDeletable = true, valueHasPlaceholder = false) |
||||
} else { |
||||
val intent = Intent() |
||||
intent.putExtra(FiltersActivity.IntentKey.FILTER_ID.keyName, row.id) |
||||
activity?.setResult(Activity.RESULT_OK, intent) |
||||
activity?.finish() |
||||
} |
||||
} |
||||
else -> { |
||||
val identifier = (row as Identifiable).id |
||||
EditableDataActivity.newInstanceForResult(this, this.dataType, identifier, REQUEST_CODE_DETAILS) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Update filter UI |
||||
*/ |
||||
fun updateFilterUIIfNecessary(context: Context, filterId: String) { |
||||
if (filterId == Preferences.getActiveFilterId(context)) { |
||||
// Send broadcast |
||||
val intent = Intent() |
||||
intent.action = INTENT_FILTER_UPDATE_FILTER_UI |
||||
context.sendBroadcast(intent) |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -1,4 +1,4 @@ |
||||
package net.pokeranalytics.android.ui.activity |
||||
package net.pokeranalytics.android.ui.modules.bankroll |
||||
|
||||
import android.content.Context |
||||
import android.content.Intent |
||||
@ -1,4 +1,4 @@ |
||||
package net.pokeranalytics.android.ui.viewmodel |
||||
package net.pokeranalytics.android.ui.modules.calendar |
||||
|
||||
import androidx.lifecycle.ViewModel |
||||
import net.pokeranalytics.android.calculus.ComputedResults |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue