PokerSnowie Mobile App Docs
Table of Contents
Application Entry Components
1.1. MainActivity
1.2. SnowieApp
1.3. SharedViewModel
Socket and API Code
2.1. Overview
2.2. Utilities
2.3. Exceptions
2.4. Main API Class
2.5 Additional Classes
Engine
3.1. Overview
3.2. AnimationsEngine
3.3. AnteAction
3.4. BettingAction
3.5. BlindAction
3.6. Card
3.7. DealAction
3.8. EngineDeck
3.9. MoveManager
3.10. PlayerRank
3.11. PokerAction
3.12. PokerHand
3.13. PokerPlayer
3.14. PokerRound
3.15. Showdowns
3.16. Winners
UI Components Documentation
4.1. Navigation
4.2. Components
4.3. Constants
4.4. Templates
4.5. UI Theme
4.6. Utils
Application Entry Components
MainActivity
File:
MainActivity.kt
Purpose: The main entry point of the application, responsible for initializing the UI and setting up default values.
Usage:
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Initialization code here } }Details:
onCreate: Initializes shared preferences, sets default button values if necessary, and sets the content view to the
Routercomposable.setDefaultButtonValues: Ensures that the selected buttons have default values if not already set in shared preferences. These buttons are the ones seen in the Raise/Call/Fold Bar in a game setting.
Example:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val sharedPrefs = getSharedPreferences("USER_PREFERENCES", Context.MODE_PRIVATE) val currentSelectedString = sharedPrefs.getString("selected_buttons", "") val currentSelected = if (currentSelectedString.isNullOrBlank()) { emptyList() } else { currentSelectedString.split(",") } if (currentSelected.size < 4) { setDefaultButtonValues(this, currentSelected) } setContent { val viewModelStoreOwner = this@MainActivity Router(viewModelStoreOwner) } }
SnowieApp
Development DocumentationDevelopment DocumentationDevelopment DocumentationFile:
SnowieApp.ktPurpose: Initializes the application-level configurations and the PokerSnowieAPI instance.
Usage:
class SnowieApp : Application(), CoroutineScope by MainScope() { override fun onCreate() { super.onCreate() // Initialization code here } }Details:
onCreate: Reads configuration properties, initializes the PokerSnowieAPI instance, and sets default shared preferences.
initializeDefaultPreferences: Ensures default values are set in shared preferences if not already present.
Example:
override fun onCreate() { super.onCreate() config = Config(this) val builder = PokerSnowieAPI.Builder() pokerSnowieAPI = builder .host(config.host) .port(config.port) .salt(config.salt) .build() initializeDefaultPreferences(this) }Companion Object:
pokerSnowieAPI: Holds the instance of
PokerSnowieAPIisFreeTrial: Manages the free trial state using a
StateFlow
SharedViewModel
File:
SharedViweModel.ktPurpose: Manages shared state and events across different parts of the application.
Usage:
class SharedViewModel : ViewModel() { // State and event management code here }Details:
State Management: Manages various states like selected mode, seat number, scores, etc.
Events: Uses
StateFlowto manage events like blinds posted, cards dealt, human move notifications, etc.Methods: Provides methods to trigger events, e.g.,
postBlinds,dealCards,requestHumanMove
Example:
fun postBlinds() { viewModelScope.launch { blindsPostedEvent.value = ConsumableEvent(Unit) } } fun dealCards() { viewModelScope.launch { cardsDealtEvent.value = ConsumableEvent(Unit) } }StateFlow Events:
blindsPosted: Triggered when blinds are posted.
cardsDealt: Triggered when cards are dealt.
humanMoveNotification: Triggered when a human move is requested.
evaluationSessionError: Triggered when there's an error evaluating the session.
operationLimit: Triggered when the operation limit is reached.
moveNotification: Triggered when a move is played.
showdownNotification: Triggered when the showdown starts.
winnerNotification: Triggered when the winner is computed.
Socket and API Code
Overview
This section covers the com.pokersnowie.client.socket package, which manages the communication between the Android client and the PokerSnowie backend services. This includes configuration management, utility functions, exception handling, and the main API class that handles the connection and data exchange.
Utilities
Config
File:
socket/util/Config.ktPurpose: The `Config` class reads configuration properties from a file and provides values such as host, port, and salt for the application.
Usage:
val config = Config(context) val host = config.host val port = config.port val salt = config.saltDetails:
Properties:
host,port,saltInitialization: Reads
config.propertiesfrom the assets folder.Exception Handling: Throws
RuntimeExceptionif the configuration file cannot be read.
Md5
File:
socket/util/Md5.ktPurpose: The
Md5object provides a utility function to compute the MD5 checksum of a given string.Usage:
val checksum = Md5.checksum("some_string")Details:
Method:
checksum(text: String): StringComputes and returns the MD5 hash of the input string.
Throws
NoSuchAlgorithmExceptionorIOException
Exceptions
APIException
File:
socket/APIException.ktPurpose: Represents general exceptions related to the API.
Usage:
throw APIException("Error message")Details: Provides constructors to create exceptions with a message, cause, or both.
AuthException
File:
socket/AuthException.ktPurpose: Represents authentication-related exceptions.
Usage:
throw AuthException("Authentication failed")Details: Similar to
APIExceptionbut specifically for authentication issues.
ConnectionException
File:
socket/ConnectionException.ktPurpose: Represents connection-related exceptions.
Usage:
throw ConnectionException("Connection failed")Details: Used to signal issues related to establishing or maintaining a network connection.
Main API Class
PokerSnowieAPI
Overview
The PokerSnowieAPI class manages all socket-based communication with the PokerSnowie backend. It handles connection setup, user authentication, and various API calls for poker operations. The main functionalities include connecting to the server, logging in, starting a new hand, evaluating moves, and retrieving possible moves for a poker situation.
Let's improve the documentation with a detailed explanation of the complex parts, focusing on how the registry, requests, declarations, messages, byte sizes, buffers, and input/output streams work. Here’s the improved version:
PokerSnowieAPI
Overview
The PokerSnowieAPI class manages all socket-based communication with the PokerSnowie backend. It handles connection setup, user authentication, and various API calls for poker operations. The main functionalities include connecting to the server, logging in, starting a new hand, evaluating moves, and retrieving possible moves for a poker situation.
Class Components
Builder Class
Purpose: Helps in constructing the
PokerSnowieAPIinstance with necessary configurations such as host, port, and salt.Usage:
val api = PokerSnowieAPI.Builder() .host("example.com") .port(443) .salt("somesalt") .build()
Connection Management
Purpose: Establishes and maintains a secure SSL connection using SSLSocket.
Details:
SSLContext Initialization: Creates an SSL context and initializes it with trust managers to trust all certificates.
Socket Creation: Establishes a socket connection using the configured host and port.
Input/Output Streams: Initializes
DataInputStreamfor input andOutputStreamfor output.
val sc = SSLContext.getInstance("TLS") sc.init(null, trustAllCerts, SecureRandom()) val factory = sc.socketFactory as SSLSocketFactory socket = factory.createSocket(host, port) as SSLSocket inputStream = DataInputStream(socket?.inputStream) outputStream = socket?.outputStream
Key Methods
1. connect()
Description: Establishes the SSL connection.
Throws:
ConnectionExceptionon failure.Example:
@Throws(ConnectionException::class) suspend fun connect() = withContext(Dispatchers.IO) { // SSL connection setup code }
2. login()
Description: Authenticates the user by sending a login request and processing the response.
Parameters:
username: The username for authentication.
password: The password for authentication.
sid: Session ID.
umi: Unique Machine Identifier.
context: Application context.
Implementation:
Password Hashing: Generates an MD5 hash of the password combined with a salt.
val passwordHash: String = Md5.checksum(salt + password)Request Building: Constructs the login request using protocol buffers.
val request = Login.Request.newBuilder() .setUsername(username) .setPassword(passwordHash) .setSid(sid) .setUmi(umi) .build()Message and Declaration: Wraps the request in a message and declaration.
val message = MessageOuterClass.Message.newBuilder().setExtension(Login.req, request).build() val declaration = DeclarationOuterClass.Declaration.newBuilder() .setExtension(MessageOuterClass.msg, message) .build()Sending Request: Writes the serialized size and declaration to the output buffer and sends it.
val size = declaration.serializedSize val sizeBytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(size).array() outputBuffer.reset() outputBuffer.write(sizeBytes) declaration.writeTo(outputBuffer) writeBufferToStream()Reading Response: Reads and parses the response to update login status.
val declaration = readResponse(registry) val message = declaration.getExtension(MessageOuterClass.msg) val response = message.getExtension(Login.res)
3. startNewHand()
Description: Starts a new poker hand by sending a request and processing the response.
Parameters:
sharedViewModel: ViewModel to update the UI state.
context: Application context.
Implementation:
Request Building: Constructs the request for a new hand.
val request = Ids.Request.newBuilder().setCategory(CategoryOuterClass.Category.Session).setSize(1).build()Sending Request and Reading Response: Similar to the
login()method.Parsing Response: Extracts the hand ID and deck of cards from the response.
val handData = response.getId(0) val handId = handData.serial for (card in handData.deck.cardList) { cards.add(EngineCard(card.suit.number, card.rank.number)) }Error Handling: Checks for errors in the response and updates the ViewModel.
if (response.idCount != 1) { throw IOException("Failed to get hand ID and deck.") }
4. evaluateMove()
Description: Evaluates a move in the poker game by sending a request and processing the response.
Parameters:
move: The move to evaluate.
amount: The amount associated with the move.
pHand: The current poker hand.
sharedViewModel: ViewModel to update the UI state.
context: Application context.
Implementation:
Request Building: Constructs the request for evaluating the move.
val movePlayed = Decision.Request.Played.newBuilder().setMove(move).setAmount(amount.toFloat()).build() val decisionReq = Decision.Request.newBuilder() .setSituation(getPokerSituation(pHand).situation) .setCategory(CategoryOuterClass.Category.Session) .setHand(hand) .setPlayed(movePlayed) .build()Sending Request and Reading Response: Similar to the
login()method.Parsing Response: Extracts evaluation results and updates the ViewModel.
val protoResult: EvalResultOuterClass.EvalResult = response.evaluation sharedViewModel.evaluateMoveData.errorRate = protoResult.level.error.action loadProbs(plyLevel, protoResult, sharedViewModel.evaluateMoveData.playerProbs)
5. getMovesFor()
Description: Retrieves possible moves for the current poker situation.
Parameters:
navController: Navigation controller to manage navigation.
pokerHand: The current poker hand.
sharedViewModel: ViewModel to update the UI state.
moveManager: Manages moves in the poker game.
context: Application context.
Implementation:
Request Building: Constructs the request for getting possible moves.
val pokerSituationObj: PokerSituationObject = getPokerSituation(pokerHand) val request = Consecutive.Request.newBuilder() .setSituation(pokerSituationObj.situation) .setStop(pokerSituationObj.humanPosition) .setHand(hand) .build()Sending Request and Reading Response: Similar to the
login()method.Parsing Response: Extracts possible moves from the response and updates the ViewModel.
for (eval in response.evaluation.evalsList) { sharedViewModel.consecutiveEvalData.evals.add(Move(eval.random.move, eval.random.amount)) }
Helper Methods
readResponse()
Purpose: Reads the response from the input stream and parses it using the provided registry.
Details:
Buffer Management: Uses an input buffer to accumulate bytes read from the stream until the entire message is received.
Message Parsing: Parses the accumulated bytes into a
Declarationobject.
private fun readResponse(registry: ExtensionRegistry): DeclarationOuterClass.Declaration { inputBuffer.reset() val bufferSize = 2048 val tempBuffer = ByteArray(bufferSize) var totalBytesRead = 0 var size = -1 while (true) { val bytesRead = inputStream?.read(tempBuffer) ?: -1 if (bytesRead == -1) { break } inputBuffer.write(tempBuffer, 0, bytesRead) totalBytesRead += bytesRead if (size == -1 && totalBytesRead >= 4) { val sizeBytes = inputBuffer.toByteArray().sliceArray(0 until 4) size = ByteBuffer.wrap(sizeBytes) .order(ByteOrder.LITTLE_ENDIAN) .int } if (size != -1 && totalBytesRead >= size + 4) { break } } val messageBytes = inputBuffer.toByteArray().sliceArray(4 until totalBytesRead) return DeclarationOuterClass.Declaration.parseFrom(messageBytes, registry) }
writeBufferToStream()
Purpose: Writes the buffered output data to the output stream.
Details:
Buffer Management: Converts the buffered data to bytes and writes them to the output stream.
private suspend fun writeBufferToStream() { withContext(Dispatchers.IO) { outputBuffer.toByteArray().let { data -> try { outputStream?.write(data) outputStream?.flush() outputBuffer.reset() } catch (e: IOException) { throw IOException(e.message.toString()) } } } }
loadProbs()
Purpose: Loads probabilities from the evaluation result into a list.
Details:
Probability Extraction: Iterates over the result and extracts probabilities for each move.
private fun loadProbs(level: Int, protoResult: EvalResultOuterClass.EvalResult, probs: MutableList<Float>) { probs.clear() for (i in 0..2) { val value = protoResult.valueList[level * 3 + i] probs.add(value.prob) } }
getPokerSituation()
Purpose: Constructs the poker situation object from the current poker hand.
Details:
Player Initialization: Initializes players with their respective hole cards and stack amounts.
Community Cards: Adds community cards to the situation.
Actions: Adds actions taken in the current hand to the situation.
private fun getPokerSituation(pokerHand: PokerHand): PokerSituationObject { // Setting blinds val blind = PokerSituation2.Blind.newBuilder() blind.large = pokerHand.getBigBlindAmount()!!.toFloat() / 1000.0f blind.small = pokerHand.getSmallBlindAmount()!!.toFloat() / 1000.0f // Initialize the PokerSituation2 val situation = PokerSituation2.newBuilder() .setBlind(blind) .setPlayers(pokerHand.getNumberOfPlayer()) .setHeadsUp(if (pokerHand.headsUp) 1 else 0) .setAnte(pokerHand.anteAmount.toFloat() / 1000.0f) // Initialize players and add community cards // ... return PokerSituationObject(situation.build(), humanPosition) }
Additional Classes
PokerSnowieHand
File:
socket/PokerSnowieHand.ktPurpose: Represents a poker hand with a unique ID and a deck of cards.
Usage:
val hand = PokerSnowieHand(handId, deck)Details:
Properties:
id,deckConstructors: Initializes a poker hand with ID and deck of cards.
Reachability
File:
socket/Reachability.ktPurpose: Monitors network connectivity status.
Usage:
val reachability = Reachability.getInstance(context) val isNetworkAvailable = reachability.getNetworkStatus().valueDetails:
NetworkCallback: Listens for network changes and updates the status.
StateFlow: Provides a flow of network status updates.
Engine
AnimationsEngine
File:
AnimationsEngine.ktPurpose: Manages animations and sound effects during the game.
Detailed Explanation: The
AnimationsEngineclass is responsible for handling animations and sounds in a queued manner to ensure a smooth user experience. This class includes logic to play sounds, manage the animation queue, and handle the completion of animations.Core Components:
Data Class
Animation:Properties:
animation: A lambda function representing the animation logic.completion: A lambda function called upon completion of the animation.sound: Optional sound associated with the animation.soundType: Type of the sound file (default is "mp3").shouldComplete: A lambda function returning a Boolean to determine if the animation should complete.duration: Duration of the animation.context: Android context for resource access.label: A label for identifying the animation.
Queue Management:
Queue: A queue (
LinkedList) to manage the animations.Flags: Boolean flags to manage the state of animations and sounds.
Methods:
addAnimation(value: Animation):Adds an animation to the queue and starts running animations if none are running.
runAnimations():Runs animations from the queue one by one. Handles sound playback and animation execution.
playSound(context: Context, soundResId: Int, times: Int):Plays a sound file a specified number of times.
reset():Resets the engine by stopping all animations and sounds.
r
eleaseMediaPlayer():Releases the media player resources.
Example Usage:
val animation = Animation( animation = { /* Animation logic */ }, completion = { success -> /* Completion logic */ }, sound = "dealCards", duration = 150, context = context, label = "Deal Cards Animation" ) animationsEngine.addAnimation(animation)
AnteAction
File:
AnteAction.ktPurpose: Represents an ante action taken by a player.
Detailed Explanation: The
AnteActionclass models an ante (a forced bet) in poker, specifying which player posted the ante and the amount posted.Core Components:
Properties:
player: The player who posted the anteamount: The amount of the ante
Methods:
deepCopy():Creates a deep copy of the
AnteActioninstance
toString()Provides a string representation of the
AnteAction.
Example Usage:
val anteAction = AnteAction(player, 100) val anteActionCopy = anteAction.deepCopy()
BettingAction
File:
BettingAction.ktPurpose: Represents a betting action taken by a player.
Detailed Explanation: The
BettingActionclass models a betting action in poker, specifying the type of move, the player making the move, the amount bet, and the total amount in the pot after the bet.Core Components:
Properties:
move: The type of move (Fold, CheckCall, BetRaise, Uncalled).player: The player making the move.amountTo: The total amount in the pot after the bet.amountBy: The amount bet in this action.
Methods:
deepCopy():Creates a deep copy of the
BettingActioninstance.
toString():Provides a string representation of the
BettingAction.
getAIMove():Returns an integer representing the AI move type (0 for Fold, 1 for Check/Call, 2 for Bet/Raise).
Example Usage:
val bettingAction = BettingAction(player, MoveType.BetRaise, 500, 100) val bettingActionCopy = bettingAction.deepCopy()
BlindAction
File:
BlindAction.ktPurpose: Represents a blind action taken by a player.
Detailed Explanation: The
BlindActionclass models a blind bet in poker, specifying the type of blind (small, big, extra), the player posting the blind, and the amount posted.Core Components:
Properties:
type: The type of blind (SmallBlind, BigBlind, ExtraBlind).amount: The amount of the blind.player: The player posting the blind.
Methods:
deepCopy():Creates a deep copy of the
BlindActioninstance.
getBlindName():Returns the name of the blind type.
toString():Provides a string representation of the
BlindAction.
Example Usage:
val blindAction = BlindAction(player, BlindType.BigBlind, 200) val blindActionCopy = blindAction.deepCopy()
Card
File:
Card.ktPurpose: Defines card ranks and suits, and provides a class for handling a single card.
Detailed Explanation: The
EngineCardclass models a playing card, including its rank and suit. It provides methods for creating, copying, and representing cards.Core Components:
Enums:
CardRank: Defines card ranks (Ace, 2, 3, ..., King).CardSuit: Defines card suits (Club, Diamond, Heart, Spade).
Properties:
suit: The suit of the card.rank: The rank of the card.
Methods:
Constructors:
EngineCard(cSuit: CardSuit, crank: CardRank): Creates a card with specified suit and rank.EngineCard(cSuit: Int, cRank: Int): Creates a card from integer values (used for protobuf conversion).
deepCopy():Creates a deep copy of the card.
toString():Provides a string representation of the card.
Utility Methods:getCardRank(),getCardSuit(),getCardName(), etc.: Methods for accessing card properties.
Example Usage:
val card = EngineCard(CardSuit.Heart, CardRank.Ace) val cardCopy = card.deepCopy()
DealAction
File:
DealAction.ktPurpose: Represents a deal action during the game.
Detailed Explanation: The
DealActionclass and its subclasses (HoleDealAction,CommunityAction) model the dealing of cards during different stages of the game.Core Components:
DealAction Class:
Methods:
getCards(): Returns a list of cards dealt.deepCopy(): Creates a deep copy of the deal action.toString(): Provides a string representation of the deal action.
HoleDealAction Class:
Represents dealing hole cards to a player.
Properties:
player,card1,card2Methods:
deepCopy(): Creates a deep copy of the hole deal action.toString(): Provides a string representation of the hole deal action.
CommunityAction Class:
Represents dealing community cards.
Properties:
internalCardsMethods:
getCards(): Returns the list of community cards.deepCopy(): Creates a deep copy of the community action.toString(): Provides a string representation of the community action.
Example Usage:
val dealAction = DealAction() val dealActionCopy = dealAction.deepCopy() val holeDealAction = HoleDealAction(player, card1, card2) val holeDealActionCopy = holeDealAction.deepCopy() val communityAction = CommunityAction(listOf(card1, card2, card3)) val communityActionCopy = communityAction.deepCopy()
EngineDeck
File:
Deck.ktPurpose: Manages the deck of cards for the game.
Detailed Explanation:
The
EngineDeckclass models a deck of cards, providing methods for deep copying the deck and drawing cards from it.Core Components:
Properties:
cards: A mutable list ofEngineCardobjects representing the deck.
Methods:
deepCopy():Creates a deep copy of the deck.
toString():Provides a string representation of the deck.
takeCard():Draws a card from the top of the deck.
Example Usage:
val deck = EngineDeck() val deckCopy = deck.deepCopy() val drawnCard = deck.takeCard()
MoveManager
File:
MoveManager.ktPurpose: Manages player moves during the game.
Detailed Explanation:
The
MoveManagerclass handles the logic for requesting and processing player moves (both human and AI) during a poker hand.Core Components:
Properties:
movesAI: List of AI moves.movesHuman: List of human moves.pokerHand: The current poker hand.viewModel: Shared view model for the application.coroutineScope: Coroutine scope for asynchronous operations.
Methods:
deepCopy():Creates a deep copy of the
MoveManager.
clean():Clears the move lists.
getMove():Retrieves the next move for a player.
performRequestMove():Requests a move from the player (either GUI for human or API for AI).
consecResponseReceived():Handles the response for consecutive evaluations.
Example Usage:
val moveManager = MoveManager(pokerHand, viewModel, coroutineScope) val moveManagerCopy = moveManager.deepCopy(pokerHandCopy, coroutineScope) moveManager.clean() val nextMove = moveManager.getMove(player, context, navController)
PlayerRank
File:
PlayerRank.ktPurpose: Represents a player's rank and associated cards during a showdown.
Detailed Explanation:
The
PlayerRankclass models a player's rank during a showdown, including the cards that form the rank and the player's position.Core Components:
Properties:
player: The player associated with the rank.cards: The cards that form the rank.rank: The hand rank (e.g., HighCard, OnePair, etc.).position: The player's position in the ranking.
Methods:
deepCopy():Creates a deep copy of the
PlayerRank.
rankWeight():Returns the weight of the hand rank for comparison.
Example Usage:
val playerRank = PlayerRank() val playerRankCopy = playerRank.deepCopy()
PokerAction
File: PokerAction.kt
Purpose: Represents a generic poker action.
Detailed Explanation:
The
PokerActionclass models a generic action in poker, including the player making the action and the type of move.Core Components:
Properties:
pokerPlayer: The player making the action.move: The type of move (Fold, CheckCall, BetRaise, Uncalled).
Example Usage:
val pokerAction = PokerAction() pokerAction.pokerPlayer = player pokerAction.move = MoveType.BetRaise
PokerHand
File: PokerHand.kt
Purpose: Represents a complete poker hand, managing all actions, players, and rounds.
Detailed Explanation:
The
PokerHandclass is the core representation of a poker hand, managing all aspects of the game, including player actions, rounds, and determining the outcome.Core Components:
Properties:
ante: List of ante actions.blinds: List of blind actions.rounds: List of poker rounds (PreFlop, Flop, Turn, River).players: List of players.dealer: Index of the dealer.humanFolded: Boolean indicating if the human player folded.minStake: Minimum stake (small blind).maxStake: Maximum stake (big blind).anteAmount: Ante amount.showdowns: Showdown results.winners: Winners of the hand.deck: Deck of cards.moveManager: Move manager for handling player moves.headsUp: Boolean indicating if the game is heads-up (2 players).handIdSerial: Unique identifier for the hand.
Methods:
setHandInfo():Initializes a new hand with the provided parameters.
deepCopy():Creates a deep copy of the poker hand.
getCommunityCards():Returns all dealt community cards.
getCurrentRound():Returns the current poker round.
getTotalAmount():Computes the total pot amount.
getNextPlayer():Returns the next player based on the current player and status.
getPlayedAmount():Returns the total amount played by a specific player.
getSmallBlind():Returns the small blind player.
getBigBlind():Returns the big blind player.
isTerminated():Checks if the hand is terminated.
countPlayers():Counts the number of players with a specific status.
getCurrentAmount():Returns the current amount held by a player.
postBlinds():Posts the blinds for the current hand.
postAnte():Posts the ante for the current hand.
getFirstPlayerToShow():Returns the first player to show their cards.
getActivePlayer():Returns the active player.
getMoves():Returns all moves of a specific type.
getMoveName():Returns the name of a move based on its ID.
printHand():Prints the hand information as a string.
Example Usage:
val pokerHand = PokerHand(viewModel, coroutineScope) pokerHand.setHandInfo(deckAI, playerList, dealerPos, handId, smallBlind, bigBlind, ante) val pokerHandCopy = pokerHand.deepCopy(coroutineScope) val communityCards = pokerHand.getCommunityCards() val totalAmount = pokerHand.getTotalAmount()
PokerPlayer
File:
PokerPlayer.ktPurpose: Represents a player in the poker game.
Detailed Explanation:
The
PokerPlayerclass models a player, including their name, starting amount, and status. It has two subclasses:HumanPokerPlayerandAIPokerPlayer.Core Components:
Enums:
GameStatus: Defines player statuses (Playing, Allin, Folded).
Properties:
name: Player's name.startingAmount: Player's starting amount.status: Player's status.
Methods:
deepCopy():Creates a deep copy of the player.
toString():Provides a string representation of the player.
Subclasses:
HumanPokerPlayer: Represents a human player.AIPokerPlayer: Represents an AI player.
Example Usage:
val player = PokerPlayer() player.name = "Player1" player.startingAmount = 1000 val playerCopy = player.deepCopy()
PokerRound
File: PokerRound.kt
Purpose: Represents a round in the poker game.
Detailed Explanation:
The
PokerRoundclass and its subclasses (PreFlop,Flop,Turn,River) model a round of poker, including actions and deals.Core Components:
Properties:
deals: List of deal actions.actions: List of betting actions.pokerHand: The current poker hand.viewModel: Shared view model for the application.
Methods:
deepCopy():Creates a deep copy of the poker round.
isTerminated():Checks if the round is terminated.
getRoundAmount():Returns the total amount bet by a player in the round.
getPlayerToMove():Returns the player to move next.
next():Advances to the next action in the round.
executeDeals():Executes the dealing of cards for the round.
getFirstPlayerToShow():Returns the first player to show their cards.
Subclasses:
PreFlop: Represents the pre-flop round.Flop: Represents the flop round.Turn: Represents the turn round.River: Represents the river round.
Example Usage:
val pokerRound = PokerRound(pokerHand, viewModel) val pokerRoundCopy = pokerRound.deepCopy(pokerHandCopy) pokerRound.next(context, navController)
Showdowns
File:
Showdowns.ktPurpose: Computes the showdown rankings for players.
Detailed Explanation:
The
Showdownsclass models the showdown phase, where player ranks are determined based on their hands.Core Components:
Properties:
playerRanks: List of player ranks.pokerHand: The current poker hand.community: Community cards.
Methods:
deepCopy():Creates a deep copy of the showdowns.
computeRank():Computes the rank for a player's hand.
addShowDown():Adds a player to the showdown rankings.
setPositions():Sets the positions for the player rankings.
Example Usage:
val showdowns = Showdowns(pokerHand) showdowns.addShowDown(player) showdowns.setPositions() val showdownsCopy = showdowns.deepCopy(coroutineScope)
Winners
File:
Winners.ktPurpose: Determines the winners and their respective pots in the poker game.
Detailed Explanation:
The
Winnersclass models the computation of pots and the determination of winners in the poker game.Core Components:
Properties:
mainPot: The main pot.sidePots: List of side pots.
Methods:
deepCopy():Creates a deep copy of the winners.
computePots():Computes the pots based on player bets.
addWinners():Adds winners to the pots.
getWonAmount():Returns the amount won by a player.
Example Usage:
val winners = Winners(pokerHand) winners.computePots(pokerRound) winners.addWinners() val wonAmount = winners.getWonAmount(player) val winnersCopy = winners.deepCopy(pokerHandCopy)
UI Components Documentation
Navigation
The navigation directory contains the navigation logic for the application, including the Router, which manages navigation between different screens.
Router.kt
File:
Router.ktPurpose:
The
Routercomposable function sets up the navigation graph and handles navigation transitions within the PokerSnowie application.Detailed Explanation:
The
Routerfunction initializes the navigation controller and sets up the navigation graph using Jetpack Compose Navigation. It defines the various routes and their corresponding composable screens.Core Components:
RouteEnum:Defines the different routes available in the application.
enum class Route { Login, Home, Options, OptionsList, OptionsSelect, ChallengeSetup, TrainingSetup, Scoreboard, GameController, }RouterComposable Function:Sets up the navigation graph and handles transitions.
@Composable fun Router(viewModelStoreOwner: ViewModelStoreOwner) { val navController = rememberNavController() val navigateTo = { routeDestination: Route -> navController.navigate(routeDestination.name) } NavHost(navController, startDestination = Route.Login.name) { composable(Route.Login.name) { Login(navigateTo, viewModelStoreOwner) } composable(Route.Home.name) { Home(navigateTo, viewModelStoreOwner) } // Define other routes similarly } }Navigation Transitions:
Defines common enter and exit transitions for navigation.
fun commonEnterTransition() = slideInHorizontally(initialOffsetX = { it }) fun commonExitTransition() = slideOutHorizontally()State Management:
Use state to manage loading spinner and error dialogue visibility,
var showSpinner by remember { mutableStateOf(false) } var showSocketError by remember { mutableStateOf(false) }Handling Loading and Errors:
Displays a loading spinner and error dialog based on the application state.
Box(modifier = Modifier.fillMaxSize()) { if (showSpinner) { CircularProgressIndicator() } if (showSocketError) { CustomAlertDialog() } }
Example Usage:
@Composable fun MainScreen(viewModelStoreOwner: ViewModelStoreOwner) { Router(viewModelStoreOwner) }
Pages
LoginPage
Purpose:
The Login.kt file defines the login page for the application. This page allows users to log in using their email and password. It includes form validation, error handling, and navigation to the Home screen upon successful login.
Key Functionalities
User Authentication:
Connects to the
PokerSnowieAPIto perform user loginHandles authentication state changes, displaying messages or navigating to the home screen upon successful login.
Form Validation:
Validate email and password inputs.
Displays alert dialogues for invalid inputs or errors.
State Management:
Uses Jetpack Compose state management for form inputs and loading states.
Utilizes
SharedViewModelto share data across composables.
Complex Parts:
Handling Login Status Changes:
The
LaunchedEffectblock reacts to changes in theloginStatus,tooManySessions, and other states from thePokerSnowieAPI.Example:
LaunchedEffect(loginStatus) { if (loginStatus == PokerSnowieAPI.LoginStatus.Logged) { if (hasAnActiveSubscription == true) { navigateTo(Route.Home) } else { runBlocking { SnowieApp.pokerSnowieAPI.disconnect() } showAlertDialog("You have no active subscription", "Check out our different subscriptions and pick yours!") } } else if (tooManySessions == true) { runBlocking { SnowieApp.pokerSnowieAPI.disconnect() } showAlertDialog("Maximum number of simultaneous connections reached.", "Please try again in a few minutes.") } else if (loginStatus == PokerSnowieAPI.LoginStatus.None) { runBlocking { SnowieApp.pokerSnowieAPI.disconnect() } showAlertDialog("Login failed", "Please try again or contact our customer support.") } signInShouldEnable.value = true isLoading = false }
Asynchronous Operations with Coroutines:
Uses
CoroutineScopeto perform network operations asynchronously within the button click handler.Example:
GenericButton( //... onClick = { keyboardController?.hide() if (emailText.trim().isEmpty()) { showAlertDialog("Missing username.", "Please enter your email.") } else if (passwordText.trim().isEmpty()) { showAlertDialog("Missing password.", "Please enter your password.") } else { signInShouldEnable.value = false isLoading = true CoroutineScope(Dispatchers.Main).launch { try { SnowieApp.pokerSnowieAPI.connect() SnowieApp.pokerSnowieAPI.login(emailText, passwordText, "", uuid, context) } catch (e: Exception) { signInShouldEnable.value = true isLoading = false Log.e("SnowieApp", "PokerSnowieAPI connection/login failure", e) } } } } )
Home Page
File: pages/Home.kt
Purpose: The Home page serves as the main landing page after a user logs in. It provides navigation to different sections such as Challenge Setup and Training Setup.
Key Functionalities:
Main Navigation:
Displays the navigation bar (
NavBar) for easy access to settings and navigation.Contains the main body (
HomeBody) and footer (HomeFooter) sections.
State Management:
Uses
SharedViewModelto manage and share the application state.
Responsive UI:
Adjusts the layout and element sizes based on the device screen size using
sharedViewModel.percentageReduction.
Complex Parts:
Navigation Handling:
Integrates the
NavControllerfor handling navigation.Example:
NavBar(navigateTo = navigateTo, navController = navController, alternativeIcon = true)
Dynamic Scaling:
Adjusts UI components based on
sharedViewModel.percentageReduction.Example:
val scaleFactor = 1 - (sharedViewModel.percentageReduction / 100)
Options Page
File: pages/Options.kt
Purpose: The Options page provides settings for the application, allowing users to toggle sound settings and navigate to the OptionsList page for additional options.
Key Functionalities:
Sound Settings:
Displays a switch to enable or disable sounds.
Saves the state of the sound settings in shared preferences.
Example:
CustomSwitch( isChecked = switchState, onCheckedChange = { switchState = it setSoundsEnabled(it) } )
Navigation:
Provides a link to navigate to the
OptionsListpage for additional settings.
Complex Parts:
State Management for Switch:
Manages the state of the switch using
rememberandmutableStateOf.Example:
var switchState by remember { mutableStateOf(getOptionEnabled(soundsKey, context)) }
Shared Preferences:
Saves the state of the sound settings in shared preferences.
Example:
fun setSoundsEnabled(enabled: Boolean) { val sharedPrefs = context.getSharedPreferences("USER_PREFERENCES", Context.MODE_PRIVATE) with(sharedPrefs.edit()) { putInt(soundsKey, if (enabled) 0 else 1) apply() } }
Navigation Handling:
Handles navigation to the
OptionsListpage.Example:
.clickable { navigateTo(Route.OptionsList) }
Options List Page
File: pages/OptionsList.kt
Purpose: The OptionsList page allows users to select their preferred values for extra buttons used in the raise bar in a in-game setting.
Key Functionalities:
Display Options:
Lists available options for extra buttons.
Allows users to select an option to customize their button layout.
State Management:
Manages the state of selected buttons using
rememberandmutableStateOf.
Complex Parts:
State Management:
Manages the state of selected buttons.
Example:
val initialSelectedButtons = remember { mutableStateOf(getSelectedButtons(context)) }
Navigation Handling:
Navigates to the
OptionsSelectpage when an option is clicked.Example:
.clickable { sharedViewModel.selectedButton = buttonString navigateTo(Route.OptionsSelect) }
Resetting to Default:
Provides a link to reset the selected buttons to default values.
Example:
Text( text = buildAnnotatedString { append(OptionsData.resetText["title"] as String) addStyle( style = SpanStyle( textDecoration = TextDecoration.Underline, color = snwGreyishBrown ), start = 0, end = (OptionsData.resetText["title"] as String).length ) }, style = ButtonTypographyStyles["small"]!!, textAlign = TextAlign.Center, modifier = Modifier.clickable(onClick = { setSelectedButtons( context, listOf("1/4 POT", "1/2 POT", "1 POT", "ALL-IN") ) initialSelectedButtons.value = getSelectedButtons(context) }) )
Options Select Page
File: pages/OptionsSelect.kt
Purpose: The OptionsSelect page allows users to select a specific button value to customize their button layout.
Key Functionalities:
Display Available Options:
Lists all available button options.
Indicates the currently selected option with a checkmark.
Update Selected Option:
Allows users to update the selected option and saves it in shared preferences.
Complex Parts:
State Management:
Manages the state of the selected buttons.
Example:
var selectedButtons = getSelectedButtons(context)
Update Handling:
Updates the selected button and saves it in shared preferences.
Example:
.clickable { if (!isSelected && !selectedButtons.contains(button["button"]!!)) { selectedButtons = selectedButtons.map { if (it == sharedViewModel.selectedButton) button["button"]!! else it } sharedViewModel.selectedButton = button["button"]!! setSelectedButtons(context, selectedButtons) } }
Challenge Setup Page
File: pages/ChallengeSetup.kt
Purpose: The ChallengeSetup page allows users to configure the settings for a new challenge session, including selecting table size and challenge mode. This page prepares the app for a challenge session and initializes necessary states.
Key Functionalities:
ViewModel Initialization:
Initializes the
SharedViewModelwith default values for a new challenge session.Example:
sharedViewModel.sessionMode = SessionMode.Challenge sharedViewModel.performLiveAdvice = true sharedViewModel.gameFinished = false sharedViewModel.handsCounter = 0 sharedViewModel.score = 0f
Component Composition:
Composes the
ChallengeHeaderandChallengeSettingscomponents.Example:
Column { ChallengeHeader(navController, sharedViewModel) ChallengeSettings(navigateTo, sharedViewModel) }
Training Setup Page
File: pages/TrainingSetup.kt
Purpose: The TrainingSetup page allows users to configure settings for a new training session, including selecting table size, stakes, and other options. This page prepares the app for a training session and initializes necessary states.
Key Functionalities:
ViewModel Initialization:
Initializes the
SharedViewModelwith default values for a new training session.Example:
sharedViewModel.sessionMode = SessionMode.Training sharedViewModel.scoresTextState = "" sharedViewModel.handsCounter = 0 sharedViewModel.trainingFinished = false sharedViewModel.score = 0f sharedViewModel.waitingForTrainingFinish = false
Component Composition:
Composes the
NavBarandTrainingBodycomponents.Example:
Column(modifier = Modifier.fillMaxWidth()) { NavBar(navController = navController) TrainingBody(navigateTo, sharedViewModel) }
Scoreboard Page
File: pages/Scoreboard.kt
Purpose: The Scoreboard composable displays the user's scoreboard, allowing them to view and reset their scores. It interacts with the shared view model and manages the state of the score list.
Key Functionalities:
Displaying Scores:
Retrieves the list of scores from the device's storage and displays them using the
ScoreboardScorescomposable.
Resetting Scores:
Provides a button that allows users to clear all scores, updating the displayed list accordingly.
Complex Parts:
State Management:
scoresCleared: Tracks whether scores have been cleared.scoreListState: Holds the list of scores, initialized by reading from a JSON file.
var scoresCleared by remember { mutableStateOf(false) }
val scoreListState = remember {
mutableStateOf(readPlayerScoresFromFile(context, "player_scores.json"))
}Interactions:
The "Reset all scores" button clears the scores file and updates the state to reflect the cleared scores.
Text(
text = ScoreboardData.resetButton["title"] as String,
color = snwGreenApple,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.clickable {
clearScores(context, "player_scores.json")
scoreListState.value = readPlayerScoresFromFile(context, "player_scores.json")
scoresCleared = !scoresCleared
}
)
Game Controller Page
File: pages/GameController.kt
Purpose
The GameController manages the overall gameplay logic, including player interactions, animations, game flow, and state updates.
Detailed Explanation
1. Player Initialization
The Player data class initializes player properties such as position, amount, cards, and animations.
data class Player(
val pos: Int,
var amount: MutableState<Long> = mutableLongStateOf(0),
// ... other properties
)
Functionality:
Properties: Each player has properties for position, amount, cards, and animations.
Mutable States: Properties are wrapped in mutable states to enable dynamic UI updates.
2. Session Mode
The SessionMode and ChallengeLength enums define different modes of gameplay and challenge lengths.
enum class SessionMode {
Challenge,
Training,
Undefined
}
enum class ChallengeLength {
Long,
Fast
}
Functionality:
SessionMode: Specifies the mode (Challenge, Training, Undefined).
ChallengeLength: Defines the length of the challenge (Long, Fast).
3. Animations Handling
The cardAnimLogic and cardFlipLogic functions manage animation progress for card dealing and flipping.
Card Animation Logic:
@Composable
fun cardAnimLogic(startAnimation: Boolean, isFastFold: MutableState<Boolean>): MutableFloatState {
val animProgress = remember { mutableFloatStateOf(0f) }
val animSpec = if (startAnimation) {
tween<Float>(durationMillis = if (isFastFold.value) 0 else 300)
} else {
snap()
}
val internalAnimProgress by animateFloatAsState(
targetValue = if (startAnimation) 1f else 0f,
animationSpec = animSpec,
label = "animateComCardProgress"
)
LaunchedEffect(internalAnimProgress) { animProgress.floatValue = internalAnimProgress }
return animProgress
}Card Flip Logic:
@Composable
fun cardFlipLogic(startAnimation: Boolean, isFastFold: MutableState<Boolean>): MutableState<Float> {
val degreesProgress = remember { mutableFloatStateOf(0f) }
val degrees by animateFloatAsState(
targetValue = if (startAnimation) 180f else 0f,
animationSpec = tween(durationMillis = if (isFastFold.value) 0 else 150, easing = LinearEasing),
label = "cardFlipAnimation"
)
LaunchedEffect(degrees) { degreesProgress.floatValue = degrees }
return degreesProgress
}
Functionality:
cardAnimLogic: Manages animation progress for card dealing using
animateFloatAsState.cardFlipLogic: Manages animation progress for card flipping using
animateFloatAsState.
4. Game Flow and State Updates
Functions like startHand, performFinishHand, and updateStats manage the game flow, starting new hands, finishing hands, and updating statistics.
Start Hand:
fun startHand() {
resetAnimations()
if (handId.value!!.deck?.isNotEmpty() == true && (pokerHand.rounds.isEmpty() || pokerHand.winners != null)) {
// ... other code
pokerHand.setHandInfo(
deckAI = handId.value!!.deck!!,
plyList = playersList,
dealerPos = dealerPos,
hId = handId.value!!.id,
sb = sbAmount,
bb = bbAmount,
anteValue = anteValue
)
pokerHand.next(context, navController)
updateStats()
} else {
if (sharedViewModel.creditsTerminated) {
creditTerminatedReceieved()
}
}
}
Functionality:
Reset Animations: Resets animations for a new hand.
Set Hand Info: Sets the hand information and moves to the next state.
Perform Finish Hand:
fun performFinishHand() {
computeStats()
if (sharedViewModel.sessionMode == SessionMode.Training) {
updateStats()
}
if (sharedViewModel.sessionMode == SessionMode.Challenge && sharedViewModel.handsCounter == totalHands) {
sharedViewModel.gameFinished = true
writePlayerScoresToFile(
context,
"player_scores.json",
sharedViewModel.score,
sharedViewModel.selectedMode!!,
sharedViewModel.seatNumber
)
} else if (sessionStatus.value == SessionStatus.Playing) {
startHand = true
} else {
sharedViewModel.score = scores["Hero"]!!.toFloat()
if (sharedViewModel.waitingForTrainingFinish) {
sharedViewModel.trainingFinished = true
showPots = false
}
}
}
Functionality:
Compute Stats: Calculates the scores for each player.
Update Training Mode Stats: Updates the statistics for training mode.
Check Challenge Completion: Checks if the challenge is completed and updates the score.
Update Stats:
fun updateStats() {
localHandId.value = pokerHand.handIdSerial
if (sharedViewModel.sessionMode == SessionMode.Training) {
if (sharedViewModel.handsCounter > 0) {
var newText = ""
for (plyName in scores.keys) {
newText += "Score $plyName: ${convertDoubleToString(scores[plyName]?.toLong() ?: 0L)}\n"
}
sharedViewModel.scoresTextState = newText.trimEnd()
}
}
if (!sharedViewModel.waitingForTrainingFinish) {
handsCounterFake.intValue = sharedViewModel.handsCounter + 1
}
}
Functionality:
Update Hand ID: Updates the current hand ID.
Update Scores Text: Constructs and updates the scores text for the UI.
Update Hands Counter: Updates the hands counter.
5. Handling Socket Reconnection
The handleSocketReconnection function manages reconnection logic to ensure the game can resume smoothly after a disconnection.
fun handleSocketReconnection() {
if (loginStatus == PokerSnowieAPI.LoginStatus.Logged && !SnowieApp.pokerSnowieAPI.isCriticalFunctionRunning.value && !waitingForHuman) {
if (sharedViewModel.performLiveAdvice) {
if (pendingMove != -1) {
coroutineScope.launch {
try {
withContext(Dispatchers.IO) {
SnowieApp.pokerSnowieAPI.evaluateMove(
move = pendingMove,
amount = pendingAmount.toDouble() / 1000.0,
pHand = pokerHand,
sharedViewModel = sharedViewModel,
context = context
)
}
decisionEval(moveInfo.value!!.buttonIndex)
} catch (e: Exception) {
AppStatus.showSocketError(
"Error evaluating move",
e.message.toString()
) {
navController.navigateUp()
}
Log.e("SnowieApp", "PokerSnowieAPI evaluateMove failure", e)
}
}
}
} else if ((pokerHand.rounds.isEmpty()) || (pokerHand.winners != null)) {
startHand = true
} else if (pokerHand.getActivePlayer() is AIPokerPlayer) {
pokerHand.next(context, navController)
}
}
}
Functionality:
Check Login Status: Ensures the user is logged in and no critical functions are running.
Handle Pending Move: Evaluates the pending move if necessary.
Start Hand: Starts a new hand if the conditions are met.
Next Action: Proceeds to the next action if an AI player is active.
Components
Generic Button Component
The GenericButton composable creates a customizable button used throughout the app. It supports various states, text styles, and optional icons.
Key Functionalities:
Customization:
Supports different button states (
Primary,Secondary, etc.) that alter the button's appearance.Displays different text styles and optional icons.
Dynamic Styling:
Adjusts its size and shape based on parameters and
SharedViewModelstate.
Complex Parts:
State-Based Styling:
Changes the button's appearance based on its state.
Example:
val (textColor, backgroundColor, borderColor) = when (buttonState) { ButtonState.Primary -> Triple(white, snwGreenApple, snwGreenApple) ButtonState.Secondary -> Triple(darkerGreen, lightGreen, darkGreen) ButtonState.Tertiary -> Triple(black, white, snwWhite) ButtonState.Action -> Triple(white, snwDustyOrange, snwDustyOrange) ButtonState.ActionSecondary -> Triple(black, white, snwGreyishBrown) ButtonState.ActionTertiary -> Triple(black, white, white) ButtonState.Image -> Triple(white, darkGreen, darkGreen) }
Content Arrangement:
Uses
RowandColumncomposables to arrange button content.Example:
Row( modifier = Modifier .fillMaxSize() .padding(PaddingValues(start = 24.dp, end = 24.dp)), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { if (isCancel) { Icon(Icons.Default.Close, contentDescription = "Cancel", Modifier.size(16.dp)) Spacer(Modifier.width(8.dp)) } Column(horizontalAlignment = Alignment.CenterHorizontally) { textLines.zip(typographyStyles).forEach { (text, style) -> Text(text = text, style = style) } } if (alignLeft) { Image(painterResource(id = R.drawable.long_arrow_right_light), contentDescription = null, Modifier.size(30.dp, 17.dp)) } }
Custom Alert Dialog
File: components/CustomAlertDialog.kt
Purpose: The CustomAlertDialog composable displays custom alert dialogues for error messages or important information.
Key Functionalities:
Display Alerts:
Shows a dialogue with a title, message, and buttons for user actions.
Customizable with different button texts and actions.
User Interaction:
Provides callback functions (
onConfirm,onDismissRequest) to handle user actions.
Complex Parts:
Dialog Properties:
Uses
DialogPropertiesto customize the dialogue's behaviour.Example:
Dialog( onDismissRequest = onDismissRequest, properties = DialogProperties(usePlatformDefaultWidth = false) ) { // Dialog content }Composable Content:
Combines multiple composables to build the dialogue.
Example:
Card( colors = CardDefaults.cardColors(containerColor = white), shape = RoundedCornerShape(8.dp), elevation = CardDefaults.cardElevation(4.dp), modifier = Modifier.fillMaxWidth().padding(horizontal = 50.dp).border(1.dp, white, RoundedCornerShape(8.dp)) ) { Column( modifier = Modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Text(text = title, style = MaterialTheme.typography.titleMedium, color = black) Spacer(modifier = Modifier.height(5.dp)) Text(text = message, style = MaterialTheme.typography.labelMedium, color = black, textAlign = TextAlign.Center) Spacer(modifier = Modifier.height(40.dp)) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { GenericButton( height = 40.dp, width = 110.dp, sharedViewModel = sharedViewModel, buttonState = ButtonState.Primary, buttonText = ButtonText.OneMedium, textLines = listOf(if (buttonText == "") "OK" else buttonText) ) { onConfirm() } if (cancelText != "") { Spacer(modifier = Modifier.width(5.dp)) GenericButton( height = 40.dp, width = 110.dp, sharedViewModel = sharedViewModel, buttonState = ButtonState.ActionTertiary, buttonText = ButtonText.OneMedium, textLines = listOf(cancelText) ) { onDismissRequest() } } } } }
Logo Header Component
File: components/LogoHeader.kt
Purpose: The LogoHeader composable displays the app's logo and header text. It is used at the top of the login screen.
Key Functionalities:
Display Logo:
Shows the application logo.
Display Header Text:
Displays one or more lines of header text below the logo.
Example:
Column(
horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.drawable.logo_positivo),
contentDescription = null,
modifier = Modifier.width(300.dp).height(44.dp)
)
Spacer(modifier = Modifier.height(12.dp))
logoHeaderData.forEach { text ->
Text(
text = text["title"] as String,
style = typography.titleLarge,
textAlign = TextAlign.Center,
color = mediumGrey
)
}
}Rectangle Text Input
File: components/RectangleTextInput.kt
Purpose: The RectangleTextInput composable creates a rectangular text input field for entering text, such as email or password.
Key Functionalities:
Customizable Input Field:
Supports hints, input values, and validation.
Form Validation:
Validates email input and displays an error message if the email is invalid.
Complex Parts:
Email Validation:
Validates the email input when the text field loses focus.
Example:
val emailPattern = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}".toRegex() val isEmailValid = remember { mutableStateOf(true) } OutlinedTextField( value = value, onValueChange = onValueChange, modifier = modifier .onFocusChanged { if (!it.isFocused) { isEmailValid.value = emailPattern.matches(value) onValidEmail(isEmailValid.value) } }, //... )
Home Body Component
File: components/HomeBody.kt
Purpose: The HomeBody component displays the main content of the home page, including a header, description, and buttons for navigating to different sections.
Key Functionalities:
Display Content:
Shows a header, description, and buttons with text from
HomeData.
Navigation:
Provides buttons to navigate to the Challenge Setup and Training Setup pages.
Complex Parts:
Dynamic Scaling:
Adjusts the size of text and buttons based on
sharedViewModel.percentageReduction.Example:
val scaleFactor = 1 - (sharedViewModel.percentageReduction / 100)
Button Configuration:
Configures buttons with specific properties and navigation actions.
Example:
GenericButton( height = 150.dp * scaleFactor, buttonState = ButtonState.Primary, buttonText = ButtonText.OneXLarge, textLines = listOf(homeBodyData[2]["challengeBtn"] as String), alignLeft = true, bgImg = "challenge", sharedViewModel = sharedViewModel, onClick = { navigateTo(Route.ChallengeSetup) } )
Home Footer Component
File: components/HomeFooter.kt
Purpose: The HomeFooter component displays additional information and links at the bottom of the home page.
Key Functionalities:
Display Footer Content:
Shows a header, body text, and a link with text from
HomeData.
External Links:
Provides clickable text and an icon to navigate to an external website.
Complex Parts:
Dynamic Scaling:
Adjusts the size of text and icons based on
sharedViewModel.percentageReduction.Example:
val scaleFactor = 1 - (sharedViewModel.percentageReduction / 100)
External Links Handling:
Uses
Intentto open a URL in the browser.Example:
val intent = Intent(Intent.ACTION_VIEW) intent.data = Uri.parse(url) context.startActivity(intent)
Navigation Bar Component
File: components/NavBar.kt
Purpose: The NavBar component provides a navigation bar for the home page, allowing users to access settings and navigate back.
Key Functionalities:
Navigation Handling:
Provides back navigation and access to settings.
Displays the app logo if required.
Dynamic Configuration:
Configurable to show different icons or handle different actions based on parameters.
Complex Parts:
Conditional Rendering:
Conditionally displays back button, settings button, and logo.
Example:
if (alternativeIcon) { IconButton( onClick = { if (navigateTo != null) { navigateTo(Route.Options) } }, ) { Image( painter = painterResource(id = settingsIcon), contentDescription = null, modifier = Modifier .height(20.dp) .width(20.dp) ) } } if (showLogo) { Image( painter = painterResource(id = miniLogo), contentDescription = null, modifier = Modifier .height(30.dp) .width(30.dp) ) }
Custom Switch Component
File: components/CustomSwitch.kt
Purpose: The CustomSwitch component provides a custom switch UI element for toggling settings in the application. It is designed to offer a smooth, animated transition when switching states, enhancing the user experience with a visually appealing interface.
Key Functionalities:
Animated State Changes:
The CustomSwitch component utilizes animations to smoothly transition the background colour and the position of the switch button when toggled. This is achieved using animateColorAsState and animateDpAsState functions.
Background Colour Animations
animateColorAsStateis used to change the background colour based on theisCheckedstate. WhenisCheckedis true, the background colour transitions tosnwGreenApple, and when false, it transitions tosnwWhite.Example:
val backgroundColor by animateColorAsState( if (isChecked) snwGreenApple else snwWhite, label = "switchBgColorAnimation" )
Position Animation:
animateDpAsStateis used to animate the horizontal position of the switch button. The button moves to18.5.dpwhenisCheckedis true and returns to0.dpwhen false.Example:
val position by animateDpAsState( if (isChecked) 18.5.dp else 0.dp, label = "switchButtonAnimation" )
State Management:
The component manages the on/off state of the switch and triggers the provided onCheckedChange function when toggled. This allows the parent component to be notified of the state change and update its state accordingly.
Example:
Box( modifier = Modifier .clickable { onCheckedChange(!isChecked) } )
Structure and Layout:
The switch consists of a background Box and an inner Box that represents the switch button. The inner Box changes position and elevation based on the isChecked state, creating the sliding effect.
Background Box:
The background
Boxadjusts its color and shape based on theisCheckedstate.Example:
Box( modifier = Modifier .width(51.dp) .height(31.dp) .background( color = backgroundColor, shape = RoundedCornerShape(20.dp) ) .clickable { onCheckedChange(!isChecked) } .padding(2.dp) )
Switch Button Box:
The inner
Boxmoves horizontally and has a shadow to give it a raised appearance.Example:
Box( modifier = Modifier .size(28.dp) .offset(x = position) .shadow(3.dp, shape = CircleShape) ) { Box( modifier = Modifier .fillMaxSize() .background(color = white, shape = CircleShape) .border(0.5.dp, Color(0x19000000), CircleShape) ) }
Challenge Header Component
File: components/ChallengeHeader.kt
Purpose: The ChallengeHeader component displays the header section for the challenge setup page, including the app logo, challenge title, and a navigation bar.
Key Functionalities:
Header Layout:
Uses
BoxandColumnto structure the header layout with an image background and gradient overlay.Example:
Box( modifier = Modifier .background(Color.Transparent) .clipToBounds() ) { Image( painter = painterResource(id = R.drawable.challenge), contentDescription = null, contentScale = ContentScale.FillWidth, modifier = Modifier .height(268.dp * scaleFactor) .scale(1.5f) ) Box( modifier = Modifier .height(268.dp * scaleFactor) .fillMaxWidth() .background(gradient) ) }
Navigation Bar Integration:
Includes the
NavBarcomponent for navigation.Example:
NavBar(navController = navController)
Dynamic Scaling:
Adjusts the scale of UI elements based on the
percentageReductionvalue in theSharedViewModel.Example:
val scaleFactor = 1 - (sharedViewModel.percentageReduction / 100)
Challenge Settings Component
File: components/ChallengeSettings.kt
Purpose: The ChallengeSettings component allows users to select the table size and challenge mode for the session. It also includes a button to start the challenge.
Key Functionalities:
Dynamic Scaling:
Similar to the header, the
ChallengeSettingscomponent scales UI elements based on thepercentageReductionvalue.Example:
val scaleFactor = 1 - (sharedViewModel.percentageReduction / 100)
Table Size Selection:
Provides options for selecting the table size using
GenericButtoncomponents.Example:
ChallengeData.tableSizeButtons.forEachIndexed { index, buttons -> val isSelected = selectedSeatIndex == index GenericButton( height = 80.dp * scaleFactor, width = 160.dp * scaleFactor, sharedViewModel = sharedViewModel, buttonState = if (isSelected) ButtonState.Secondary else ButtonState.Tertiary, buttonText = ButtonText.TwoLarge, textLines = listOf(buttons[0]["number"] as String, buttons[1]["text"] as String) ) { selectedSeatIndex = index sharedViewModel.seatNumber = (buttons[0]["number"] as String).toInt() } }
Challenge Mode Selection:
Allows users to choose between different challenge modes using
GenericButtoncomponents.Example:
ChallengeData.modeButtons.forEachIndexed { index, buttons -> val isSelected = selectedModeIndex == index GenericButton( height = 110.dp * scaleFactor, width = 160.dp * scaleFactor, sharedViewModel = sharedViewModel, buttonState = if (isSelected) ButtonState.Secondary else ButtonState.Tertiary, buttonText = ButtonText.Three, textLines = listOf(buttons[0]["typeText"] as String, buttons[1]["hands"] as String), iconName = buttons[2]["icon"] as String ) { selectedModeIndex = index sharedViewModel.selectedMode = buttons[3]["mode"] as ChallengeLength } }
Starting the Challenge:
Includes a button to start the challenge, navigating to the
GameControllerroute.Example:
GenericButton( height = 60.dp * scaleFactor, sharedViewModel = sharedViewModel, buttonState = ButtonState.Primary, buttonText = ButtonText.OneLarge, textLines = listOf(ChallengeData.startButtonText["title"] as String), onClick = { navigateTo(Route.GameController) } )
Scoreboard Navigation:
Provides a clickable text to navigate to the scoreboard.
Example:
Text( text = ChallengeData.scoreboardText["title"] as String, style = MaterialTheme.typography.bodyLarge, color = snwGreenApple, textAlign = TextAlign.Center, modifier = Modifier .fillMaxWidth() .padding(bottom = 32.dp * scaleFactor) .scale(scaleFactor) .clickable { navigateTo(Route.Scoreboard) } )
Initialization with LaunchedEffect:
Initializes default challenge settings when the component is first composed.
Example:
LaunchedEffect(Unit) { sharedViewModel.selectedMode = ChallengeLength.Fast sharedViewModel.seatNumber = 5 }
Training Body Component
File: components/TrainingBody.kt
Purpose: The TrainingBody component allows users to select various training settings such as table size, stakes, stack size, and other options. It includes several interactive UI elements for configuring these settings.
Key Functionalities:
Dynamic Scaling:
Adjusts the scale of UI elements based on the
percentageReductionvalue in theSharedViewModel.Example:
val scaleFactor = 1 - (sharedViewModel.percentageReduction / 100)
Custom Table Size Popup:
Displays a popup dialogue for selecting a custom table size.
Example:
if (showDialog) { PopUp( onDismissRequest = { showDialog = false }, sharedViewModel = sharedViewModel, onConfirm = { tableSize -> selectedSeat = tableSize selectedCustomTableSize = tableSize.toString() sharedViewModel.seatNumber = tableSize setTrainingPref(key = tableSizeKey, index = sharedViewModel.seatNumber) } ) }
Table Size Selection:
Provides options for selecting the table size using
GenericButtoncomponents.Example:
TrainingData.tableSizeButtons.forEachIndexed { index, buttons -> val isSelected = selectedSeat.toString() == (buttons[0]["number"] as String) || (index + 1 == TrainingData.tableSizeButtons.size) && selectedSeat.toString() == selectedCustomTableSize val numberText = if ((index + 1 == TrainingData.tableSizeButtons.size) && selectedCustomTableSize != "") selectedCustomTableSize else buttons[0]["number"] as String GenericButton( height = 80.dp * scaleFactor, width = 76.dp * scaleFactor, buttonState = if (isSelected) ButtonState.Secondary else ButtonState.Tertiary, buttonText = ButtonText.TwoLarge, sharedViewModel = sharedViewModel, textLines = listOf(numberText, buttons[1]["text"] as String) ) { if (index + 1 == TrainingData.tableSizeButtons.size) { showDialog = true } else { selectedSeat = (buttons[0]["number"] as String).toInt() sharedViewModel.seatNumber = (buttons[0]["number"] as String).toInt() setTrainingPref(key = tableSizeKey, index = sharedViewModel.seatNumber) } } }
Stakes Selection:
Allows users to choose stakes using the
SelectorBoxcomponent.Example:
SelectorBox( sharedViewModel = sharedViewModel, text = TrainingData.blindsList[currentBlindIndex], currentIndex = currentBlindIndex, listSize = TrainingData.blindsList.size, onLeftTap = { currentBlindIndex = max(0, currentBlindIndex - 1) setTrainingPref(key = stakesKey, index = currentBlindIndex) }, onRightTap = { currentBlindIndex = min(TrainingData.blindsList.size - 1, currentBlindIndex + 1) setTrainingPref(key = stakesKey, index = currentBlindIndex) } )
Stack Size Selection:
Allows users to choose the stack size using the
SelectorBoxcomponent.Example:
SelectorBox( sharedViewModel = sharedViewModel, text = TrainingData.stacksList[currentStackIndex], currentIndex = currentStackIndex, listSize = TrainingData.stacksList.size, onLeftTap = { currentStackIndex = max(0, currentStackIndex - 1) setTrainingPref(key = stackSizeKey, index = currentStackIndex) }, onRightTap = { currentStackIndex = min(TrainingData.stacksList.size - 1, currentStackIndex + 1) setTrainingPref(key = stackSizeKey, index = currentStackIndex) } ) sharedViewModel.startingChips = (regex.find(TrainingData.stacksList[currentStackIndex])).let { "(${it!!.groups[1]?.value})" }
Toggle Options:
Provides switches for toggling ante and live advice options.
Example:
SelectorBox( sharedViewModel = sharedViewModel, text = TrainingData.anteText["title"] as String, isSwitch = true, switchValue = anteSwitchState, onSwitchValueChange = { newValue -> sharedViewModel.anteEnabled = newValue anteSwitchState = newValue setTrainingPref(key = anteKey, enabled = newValue) } )
Starting the Training:
Includes a button to start the training session, navigating to the
GameControllerroute.Example:
GenericButton( height = 60.dp * scaleFactor, buttonState = ButtonState.Primary, buttonText = ButtonText.OneLarge, sharedViewModel = sharedViewModel, textLines = listOf(TrainingData.buttonText["title"] as String) ) { sharedViewModel.selectedMode = null sharedViewModel.seatNumber = getTrainingPref(tableSizeKey) navigateTo(Route.GameController) }
Selector Box Component
File: components/SelectorBox.kt
Purpose: The SelectorBox component provides a customizable UI element for selecting different options such as table size, stakes, and other preferences. It can display text, icons, and switches.
Key Functionalities:
Dynamic Scaling:
The component uses the
percentageReductionvalue from theSharedViewModelto adjust its size dynamically. This ensures that the UI scales appropriately based on user preferences or device characteristics.Example:
val scaleFactor = 1 - (sharedViewModel.percentageReduction / 100)
Custom Switch Handling:
When
isSwitchis true, aCustomSwitchcomponent is rendered. The switch state is managed locally using arememberblock. When the switch state changes, the providedonSwitchValueChangecallback is triggered to notify the parent component of the change.Example:
if (isSwitch) { var switchState by remember { mutableStateOf(switchValue) } CustomSwitch( isChecked = switchState, onCheckedChange = { switchState = it onSwitchValueChange(it) } ) }
Arrow Buttons for Navigation:
The left and right arrow buttons allow users to navigate through a list of options. The
onLeftTapandonRightTapfunctions handle the logic for updating thecurrentIndexwhen the buttons are clicked.Example:
IconButton( onClick = { if (currentIndex > 0) onLeftTap() } ) { Icon( painter = painterResource(id = R.drawable.ic_arrow_minus), contentDescription = null, tint = if (currentIndex > 0) darkBlue else Color.Gray, modifier = Modifier .height(26.dp) .width(15.dp) ) } IconButton( onClick = { if (currentIndex < listSize - 1) onRightTap() } ) { Icon( painter = painterResource(id = R.drawable.ic_arrow_plus), contentDescription = null, tint = if (currentIndex < listSize - 1) darkBlue else Color.Gray, modifier = Modifier .height(26.dp) .width(15.dp) ) }
Popup Selector Handling:
Displays additional text and handles layout differently when
isPopupSelectoris true. WhenisPopupSelectoris true, the component displays additional text fields. This is useful for providing more detailed information in a larger format. The layout adjusts to include both primary and secondary text.Example:
if (isPopupSelector) { Column(horizontalAlignment = Alignment.CenterHorizontally) { ButtonTypographyStyles["heading"]?.let { heading -> Text( text = text, style = heading, color = darkBlue, textAlign = TextAlign.Center, modifier = Modifier.scale(scaleFactor) ) } ButtonTypographyStyles["tinyHeading"]?.let { tiny -> Text( text = text2, style = tiny, color = darkBlue, textAlign = TextAlign.Center, modifier = Modifier.scale(scaleFactor) ) } } } else { Text( text = text, style = MaterialTheme.typography.displayMedium, color = darkBlue, textAlign = TextAlign.Center, modifier = Modifier.scale(scaleFactor) ) }
Options Grid Component
File: components/OptionsGrid.kt
Purpose: The OptionsGrid component provides a user interface for displaying a grid of buttons that users can select. This is used in the options-related pages to allow users to choose their preferred settings, such as extra buttons.
Technical Details:
State Management:
The component uses
SnapshotStateListto manage the state of button selections.Initial button states are loaded from shared preferences.
Example:
val initialSelectedButtons = getSelectedButtons(context) LaunchedEffect(Unit) { OptionsData.buttons.forEachIndexed { index, button -> val buttonLabel = button["button"] as String buttonStates[index] = initialSelectedButtons.contains(buttonLabel) } }
Button Selection Logic:
The component ensures that a maximum of 4 buttons can be selected at a time.
When a button is clicked, its selection state toggles, and the new state is saved in shared preferences.
Example:
GenericButton( height = 76.dp, width = 50.dp, buttonState = if (isSelected) ButtonState.Secondary else ButtonState.Tertiary, buttonText = ButtonText.OneTiny, sharedViewModel = sharedViewModel, textLines = listOf(button["button"] as String) ) { if (isSelected || buttonStates.count { it } < 4) { buttonStates[index] = !isSelected val selectedLabels = OptionsData.buttons .filterIndexed { i, _ -> buttonStates[i] } .map { it["button"] as String } setSelectedButtons(context, selectedLabels) } }
Custom Grid Layout:
The grid layout is managed using the
Layoutcomposable, which allows custom arrangement of child composables.The layout calculates the number of columns based on the available width and positions the buttons accordingly.
Example:
Layout( content = { OptionsData.buttons.forEachIndexed { index, button -> // Button content here } }, measurePolicy = { measurables, constraints -> val columnCount = (constraints.maxWidth - spacingPx) / (itemWidthPx + spacingPx) val rowCount = kotlin.math.ceil(measurables.size / columnCount.toDouble()).toInt() val itemConstraints = Constraints.fixed(itemWidthPx, itemHeightPx) val placeables = measurables.map { it.measure(itemConstraints) } layout(constraints.maxWidth, rowCount * (itemHeightPx + spacingPx)) { placeables.forEachIndexed { index, placeable -> val column = index % columnCount val row = index / columnCount val x = column * (itemWidthPx + spacingPx) val y = row * (itemHeightPx + spacingPx) placeable.place(x, y) } } } )
Detailed Explanation:
State Initialization:
The component initializes the state of each button using the
SnapshotStateListto ensure that the selection states are managed efficiently and reactively. The initial states are loaded using aLaunchedEffect, which ensures that the initialization code runs only once when the composable is first composed.
Selection Handling:
Each button's state is toggled when clicked, but only if the current selection count is less than 4 or the button is already selected. This logic ensures that users cannot select more than 4 buttons simultaneously. The updated selection states are saved back to shared preferences to persist the user's choices across sessions.
Custom Layout:
The
Layoutcomposable is used to define a custom grid layout for the buttons. This provides more control over the positioning of the child composables compared to using standard layout composables likeRoworColumn. The layout logic calculates the number of columns that can fit within the available width and arranges the buttons in rows accordingly. Each button is measured with fixed constraints, ensuring uniform sizing.
ScoreboardScores Component
File: components/ScoreboardScores.kt
Purpose: The ScoreboardScores composable displays the list of scores for the selected game type. It includes UI elements for selecting different game modes and displays the scores accordingly.
Key Functionalities:
Mode Selection:
Provides buttons to switch between different game modes, filtering and displaying scores based on the selected mode.
Score Display:
Displays each score in a card format, highlighting the best score.
Complex Parts:
State Management:
selectedModeIndex: Tracks the currently selected game mode.displayedScores: Holds the list of scores to be displayed, filtered and sorted by the selected game type.
var selectedModeIndex by remember { mutableStateOf(0) }
var displayedScores by remember {
mutableStateOf(scoreListState.value.filter { it.type == gameType }.sortedBy { it.score })
}
LaunchedEffect(scoreListState) {
displayedScores = scoreListState.value.filter { it.type == gameType }.sortedBy { it.score }
}Dynamic Filtering and Sorting:
Filters scores based on the selected game mode and updates the displayed list dynamically.
GenericButton(
height = 110.dp,
width = 76.dp,
buttonState = if (isSelected) ButtonState.Secondary else ButtonState.Tertiary,
buttonText = ButtonText.Three,
textLines = listOf(buttons[0]["type"] as String, buttons[1]["seats"] as String),
sharedViewModel = sharedViewModel,
iconName = buttons[2]["icon"] as String
) {
selectedModeIndex = index
val selectedType = (buttons[0]["type"] as String).first().toString() +
(buttons[1]["seats"] as String).first().toString()
displayedScores = scoreListState.value.filter { it.type == selectedType }.sortedBy { it.score }
}Human Moves Component
File: components/HumanMoves.kt
Purpose
The HumanMoves component provides a user interface for the human player to make decisions during the poker game. This includes options for folding, calling, betting, and raising. It dynamically updates based on the game state and user interactions.
Detailed Explanation
1. State Management
Several states are defined to manage the UI and logic for human moves:
curPlayerandcurRound: Track the current player and round.isRaising: Indicates whether the player is in the process of raising.selectedOptions: Holds the selected button options.selectedButton,selectedBetRaiseAmount,sliderValue,sliderMinValue,sliderMaxValue: Manage the bet/raise UI components.foldIsHiddenandbetRaiseIsHidden: Control the visibility of fold and bet/raise buttons.checkCallText,betRaiseLabel,betOrRaise,betRaiseText: Hold text values for buttons and labels.buttonEnabledMap: Stores the enabled state of each button.fastFoldSituation: Indicates if the fast fold option is available.
2. Calculating Bet/Raise Values
The getExtraButtonValue function calculates the amount for different bet/raise options based on the pot size and the current round.
fun getExtraButtonValue(extraButtonValue: String): Long {
val potAmount = pHand.getTotalAmount() + curRound.value!!.getCallAmount(curPlayer.value!!)
return when (extraButtonValue) {
"1/5 POT" -> potAmount / 5 + curRound.value!!.getRoundAmount(curPlayer.value!!) + curRound.value!!.getCallAmount(curPlayer.value!!)
"1/4 POT" -> potAmount / 4 + curRound.value!!.getRoundAmount(curPlayer.value!!) + curRound.value!!.getCallAmount(curPlayer.value!!)
"1/3 POT" -> potAmount / 3 + curRound.value!!.getRoundAmount(curPlayer.value!!) + curRound.value!!.getCallAmount(curPlayer.value!!)
"1/2 POT" -> potAmount / 2 + curRound.value!!.getRoundAmount(curPlayer.value!!) + curRound.value!!.getCallAmount(curPlayer.value!!)
"2/3 POT" -> potAmount * 2 / 3 + curRound.value!!.getRoundAmount(curPlayer.value!!) + curRound.value!!.getCallAmount(curPlayer.value!!)
"3/4 POT" -> potAmount * 3 / 4 + curRound.value!!.getRoundAmount(curPlayer.value!!) + curRound.value!!.getCallAmount(curPlayer.value!!)
"1 POT" -> potAmount + curRound.value!!.getRoundAmount(curPlayer.value!!) + curRound.value!!.getCallAmount(curPlayer.value!!)
"2 POT" -> potAmount * 2 + curRound.value!!.getRoundAmount(curPlayer.value!!) + curRound.value!!.getCallAmount(curPlayer.value!!)
"ALL-IN" -> curRound.value!!.getRoundAmount(curPlayer.value!!) + pHand.getCurrentAmount(curPlayer.value!!)
else -> 0
}
}
Functionality:
Calculates Pot-Based Values: Computes the bet/raise amount based on the pot size and the player's current round amount.
3. Updating Bet/Raise GUI
The updateBetRaiseGUI function updates the UI elements related to betting and raising.
fun updateBetRaiseGUI() {
selectedButton.value = ""
for (buttonData in selectedOptions) {
val buttonValue = getExtraButtonValue(buttonData)
if (buttonValue == selectedBetRaiseAmount.longValue) {
selectedButton.value = buttonData
break
}
}
if (selectedBetRaiseAmount.longValue == (sliderMaxValue.floatValue * 1000f).toLong()) {
betRaiseLabel.value = listOf("ALL IN", convertDoubleToString(selectedBetRaiseAmount.longValue))
} else {
betRaiseLabel.value = listOf(betOrRaise.value, convertDoubleToString(selectedBetRaiseAmount.longValue))
}
sliderValue.floatValue = selectedBetRaiseAmount.longValue.toFloat() / 1000f
}
Functionality:
Updates Labels and Values: Adjusts the text labels and slider values to reflect the current bet/raise amount.
4. Button State Management
Functions like setBetRaiseButton and paintStandardButtons manage the state and visibility of buttons based on the current game context.
Set Bet/Raise Button:
fun setBetRaiseButton(lbl: String) {
val minBet = curRound.value!!.getMinBet(curPlayer.value!!)
val maxBet = curRound.value!!.getMaxBet(curPlayer.value!!)
selectedBetRaiseAmount.longValue = minBet
if (minBet == maxBet) {
betRaiseText.value = listOf("ALL IN", convertDoubleToString(selectedBetRaiseAmount.longValue))
} else {
betRaiseLabel.value = listOf(betRaiseLabel.value[0].split(" ")[0], convertDoubleToString(selectedBetRaiseAmount.longValue))
betRaiseText.value = listOf(lbl)
sliderMinValue.floatValue = minBet.toFloat() / 1000.0f
sliderMaxValue.floatValue = maxBet.toFloat() / 1000.0f
sliderValue.floatValue = minBet.toFloat() / 1000.0f
updateBetRaiseGUI()
}
betRaiseIsHidden.value = false
}
Paint Standard Buttons:
fun paintStandardButtons() {
if (!fastFoldSituation.value) {
foldIsHidden.value = false
if (curRound.value!!.canCheck(player = curPlayer.value!!)) {
foldIsHidden.value = true
checkCallText.value = listOf("CHECK")
} else if (curRound.value!!.canCall(player = curPlayer.value!!)) {
if (curRound.value!!.getCallAmount(player = curPlayer.value!!) == pHand.getCurrentAmount(player = curPlayer.value!!)) {
checkCallText.value = listOf("ALL IN")
} else {
checkCallText.value = listOf("CALL", convertDoubleToString(curRound.value!!.getCallAmount(player = curPlayer.value!!)))
}
}
if (curRound.value!!.canBet(player = curPlayer.value!!)) {
betOrRaise.value = "BET "
setBetRaiseButton(lbl = "BET...")
} else if (curRound.value!!.canRaise(player = curPlayer.value!!)) {
betOrRaise.value = "RAISE "
setBetRaiseButton(lbl = "RAISE...")
} else {
betRaiseIsHidden.value = true
}
}
}
Functionality:
Button Visibility and Labels: Determines the visibility and text of the fold, check, call, bet, and raise buttons based on the game state.
5. Rendering the Component
The HumanMoves composable renders the UI for human moves, handling different states (raising, standard moves, fast fold).
Box(
modifier = Modifier.customShadow(offsetY = 7.dp, spread = 7.dp, blurRadius = 7.dp).fillMaxWidth().background(white),
contentAlignment = Alignment.Center
) {
if (isRaising) {
// Render raising UI
} else {
if (fastFoldSituation.value) {
// Render fast fold UI
} else {
// Render standard move buttons (fold, check/call, bet/raise)
}
}
}
Functionality:
Conditional Rendering: Displays different UI elements based on whether the player is raising, performing a standard move, or using the fast fold option.
6. Showing Human Moves
The showFor function determines whether the human moves panel should be shown based on the game state.
fun showFor() {
val activePlayer = pHand.getActivePlayer()
val humanPlayer = pHand.getHumanPlayer()
val round = pHand.getCurrentRound()
if (activePlayer == null || humanPlayer == null || round == null) {
isVisible.value = false
return
}
if (round.isTerminated() || pHand.winners != null || pHand.showdowns != null) {
isVisible.value = false
return
}
curPlayer.value = activePlayer
curRound.value = round
if (activePlayer == humanPlayer && pHand.countPlayers(status = listOf(GameStatus.Playing, GameStatus.Allin)) > 1 && humanPlayer.status == GameStatus.Playing) {
CoroutineScope(Dispatchers.Main).launch {
while (animations.getLastCompletedAnimation() != "showHumanPanel/FastFold") {
delay(100)
}
fastFoldSituation.value = false
paintStandardButtons()
isVisible.value = true
}
} else if (curRound.value!!.getCallAmount(player = humanPlayer) > 0 && humanPlayer.status == GameStatus.Playing) {
fastFoldSituation.value = true
paintStandardButtons()
isVisible.value = true
} else {
isVisible.value = false
}
}
Functionality:
Visibility Logic: Determines when to show the human moves panel based on the active player, current round, and game status.
7. Handling Human Moves Trigger
The LaunchedEffect block handles the humanMovesTrigger to show the human moves panel when triggered.
LaunchedEffect(humanMovesTrigger.value) {
if (humanMovesTrigger.value) {
showFor()
humanMovesTrigger.value = false
}
}
Functionality:
Effect Hook: Reacts to changes in the
humanMovesTriggerstate to display the human moves panel appropriately.
LiveAdvice Component
File: components/LiveAdvice.kt
Purpose
The LiveAdvice component provides real-time feedback and advice to the player during the game, highlighting the best possible move compared to the player's move.
Detailed Explanation
State Management
The component maintains several states to manage UI elements and logic for live advice. Key states include:
isHidden: Controls visibility of the advice panel.bestMoveString,bestFoldProbData,bestCheckCallProbData,bestBetRaiseProbData,playerBetRaiseProbData: Store probability data and move descriptions.
Probability Data Class
The MoveProbsData class holds the probability data for different moves.
data class MoveProbsData(
var moveName: MutableState<String> = mutableStateOf(""),
var pValue: MutableState<Double> = mutableStateOf(0.0),
var pFractDesc: MutableState<String> = mutableStateOf("")
)
Showing Live Advice
The show function updates the UI with the best move data and makes the advice panel visible. It sets the data for various move probabilities and updates the isVisible state to true.
Key steps in the show function:
Clear player bet/raise probability data if the best raise was made.
Populate
bestMoveStringand probability data for fold, check/call, and bet/raise moves.Set
isVisibleto true.
fun show() {
if (bestRaiseMade) {
playerBetRaiseProbData.value = MoveProbsData()
} else {
// Calculate probability descriptions
}
// Populate best move strings
bestMoveString.value = bestMove.value.split(" or ").joinToString(" or ")
// Set fold, check/call, bet/raise probabilities
bestFoldProbData.value = MoveProbsData(possibleMoves[0], sharedViewModel.evaluateMoveData.bestProbs[0].toDouble(), "")
bestCheckCallProbData.value = MoveProbsData(possibleMoves[1], sharedViewModel.evaluateMoveData.bestProbs[1].toDouble(), "")
bestBetRaiseProbData.value = MoveProbsData(possibleMoves[2], sharedViewModel.evaluateMoveData.bestProbs[2].toDouble(), "POT")
isVisible.value = true
sharedViewModel.evaluateMoveData.bestProbs.clear()
}
Rendering the Component
The LiveAdvice composable function renders the UI for live advice. It displays the best move and probability data, and provides buttons to show/hide the advice panel or to continue the game.
The
BoxandColumnelements manage the layout and styling.Text elements display the advice.
GenericButtoncomponents are used for show/hide and continue actions.
@Composable
fun LiveAdvice(
navController: NavController,
liveAdviceTrigger: MutableState<Boolean>,
buttonIndex: MutableIntState,
sharedViewModel: SharedViewModel,
humanMove: MutableState<String>,
bestMove: MutableState<String>,
possibleMoves: List<String>,
isVisible: MutableState<Boolean>,
pokerHand: PokerHand
) {
val isHidden = remember { mutableStateOf(false) }
// Annotated text showing player and best moves
val annotatedText = buildAnnotatedString {
append("Your move was ${humanMove.value}, best would have been ${bestMoveString.value}")
}
// Main layout container
Box(
modifier = Modifier
.customShadow(offsetY = 7.dp, spread = 7.dp, blurRadius = 7.dp)
.fillMaxWidth()
.background(white)
.alpha(if (isVisible.value) 1f else 0f),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(27.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (!isHidden.value) {
// Display live advice text
Text("LIVE ADVICE", style = ExtraTypographyStyles["xtinyM"]!!, color = snwWarmGrey)
Text(annotatedText, color = black, textAlign = TextAlign.Center)
// Box and Column for MoveProbs
// Buttons to hide/show advice and continue the game
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
GenericButton(
height = 60.dp,
width = 132.dp,
buttonState = ButtonState.ActionTertiary,
buttonText = ButtonText.OneSmall,
sharedViewModel = sharedViewModel,
textLines = if (isHidden.value) listOf("SHOW") else listOf("HIDE"),
) {
isHidden.value = !isHidden.value
}
GenericButton(
height = 60.dp,
width = 132.dp,
buttonState = ButtonState.Action,
buttonText = ButtonText.TwoSmallDouble,
sharedViewModel = sharedViewModel,
textLines = listOf("CONTINUE"),
) {
isVisible.value = false
isHidden.value = false
if (buttonIndex.intValue == 0) pokerHand.humanFolded = true
pokerHand.next(context, navController)
}
}
}
}
}
}
Handling the Live Advice Trigger
The LaunchedEffect hook listens for changes in the liveAdviceTrigger state to show the live advice.
LaunchedEffect(liveAdviceTrigger.value) {
if (liveAdviceTrigger.value) {
show()
liveAdviceTrigger.value = false
}
}
NavBarGame Component
File: components/NavBarGame.kt
Purpose
The NavBarGame component serves as the navigation bar within the game interface, providing important session details such as hand ID, game type, hand count, and a button to close the game.
Detailed Explanation
Key Functionalities
Displaying Session Information
Handling Scoreboard Display
Game Navigation and Closure
1. Displaying Session Information
The navigation bar displays essential information such as the current hand ID, game type, and the number of hands played. This information is critical for the player to stay informed about their current session.
Relevant Code:
Column {
ExtraTypographyStyles["tinyL"]?.let {
Text(
text = "Hand id: $handId",
color = black,
style = it
)
}
if (sharedViewModel.sessionMode == SessionMode.Challenge) {
ExtraTypographyStyles["xSmallSB"]?.let {
Text(
text = "$gameType CHALLENGE",
color = black,
style = it,
)
}
ExtraTypographyStyles["tinyM"]?.let {
Text(
text = "Hands: ${handsCounter}/$totalHands",
color = black,
style = it
)
}
} else {
ExtraTypographyStyles["tinyM"]?.let {
Text(
text = "Hands: $handsCounter",
color = black,
style = it
)
}
}
}
Hand ID: Displays the current hand ID.
Game Type: Displays "FAST CHALLENGE" or "EXTENDED CHALLENGE" based on the selected mode.
Hand Counter: Shows the number of hands played out of the total hands.
2. Handling Scoreboard Display
The navigation bar includes functionality to display the scoreboard when needed. This is controlled by a mutable state that triggers the ScoreBoardPopup component.
Relevant Code:
val showScoreboardDialog = remember { mutableStateOf(false) }
if (showScoreboardDialog.value) {
ScoreBoardPopup(
onDismissRequest = { showScoreboardDialog.value = false },
sharedViewModel = sharedViewModel,
scores = scores
)
}
Scoreboard Dialog: Opens a popup displaying player scores when
showScoreboardDialogis true.
3. Game Navigation and Closure
The NavBarGame includes a button to close the game, which changes the session status and navigates the user back to the main menu.
Relevant Code:
CloseGameButton(
sessionStatus,
closeBtnWidth,
sharedViewModel,
navigateTo,
navController
)
Close Game Button: Triggers the closure of the game session and handles the navigation back to the main menu.
PlayerGUI Component
File: components/PlayerGUI.kt
Purpose
The PlayerGUI component is responsible for displaying the user interface elements associated with each player in the game. This includes player cards, chips, dealer button, and move information. The component handles animations for card dealing, card flipping, and chip movements.
Detailed Explanation
Key Functionalities
Animating Card Movements and Flips
Displaying Player Information
Handling Chip Animations
Updating Player UI Elements
1. Animating Card Movements and Flips
The component animates the movement of cards and their flipping based on the game state.
Relevant Code:
val degrees by animateFloatAsState(
targetValue = if (player.shouldFlipCard.value) 180f else 0f,
animationSpec = tween(durationMillis = if (isFastFold.value) 0 else 75, easing = LinearEasing),
label = "cardFlipAnimation"
)
LaunchedEffect(player.shouldFlipCard.value) {
if (player.shouldFlipCard.value) {
val anim = Animation(
animation = {},
shouldComplete = { flipAnimDone.value },
completion = { _ -> flipAnimDone.value = false },
context = context,
label = "flipCard"
)
animations.addAnimation(anim)
}
}
Card Flip Animation: Animates the card flip based on
player.shouldFlipCardstate.Animation Effect: Triggers an animation when
shouldFlipCardstate changes.
2. Displaying Player Information
The component displays player-specific information such as cards, amounts, and the dealer button.
Relevant Code:
val dealerImgElement: @Composable () -> Unit = {
Image(
painter = painterResource(id = R.drawable.dealer),
contentDescription = null,
modifier = Modifier
.size(24.dp)
.alpha(dealerImageAlpha.floatValue)
.scale(scaleFactor)
)
}
val cardsImageElement: @Composable () -> Unit = {
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
player.cards.forEachIndexed { index, card ->
Image(
painter = painterResource(id = if (degrees < 90) R.drawable.back else TableData.cardImageResourceIds[card.value]!!),
contentDescription = null,
modifier = Modifier
.width(cardWidth)
.graphicsLayer { rotationY = if (degrees < 90) degrees else degrees - 180 }
.height(if (isHuman) 76.dp else 50.dp)
.offset(x = cardCurrentX - endX, y = cardCurrentY - endY)
)
}
}
}
Dealer Button: Displays the dealer button with appropriate animation.
Cards Display: Shows player cards with flip animation.
3. Handling Chip Animations
The component animates the movement and display of chips during the game.
Relevant Code:
val chipRowElement: @Composable () -> Unit = {
var chipRowWidth by remember { mutableStateOf(0.dp) }
var shouldAnimateChips by remember { mutableStateOf(false) }
val chipsAlpha = remember { mutableStateOf(0f) }
LaunchedEffect(dealAnimDone.value, player.roundAmount.value) {
chipsAlpha.value = if (dealAnimDone.value && player.roundAmount.value > 0) 1f else 0f
}
val animSpec = if (startChipsAnimation) {
tween<Float>(durationMillis = if (isFastFold.value) 0 else 300)
} else {
snap()
}
val chipRowAnimationProgress by animateFloatAsState(
targetValue = if (startChipsAnimation) 1f else 0f,
animationSpec = animSpec,
label = "animateChipRowProgress"
)
LaunchedEffect(startChipsAnimation) { shouldAnimateChips = startChipsAnimation }
LaunchedEffect(chipRowAnimationProgress) {
if (chipRowAnimationProgress >= 1) {
chipsAnimDone.value = true
}
}
Row(
modifier = Modifier
.onGloballyPositioned { coordinates ->
val position = coordinates.positionInRoot()
chipRowWidth = (coordinates.size.width).dp
player.endX.value = (position.x / density).dp
player.endY.value = (position.y / density).dp
}
.alpha(chipsAlpha.value)
.offset(x = chipsCurrentX - chipsEndX, y = chipsCurrentY - endY),
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
if (isRightPositioned) chipTextElement() else chipIconElement()
Spacer(modifier = Modifier.width(2.dp))
if (isRightPositioned) chipIconElement() else chipTextElement()
}
}
Chip Animation: Animates the chips based on the game events and player actions.
Alpha Effect: Controls the visibility of chips.
4. Updating Player UI Elements
Functions to update player cards and amounts based on game state.
Relevant Code:
fun updateCards(
player: Player,
eCards: List<EngineCard>,
shouldFlipPlayer: Boolean,
isShowDown: Boolean,
) {
if (eCards.isEmpty()) {
player.cards.forEach { card ->
if ((player.pos == 5) || !player.playerFolded.value) {
card.value = "back"
player.alpha.value = 1f
} else {
card.value = "back"
player.alpha.value = 0.4f
}
}
} else {
eCards.forEachIndexed { index, element ->
if ((player.pokerPlayer is HumanPokerPlayer) || isShowDown) {
player.cards[index].value = element.getCardName().lowercase()
if (shouldFlipPlayer) {
player.shouldFlipCard.value = true
}
} else {
player.cards[index].value = "back"
}
player.alpha.value = 1f
}
}
}
fun updateAmount(value: Long): String {
return if (value <= 0) " All in " else convertDoubleToString(value)
}
Card Updates: Sets the card images based on game state.
Amount Updates: Updates the player's chip count display.
EndGamePopUp Component
File: components/EndGamePopUp.kt
Purpose
The EndGamePopUp component is responsible for displaying a popup dialogue at the end of a game session. This dialog provides information about the player's performance, their score, and offers options to replay or view the scoreboard.
Detailed Explanation
Key Functionalities
Displaying the Initial End Game Popup
Handling User Actions
Displaying Final Results
Rendering the Component
1. Displaying the Initial End Game Popup
The component shows a popup with options to continue or end the session when the game ends. This initial popup differs based on the session mode (Challenge or Training).
Relevant Code:
if (!finalPopUp) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(40.dp, 36.dp, 40.dp, 33.dp)
) {
Text(
text = EndGamePopUpData.firstPopup.title,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 26.dp),
color = black
)
Text(
text = if (sharedViewModel.sessionMode == SessionMode.Challenge)
EndGamePopUpData.firstPopup.gameStateChallenge
else EndGamePopUpData.firstPopup.gameStateTraining,
style = ExtraTypographyStyles["mediumB"]!!,
modifier = Modifier.padding(bottom = 57.dp),
color = snwGreyishBrown,
textAlign = TextAlign.Center
)
// Buttons for confirmation and cancellation
}
}
Conditional Rendering: Shows different texts and options based on the session mode.
2. Handling User Actions
The component provides buttons for the user to confirm the end of the game or to dismiss the dialogue.
Relevant Code:
GenericButton(
height = 50.dp,
buttonState = ButtonState.Primary,
buttonText = ButtonText.OneMedium,
sharedViewModel = sharedViewModel,
textLines = if (sharedViewModel.sessionMode == SessionMode.Challenge)
listOf(EndGamePopUpData.firstPopup.confirmButtonTextChallenge)
else listOf(EndGamePopUpData.firstPopup.confirmButtonTextTraining)
) {
if ((!sharedViewModel.gameFinished || sharedViewModel.selectedMode == null) && sharedViewModel.sessionMode == SessionMode.Challenge) {
showEndConfirmDialog.value = false
navController.navigateUp()
} else if (sharedViewModel.sessionMode == SessionMode.Challenge) {
finalPopUp = true
} else {
finalPopUp = true
showEndConfirmDialog.value = false
sharedViewModel.waitingForTrainingFinish = true
onConfirm()
}
}
Buttons for Confirmation and Cancellation: Handles user actions to either confirm or dismiss the end game dialog.
State Management: Updates the state based on user actions and session mode.
3. Displaying Final Results
After the user confirms the end of the game, the component displays the final results, including the player's score and comparison with the best score.
Relevant Code:
if (sharedViewModel.sessionMode == SessionMode.Challenge || sharedViewModel.trainingFinished) {
if (!sharedViewModel.trainingFinished) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(40.dp, 21.dp, 40.dp, 16.dp)
.fillMaxWidth()
) {
Text(
text = EndGamePopUpData.secondPopup.title,
style = ExtraTypographyStyles["xlargeSB"]!!,
modifier = Modifier.padding(bottom = 9.dp),
color = black
)
Text(
text = EndGamePopUpData.secondPopup.subtitle,
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(bottom = 3.dp),
color = mediumGrey
)
Text(
text = String.format("%.2f", sharedViewModel.score),
style = ButtonTypographyStyles["heading"]!!,
color = snwGreenApple
)
// Additional UI for best score and replay options
}
}
}
Displaying Final Scores: Shows the final scores and comparisons with the best scores.
Animations: Uses Lottie animations to enhance the visual feedback.
4. Rendering the Component
The component renders the popup dialogue with all the necessary UI elements and handles visibility and interactions.
Relevant Code:
Dialog(
onDismissRequest = onDismissRequest,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Card(
colors = CardDefaults.cardColors(containerColor = white),
shape = RoundedCornerShape(8.dp),
elevation = CardDefaults.cardElevation(4.dp),
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 35.dp)
.border(1.dp, color = white, shape = RoundedCornerShape(8.dp))
) {
// Conditional rendering of initial and final popups
}
}
Dialogue Properties: Configures the dialogue properties and handles dismissal.
Card Component: Uses the
Cardcomponent to style the popup.
MoveProbs Component
File: components/MoveProbs.kt
Purpose
The MoveProbs component displays the probability of different poker moves in a visual and text format. It helps players understand the recommended move probabilities during the game.
Detailed Explanation
Key Functionalities
Displaying Move Information
Visual Representation of Move Probability
Handling Double Text for Special Moves
Rendering the Component
1. Displaying Move Information
This part of the component shows the move name and its associated probability. The move name is displayed in uppercase, and the probability is formatted as a percentage.
Relevant Code:
Text(
style = moveNameStyle,
text = moveName.uppercase(),
color = black,
modifier = Modifier
.width(65.5.dp)
.padding(end = 15.dp)
)
Text(
style = valueStyle,
text = "${convertDoubleToString((value * 100.0 * 1000).toLong())}%",
color = black,
modifier = Modifier
.padding(start = 15.dp + (maxWidth.value - currentWidth.value))
.onGloballyPositioned { layoutInfo ->
currentWidth.value = with(density) { layoutInfo.size.width.toDp() }
if (currentWidth.value > maxWidth.value) {
maxWidth.value = currentWidth.value
}
}
)
Move Name: Displayed in uppercase with a specific style.
Probability Value: Displayed as a percentage with dynamic width adjustment to ensure proper alignment.
2. Visual Representation of Move Probability
This part of the component represents the probability of the move as a progress bar, which gives a visual indication of the likelihood of each move.
Relevant Code:
Box(
modifier = Modifier
.weight(1f)
.background(snwWhite, RoundedCornerShape(4.dp))
.height(8.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth(value.toFloat())
.fillMaxHeight()
.background(progressColour, RoundedCornerShape(4.dp))
)
}
Outer Box: Acts as the container for the progress bar.
Inner Box: Fills the width based on the probability value to visually represent the likelihood of the move.
3. Handling Double Text for Special Moves
For moves that include additional text (like bet fractions), the component displays the move name and the additional text in a stacked format.
Relevant Code:
if (isDoubleText) {
Column(modifier = Modifier
.padding(end = 15.dp)
.width(50.dp)) {
Text(
style = moveNameStyle,
text = moveName.uppercase(),
color = black
)
Text(
style = potFractStyle,
text = potFract,
color = black
)
}
}
Conditional Check: Determines if the move should display additional text.
Column Layout: Stacks the move name and the additional text vertically.
4. Rendering the Component
The MoveProbs composable function arranges all the elements to display the move information and probability in a structured format.
Relevant Code:
@Composable
fun MoveProbs(
maxWidth: MutableState<Dp>,
moveName: String,
value: Double,
progressColour: Color,
topPadding: Dp,
bottomPadding: Dp = 0.dp,
isDoubleText: Boolean = false,
potFract: String = ""
) {
val moveNameStyle = ExtraTypographyStyles["xSmallM"] ?: TextStyle.Default
val valueStyle = ButtonTypographyStyles["medium"] ?: TextStyle.Default
val potFractStyle = ButtonTypographyStyles["tiny"] ?: TextStyle.Default
val density = LocalDensity.current
val currentWidth = remember { mutableStateOf(0.dp) }
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = topPadding, bottom = bottomPadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
// Conditional rendering for double text moves
// Move name and probability value display
// Visual representation of probability
}
}
Composable Function: Sets up the layout and styling for the move probability display.
Dynamic Adjustments: Ensures that the widths of elements are adjusted dynamically to fit the content.
PopUp Component
File: components/PopUp.kt
Purpose
The PopUp component displays a modal dialogue that allows the user to select a table size for a training session in the PokerSnowie app. It provides a user-friendly interface to choose from predefined options and confirm their selection.
Detailed Explanation
Key Functionalities
Displaying the PopUp Dialog
Table Size Selection
Rendering the Component
1. Displaying the PopUp Dialog
The PopUp composable function creates a modal dialogue using the Dialog composable. It uses a Surface for the dialogue's container to apply styling such as rounded corners and shadow.
Relevant Code:
Dialog(onDismissRequest = onDismissRequest) {
Surface(shape = RoundedCornerShape(8.dp), shadowElevation = 4.dp) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(30.dp, 36.dp, 30.dp, 33.dp)
) {
// Contents of the PopUp dialog
}
}
}
Dialogue: Creates a modal dialogue that can be dismissed by clicking outside.
Surface: Provides a styled container with rounded corners and shadow.
2. Table Size Selection
The table size selection is handled using a custom SelectorBox composable, which allows the user to navigate through a list of options. The selected index is tracked using a mutable state.
Relevant Code:
var tableSizeIndex by remember { mutableIntStateOf(0) }
SelectorBox(
sharedViewModel = sharedViewModel,
text = TrainingData.popupTextList[tableSizeIndex],
text2 = TrainingData.popupSubText["title"] as String,
currentIndex = tableSizeIndex,
listSize = TrainingData.popupTextList.size,
onLeftTap = { tableSizeIndex = max(0, tableSizeIndex - 1) },
onRightTap = { tableSizeIndex = min(TrainingData.popupTextList.size - 1, tableSizeIndex + 1) },
isPopupSelector = true
)
tableSizeIndex: Mutable state to track the current selected index.
SelectorBox: Custom composable for navigating through the list of table sizes.
onLeftTap: Decrements the index, ensuring it doesn't go below 0.
onRightTap: Increments the index, ensuring it doesn't exceed the list size.
3. Rendering the Component
The PopUp composable arranges the text, selector, and buttons within a column layout to render the complete dialogue. It includes a confirmation button to finalize the selection.
Relevant Code:
@Composable
fun PopUp(
sharedViewModel: SharedViewModel,
onDismissRequest: () -> Unit,
onConfirm: (Int) -> Unit
) {
var tableSizeIndex by remember { mutableIntStateOf(0) }
Dialog(onDismissRequest = onDismissRequest) {
Surface(shape = RoundedCornerShape(8.dp), shadowElevation = 4.dp) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(30.dp, 36.dp, 30.dp, 33.dp)
) {
Text(
text = "Table size",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 22.dp),
color = darkBlue
)
SelectorBox(
sharedViewModel = sharedViewModel,
text = TrainingData.popupTextList[tableSizeIndex],
text2 = TrainingData.popupSubText["title"] as String,
currentIndex = tableSizeIndex,
listSize = TrainingData.popupTextList.size,
onLeftTap = { tableSizeIndex = max(0, tableSizeIndex - 1) },
onRightTap = { tableSizeIndex = min(TrainingData.popupTextList.size - 1, tableSizeIndex + 1) },
isPopupSelector = true
)
Spacer(modifier = Modifier.height(61.dp))
GenericButton(
height = 50.dp,
width = 160.dp,
buttonState = ButtonState.Primary,
buttonText = ButtonText.OneMedium,
sharedViewModel = sharedViewModel,
textLines = listOf("CONFIRM"),
) {
onConfirm(TrainingData.popupTextList[tableSizeIndex].toInt())
onDismissRequest()
}
}
}
}
}
Text: Displays the title "Table size".
SelectorBox: Allows the user to select a table size.
GenericButton: Provides a confirmation button to finalize the selection and dismiss the dialog.
CloseGameButton Component
File: components/CloseGameButton.kt
Purpose
The CloseGameButton component is designed to handle the termination of a game session in the PokerSnowie app. It provides a button that changes state based on the current session status and allows the user to resign or end the session.
Detailed Explanation
Key Functionalities
Handling End Game Confirmation
Rendering the Button
State Management
1. Handling End Game Confirmation
The CloseGameButton component manages the visibility of the end-game confirmation dialogue. It uses the EndGamePopUp composable to show a confirmation dialogue when the user attempts to end the session.
Relevant Code:
val showEndConfirmDialog = remember { mutableStateOf(false) }
if (showEndConfirmDialog.value || sharedViewModel.trainingFinished || sharedViewModel.gameFinished) {
EndGamePopUp(
onDismissRequest = {
showEndConfirmDialog.value = false
sessionStatus.value = SessionStatus.Playing
closeBtnWidth.value = 46.dp
},
onConfirm = {
sessionStatus.value = SessionStatus.Stopped
},
sharedViewModel = sharedViewModel,
navigateTo = navigateTo,
navController = navController,
showEndConfirmDialog = showEndConfirmDialog
)
}
showEndConfirmDialog: State to manage the visibility of the confirmation dialogue.
EndGamePopUp: Displays the confirmation dialogue when
showEndConfirmDialogis true or when the game/training session is finished.
2. Rendering the Button
The CloseGameButton component renders a button that changes its appearance and behavior based on the sessionStatus. It uses different UI elements and animations to provide visual feedback for the current state.
Relevant Code:
Box(
modifier = Modifier
.size(width = closeBtnWidth.value, height = 40.dp)
.clickable {
manageTerminateSession(sessionStatus, showEndConfirmDialog)
}
.clip(
shape = RoundedCornerShape(
topStart = 20.dp,
bottomStart = 20.dp,
topEnd = 0.dp,
bottomEnd = 0.dp
)
)
.border(
width = 1.dp,
color = if (sessionStatus.value != SessionStatus.Stopped) snwGreyishBrown
else Color.Transparent,
shape = RoundedCornerShape(
topStart = 20.dp,
bottomStart = 20.dp,
topEnd = 0.dp,
bottomEnd = 0.dp
)
)
.background(white)
.animateContentSize(),
contentAlignment = Alignment.Center
) {
when (sessionStatus.value) {
SessionStatus.Playing -> {
Image(
painter = painterResource(R.drawable.end_arrow),
contentDescription = "Close button image",
modifier = Modifier.width(22.dp).height(20.dp)
)
}
SessionStatus.Stopping -> {
ExtraTypographyStyles["xSmallM"]?.let {
Text(
text = if (sharedViewModel.sessionMode == SessionMode.Challenge) "RESIGN" else "END SESSION",
style = it
)
}
closeBtnWidth.value = 167.dp
}
SessionStatus.Stopped -> {
ExtraTypographyStyles["xSmallM"]?.let {
Text(
text = if (sharedViewModel.sessionMode == SessionMode.Challenge) "RESIGNING..." else "ENDING SESSION...",
style = it
)
}
closeBtnWidth.value = 167.dp
}
}
}
Box: Container for the button with various modifiers applied for size, shape, border, background, and animation.
when (sessionStatus.value): Changes the button content based on the current session status.
SessionStatus.Playing: Displays an image.
SessionStatus.Stopping: Displays "RESIGN" or "END SESSION" text.
SessionStatus.Stopped: Displays "RESIGNING..." or "ENDING SESSION..." text.
3. State Management
The component uses mutable states to track the current session status and the width of the button. It also manages the confirmation dialogue's visibility and updates the session status accordingly.
Relevant Code:
val showEndConfirmDialog = remember { mutableStateOf(false) }
val closeBtnWidth = remember { mutableStateOf(46.dp) }
showEndConfirmDialog: Manages the visibility of the end-game confirmation dialogue.
closeBtnWidth: Tracks the width of the button, changing based on the session status.
Constants
Login Data Constants
File: constants/LoginData.kt
Purpose: The LoginData.kt file contains constant data used in the login screen, this is the text for the page titles and button text.
Key Functionalities:
Predefined Texts:
Contains predefined texts for the login screen, making it easy to manage and change the texts from a single location.
Example:
object LoginData { val logoHeaderData = arrayOf( mapOf("title" to "Texas Hold’em No Limit"), mapOf("title" to "Poker Artificial Intelligence") ) val loginButton = mapOf("title" to "LOGIN") val forgotText = mapOf("title" to "Forgot Password?") val createText = mapOf("title" to "Create an Account") }
Home Data Constants
File: constants/HomeData.kt
Purpose: The HomeData.kt file contains constant data used in the home screen, which is the text for headers, body content, and buttons.
Key Functionalities:
Predefined Texts:
Contains predefined texts for the home screen, making it easy to manage and change the texts from a single location.
Example:
object HomeData {
val homeBodyData = arrayOf(
mapOf("header" to "Play against the AI"),
mapOf("body" to "Using PokerSnowie will benefit every level of Player. Start today!"),
mapOf("challengeBtn" to "Challenge"),
mapOf("trainingBtn" to "Training")
)
val homeFooterData = arrayOf(
mapOf("header" to "What is PokerSnowie?"),
mapOf("body" to "PokerSnowie is a leading-edge Artificial Intelligence poker software and training tool, designed to improve the No Limit Hold'em Poker skills of all players. "),
mapOf("linkText" to "Learn more about us")
)
}Options Data Constants
File: constants/OptionsData.kt
Purpose: The OptionsData file contains constant data used in the options pages, the text for headers, body content, buttons, and default button values.
Key Functionalities:
Predefined Texts and Buttons:
Contains predefined texts and button values for the options pages, making it easy to manage and change the texts from a single location.
Example:
object OptionsData {
val pageTitle = mapOf("title" to "Options")
val sliderText = mapOf("title" to "Sounds")
val buttonsHeader = mapOf("title" to "Extra buttons")
val buttonsSubheader = mapOf("title" to "Pick your favourite 4 values:")
val buttons = listOf(
mapOf("button" to "1/5 POT"),
mapOf("button" to "1/4 POT"),
mapOf("button" to "1/3 POT"),
mapOf("button" to "1/2 POT"),
mapOf("button" to "2/3 POT"),
mapOf("button" to "3/4 POT"),
mapOf("button" to "1 POT"),
mapOf("button" to "2 POT"),
mapOf("button" to "ALL-IN")
)
val resetText = mapOf("title" to "Reset to default")
}
Challenge Data Constants
File: constants/ChallengeData.kt
Purpose: The ChallengeData file contains constant data used in the ChallengeSetup page, such as texts for headers, buttons, and default values for challenge settings.
Key Functionalities:
Predefined Texts and Buttons:
Contains predefined texts and button values for the challenge setup page, making it easy to manage and change the texts from a single location.
Example:
val logoText = mapOf("title" to "CHALLENGE") val subheader = mapOf("title" to "Are you Ready for the competition?") val tableText = mapOf("title" to "Table size") val tableSizeButtons = arrayOf( arrayOf(mapOf("number" to "5"), mapOf("text" to "SEATS")), arrayOf(mapOf("number" to "8"), mapOf("text" to "SEATS")), ) val modeText = mapOf("title" to "Mode") val modeButtons = arrayOf( arrayOf( mapOf("typeText" to "FAST"), mapOf("hands" to "20 HANDS"), mapOf("icon" to "fast_watch"), mapOf("mode" to ChallengeLength.Fast) ), arrayOf( mapOf("typeText" to "EXTENDED"), mapOf("hands" to "100 HANDS"), mapOf("icon" to "extended_watch"), mapOf("mode" to ChallengeLength.Long) ), )
Training Data Constants
File: constants/TrainingData.kt
Purpose: The TrainingData file contains constant data used in the TrainingSetup page, the text for headers, buttons, and default values for training settings.
Key Functionalities:
Predefined Texts and Buttons:
Contains predefined texts and button values for the training setup page, making it easy to manage and change the texts from a single location.
Example:
val heading = mapOf("title" to "Training setup") val tableSizeTitle = mapOf("title" to "Table size") val tableSizeButtons = arrayOf( arrayOf( mapOf("number" to "2"), mapOf("text" to "SEATS"), ), arrayOf( mapOf("number" to "5"), mapOf("text" to "SEATS"), ), arrayOf( mapOf("number" to "8"), mapOf("text" to "SEATS"), ), arrayOf( mapOf("number" to "..."), mapOf("text" to "CUSTOM"), ) ) val stakesTitle = mapOf("title" to "Stakes") val blindsList = listOf("0.5/1", "1/2", "2/4", "3/6", "5/10", "10/20", "15/30", "25/50") val stackSizeTitle = mapOf("title" to "Stack size") val stacksList = listOf( "Push or Fold (15BB)", "Shallow (40BB)", "Standard (100BB)", "Deep (200BB)" ) val anteText = mapOf("title" to "Ante (1 SB)") val adviceText = mapOf("title" to "Live advice") val iconName = mapOf("title" to "info") val buttonText = mapOf("title" to "PLAY") val popupTextList = listOf("3", "4", "6", "7", "9", "10") val popupSubText = mapOf("title" to "SEATS")
Image Data Constants
File: constants/ImageData.kt
Purpose: The ImageData file contains mappings of image resource IDs used in the SelectorBox and other components. It allows for easy reference and management of image resources.
Key Functionalities:
Image Resource Mappings:
Maps image names to their corresponding drawable resource IDs.
Example:
val imageResourceIds = mapOf( "fast_watch" to R.drawable.fast_watch, "extended_watch" to R.drawable.extended_watch, "info" to R.drawable.info )
Scoreboard Data Constants
File: constants/ScoreboardData.kt
Purpose: Contains constant data used in the scoreboard components, such as button labels and headings.
Key Functionalities:
Text Constants: Defines text for the scoreboard heading, reset button, and no score message.
Mode Buttons Data: Provides data for the mode selection buttons, including labels and icons
EndGamePopUpData Constants
File: constants/EndGamePopUpData.kt
Overview
The EndGamePopUpData object holds the static data used in the end-game popup dialogues. It defines the texts displayed in the two types of popups: one shown when the user initiates ending the session, and the other shown after the session has ended.
Structure
The object contains two main data classes:
FirstPopup: Data for the initial confirmation dialog.SecondPopup: Data for the final session-ended dialog.
Detailed Explanation
1. FirstPopup
This data class contains the text data for the first popup shown to the user when they attempt to end a session. It includes titles, game state descriptions, and button texts.
Fields:
title: The title of the popup.
gameStateChallenge: The description text for an ongoing challenge session.
gameStateTraining: The description text for an ongoing training session.
confirmButtonTextChallenge: The text for the confirmation button in a challenge session.
confirmButtonTextTraining: The text for the confirmation button in a training session.
cancelButtonText: The text for the cancel button.
Code:
data class FirstPopup(
val title: String,
val gameStateChallenge: String,
val gameStateTraining: String,
val confirmButtonTextChallenge: String,
val confirmButtonTextTraining: String,
val cancelButtonText: String
)
Example Data:
val firstPopup = FirstPopup(
title = "Are you sure...",
gameStateChallenge = "In progress",
gameStateTraining = "If yes, please finish your current hand in order to end the session",
confirmButtonTextChallenge = "YES, RESIGN",
confirmButtonTextTraining = "YES, END SESSION",
cancelButtonText = "KEEP PLAYING"
)
2. SecondPopup
This data class contains the text data for the final popup shown after the session has ended. It includes titles, subtitles, messages, and button texts.
Fields:
title: The title of the popup.
subtitle: The subtitle text, typically showing the user's error rate.
bestResultMessage: The message shown when the user improves their best result.
navigationText: The text for navigating to the scoreboard.
replayButtonText: The text for the replay button.
notBeatHighscoreText1: The first part of the message when the user does not beat their high score.
notBeatHighscoreText2: The second part of the message when the user does not beat their high score.
Code:
data class SecondPopup(
val title: String,
val subtitle: String,
val bestResultMessage: String,
val navigationText: String,
val replayButtonText: String,
val notBeatHighscoreText1: String,
val notBeatHighscoreText2: String
)
Example Data:
val secondPopup = SecondPopup(
title = "Session ended",
subtitle = "Your error rate is",
bestResultMessage = "Well done! You improved your best result",
navigationText = "Check your scoreboard",
replayButtonText = "PLAY AGAIN",
notBeatHighscoreText1 = "Your best result is",
notBeatHighscoreText2 = "Keep playing and improve your game skills"
)
SoundData Constants
File: constants/SoundData.kt
Overview
The SoundData object provides a centralized mapping of sound identifiers to their corresponding resource IDs. This allows for easy management and retrieval of sound resources used within the application.
Structure
The object consists of a single soundMap, which is a map of string keys (sound names) to integer values (resource IDs).
Detailed Explanation
soundMap
The soundMap is a Map<String, Int> that associates descriptive sound names with their corresponding resource IDs in the R.raw directory. This structure enables easy access to sound resources by their descriptive names.
Fields:
"Call": Maps to the resource ID for the "call" sound.
"Check": Maps to the resource ID for the "check" sound.
"dealCards": Maps to the resource ID for the sound of dealing cards.
"Fold": Maps to the resource ID for the "fold" sound.
"humanMoves": Maps to the resource ID for the sound associated with human moves.
"tablePot": Maps to the resource ID for the sound associated with the table pot.
Code:
object SoundData {
val soundMap = mapOf(
"Call" to R.raw.call,
"Check" to R.raw.check,
"dealCards" to R.raw.deal_cards,
"Fold" to R.raw.fold,
"humanMoves" to R.raw.human_moves,
"tablePot" to R.raw.table_pot,
)
}
Example Usage
When a specific sound needs to be played in the application, the soundMap can be used to retrieve the appropriate resource ID based on the sound's descriptive name. For instance, to play the "Fold" sound:
val foldSoundId = SoundData.soundMap["Fold"]
// Use foldSoundId to play the sound
TableData Constants
File: constants/TableData.kt
Overview
The TableData object provides a centralized mapping of card identifiers to their corresponding drawable resource IDs. This allows for easy management and retrieval of card images used within the application.
Structure
The object consists of a single cardImageResourceIds, which is a map of string keys (card identifiers) to integer values (drawable resource IDs).
Detailed Explanation
cardImageResourceIds
The cardImageResourceIds is a Map<String, Int> that associates card identifiers with their corresponding drawable resource IDs in the R.drawable directory. This structure enables easy access to card images by their identifiers.
Fields:
"back": Maps to the resource ID for the back of the card image.
"ac": Maps to the resource ID for the Ace of Clubs card image.
"2c": Maps to the resource ID for the Two of Clubs card image.
"3c": Maps to the resource ID for the Three of Clubs card image.
...
(continues for all card identifiers)
Example Usage
When a specific card image needs to be displayed in the application, the cardImageResourceIds can be used to retrieve the appropriate resource ID based on the card's identifier. For instance, to display the image of the Ace of Clubs:
val aceOfClubsImageId = TableData.cardImageResourceIds["ac"]
// Use aceOfClubsImageId to display the image
Templates
TemplateWrapper
File: templates/TemplateWrapper.kt
Overview
The TemplateWrapper file contains a composable function that sets up the UI template for the application. It primarily manages the system UI, specifically the status bar colour, using the Accompanist library's SystemUiController.
1. TemplateWrapper Composable Function
The TemplateWrapper composable function configures the system UI settings, such as the status bar colour, to match the application's theme.
Function Signature
@Composable
fun TemplateWrapper()
Parameters
This function does not take any parameters.
Functionality
System UI Controller: It uses
rememberSystemUiControllerfrom the Accompanist library to get a controller for changing system UI properties.Status Bar Color: Sets the status bar colour to the background colour of the current MaterialTheme, ensuring it is fully opaque by copying the colour and setting its alpha to 1f.
Side Effect: Uses
SideEffectto apply the status bar colour setting.SideEffectensures that this side effect runs whenever the composition is recomposed.
Example Usage:
This composable is designed to be used as a wrapper around the application's UI components to ensure consistent system UI settings.
@Composable
fun MyApp() {
TemplateWrapper()
// Your main composable content here
}
Explanation of Key Components:
rememberSystemUiController: Acquires an instance of
SystemUiControllerwhich allows manipulation of system UI elements.MaterialTheme.colorScheme.background: Fetches the background colour from the current MaterialTheme.
SideEffect: Ensures that the status bar colour change is applied during composition.
UI Theme
Overview
The ui.theme directory defines the visual styling of the application, including colours, typography, and themes. This ensures a consistent look and feel throughout the app.
Files and Their Roles
Color.kt: Defines the colour palette used in the application.
Theme.kt: Sets up the light and dark themes using the colours defined in
Color.ktand applies them throughout the app.Type.kt: Specifies the typography styles used in the application, including custom font families and text styles.
Color.kt
This file contains a set of predefined colour values used throughout the app to maintain a consistent colour scheme. Colours are defined using their hexadecimal values.
Theme.kt
This file defines the light and dark themes for the app. It uses the colours from Color.kt and applies them to various UI elements. The file contains two main composable functions:
LightTheme: Applies the light colour scheme.DarkTheme: Applies the dark colour scheme.
These functions are used to wrap the main content of the app to ensure the selected theme is applied.
Type.kt
This file defines the typography styles used in the app, including custom font families and specific text styles for different UI components. It uses the Montserrat font family and sets various text styles like headings, body text, and labels to ensure consistent typography throughout the app.
Utilities
Login Utilities
File: utils/LoginUtils.kt
Purpose: The LoginUtils.kt file contains utility functions for managing encrypted shared preferences used in the login screen.
Key Functionalities:
Encrypted Shared Preferences:
Provides functions to create and access encrypted shared preferences, ensuring sensitive data is securely stored.
Example:
fun getEncryptedSharedPreferences(context: Context): SharedPreferences {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
return EncryptedSharedPreferences.create(
context,
"encrypted_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
fun safeGetEncryptedSharedPreferences(context: Context): SharedPreferences {
return try {
getEncryptedSharedPreferences(context)
} catch (e: AEADBadTagException) {
Log.e("EncryptedPrefs", "Error accessing encrypted shared preferences", e)
context.getSharedPreferences("encrypted_prefs", Context.MODE_PRIVATE).edit().clear().apply()
getEncryptedSharedPreferences(context)
}
}Game Utilities
File: utils/GameUtils.kt
Purpose: The GameUtils file contains utility functions that are used throughout the application. These utilities handle tasks such as managing session termination, formatting values, and interacting with shared preferences.
Detailed Explanation:
Session Management
The manageTerminateSession function is responsible for managing the termination of game sessions. It updates the session status and controls whether a dialogue should be displayed to the user.
fun manageTerminateSession(
sessionStatus: MutableState<SessionStatus>,
showDialog: MutableState<Boolean>
) {
when (sessionStatus.value) {
SessionStatus.Playing -> {
sessionStatus.value = SessionStatus.Stopping
}
SessionStatus.Stopping -> {
showDialog.value = true
}
else -> {}
}
}Functionality:
SessionStatus.Playing: When the session is in the "Playing" state, it changes the state to "Stopping".
SessionStatus.Stopping: If the session is already stopping, it sets
showDialogto true, indicating that a confirmation dialog should be shown to the user.
2. Formatting Values
The convertDoubleToString function formats long values to strings with specific formatting rules. It is typically used for displaying monetary values or other numerical data.
fun convertDoubleToString(value: Long): String {
val symbols = DecimalFormatSymbols(Locale.US).apply {
decimalSeparator = '.'
}
val formatter = DecimalFormat().apply {
decimalFormatSymbols = symbols
maximumFractionDigits = 2
minimumIntegerDigits = 1
isGroupingUsed = false
}
return formatter.format(value.toDouble() / 1000)
}Functionality:
DecimalFormatSymbols: Sets the decimal separator to a period ('.').
DecimalFormat: Configures the formatter to use the specified symbols, and sets rules for maximum and minimum fraction digits, and disables grouping.
Conversion: Converts the input value from
LongtoDoubleand scales it by dividing by 1000 before formatting it to a string.
3. Shared Preferences Handling
The getOptionEnabled, getSelectedButtons, and setSelectedButtons functions interact with the shared preferences to store and retrieve user settings. These functions enable the application to remember user preferences between sessions.
Retrieving Preferences:
fun getOptionEnabled(key: String, context: Context): Boolean {
val sharedPrefs = context.getSharedPreferences("USER_PREFERENCES", Context.MODE_PRIVATE)
return sharedPrefs.getInt(key, 1) == 1
}
Functionality:
Retrieves an integer value from shared preferences using the provided key.
Returns
trueif the stored value is 1, indicating the option is enabled
fun getSelectedButtons(context: Context): List<String> {
val sharedPrefs = context.getSharedPreferences("USER_PREFERENCES", Context.MODE_PRIVATE)
val savedString = sharedPrefs.getString("selected_buttons", "1/4 POT,1/2 POT,1 POT,ALL-IN")
val selectedButtons = savedString?.split(",") ?: emptyList()
val buttonsOrder = listOf(
"1/5 POT",
"1/4 POT",
"1/3 POT",
"1/2 POT",
"2/3 POT",
"3/4 POT",
"1 POT",
"2 POT",
"ALL-IN"
)
return selectedButtons.sortedBy { buttonsOrder.indexOf(it) }
}
Functionality:
Retrieves a comma-separated string of selected button labels from shared preferences.
Splits the string into a list of button labels and sorts them according to a predefined order.
Storing Preferences:
fun setSelectedButtons(context: Context, selectedButtons: List<String>) {
val sharedPrefs = context.getSharedPreferences("USER_PREFERENCES", Context.MODE_PRIVATE)
with(sharedPrefs.edit()) {
putString("selected_buttons", selectedButtons.joinToString(","))
apply()
}
}Functionality:
Converts the list of selected button labels into a comma-separated string.
Stores the string in shared preferences under the key "selected_buttons".
4. Performing Logout
The performLogout function handles the process of logging out by disconnecting from the PokerSnowie API. This ensures that the user is properly logged out and the session is terminated.
fun performLogout() {
runBlocking {
try {
SnowieApp.pokerSnowieAPI.disconnect()
} catch (e: Exception) {
Log.e("Disconnect", "Failed to disconnect", e)
}
}
}Functionality:
runBlocking: Executes the disconnect operation in a blocking coroutine to ensure it completes before the function returns.
Disconnect: Calls the disconnect method on the PokerSnowie API to log the user out.
Error Handling: Logs an error message if the disconnect operation fails.
Scoreboard Utilities
File: utils/ScoreboardUtils.kt
Purpose: The ScoreboardUtils.kt file contains utility functions that handle tasks such as converting dates, clearing scores, and reading/writing scores to files. These utilities are used to manage the user's score data within the application.
Detailed Explanation:
1. Converting Dates
The convertDate function converts a date string from the format yyyy-MM-dd to a more human-readable format MMM dd, yyyy. If the date is the current date, it returns "Today".
fun convertDate(inputDateString: String): String {
val inputFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val outputFormat = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault())
val date = inputFormat.parse(inputDateString)
return if (date != null) {
val today = Calendar.getInstance().time
if (inputFormat.format(date) == inputFormat.format(today)) {
"Today"
} else {
outputFormat.format(date)
}
} else {
inputDateString
}
}Functionality:
Date Parsing: Parses the input date string using
SimpleDateFormat.Current Date Check: Compares the parsed date to the current date.
Formatting: Returns "Today" if the dates match, otherwise returns the formatted date string.
2. Clearing Scores
The clearScores function clears all scores by writing an empty JSON array to the specified file.
fun clearScores(context: Context, fileName: String) {
val file = File(context.filesDir, fileName)
if (file.exists()) {
val emptyJson = "[]"
FileOutputStream(file).use {
it.write(emptyJson.toByteArray())
}
}
}
Functionality:
File Handling: Checks if the file exists.
Clearing Content: Writes an empty JSON array (
"[]") to the file, effectively clearing its content.
3. Reading Scores
The readPlayerScoresFromFile function reads the scores from a JSON file and returns them as a list of Score objects.
fun readPlayerScoresFromFile(context: Context, fileName: String): List<Score> {
val gson = Gson()
val file = File(context.filesDir, fileName)
if (!file.exists()) {
val initialJson = "[]"
FileOutputStream(file).use {
it.write(initialJson.toByteArray())
}
}
val jsonString = FileInputStream(file).bufferedReader().use {
it.readText()
}
val listType = object : TypeToken<List<Score>>() {}.type
return gson.fromJson(jsonString, listType) ?: listOf()
}Functionality:
File Creation: Creates the file with an empty JSON array if it does not exist.
Reading Content: Reads the JSON content from the file.
Deserialization: Uses Gson to convert the JSON string into a list of
Scoreobjects.
4. Writing Scores
The writePlayerScoresToFile function writes a new score to the JSON file, adding it to the existing list of scores.
fun writePlayerScoresToFile(
context: Context,
fileName: String,
newScore: Float,
challengeType: ChallengeLength,
seatNumber: Int
) {
val newType = if (challengeType == ChallengeLength.Fast) "F$seatNumber" else "E$seatNumber"
val gson = Gson()
val listType = object : TypeToken<List<Score>>() {}.type
val file = File(context.filesDir, fileName)
if (!file.exists()) {
val initialJson = "[]"
FileOutputStream(file).use {
it.write(initialJson.toByteArray())
}
}
val existingJsonString = FileInputStream(file).bufferedReader().use {
it.readText()
}
val existingScores: MutableList<Score> = gson.fromJson(existingJsonString, listType) ?: mutableListOf()
val dateFormatter = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val date = dateFormatter.format(Date())
val newEntry = Score(date, newScore, newType)
existingScores.add(newEntry)
val updatedJsonString = gson.toJson(existingScores)
FileOutputStream(file).use {
it.write(updatedJsonString.toByteArray())
}
}Functionality:
Type Assignment: Determines the type of score based on
challengeTypeandseatNumber.File Handling: Ensures the file exists, creating it if necessary.
Reading Content: Reads existing scores from the file.
Adding New Score: Creates a new
Scoreobject and adds it to the existing list.Writing Content: Serializes the updated list of scores to JSON and writes it back to the file
UI Utilities
File: utils/UIUtils.kt
The UIUtils file contains utility functions and objects that assist in the UI development of the application. This includes custom modifiers for Compose and managing application status through state flows.
Contents
customShadowModifierAppStatusObject
1. customShadow Modifier
The customShadow function extends the Modifier class in Jetpack Compose to create a custom shadow effect for UI components.
Function Signature
fun Modifier.customShadow(
color: Color = Color.Black,
borderRadius: Dp = 0.dp,
blurRadius: Dp = 0.dp,
offsetY: Dp = 0.dp,
offsetX: Dp = 0.dp,
spread: Dp = 0f.dp,
modifier: Modifier = Modifier
): ModifierParameters
color: The color of the shadow. Default is
Color.Black.borderRadius: The radius of the corners of the shadow. Default is
0.dp.blurRadius: The blur radius of the shadow. Default is
0.dp.offsetY: The vertical offset of the shadow. Default is
0.dp.offsetX: The horizontal offset of the shadow. Default is
0.dp.spread: The spread of the shadow. Default is
0.dp.modifier: An additional modifier that can be chained. Default is
Modifier.
Functionality
The customShadow modifier applies a shadow with customizable properties to a UI element. It uses the drawBehind method to draw the shadow on the canvas with specified properties such as color, blur radius, offset, and spread.
Example Usage:
Box(
modifier = Modifier
.size(100.dp)
.customShadow(
color = Color.Gray,
borderRadius = 10.dp,
blurRadius = 8.dp,
offsetY = 4.dp,
spread = 2.dp
)
)2. AppStatus Object
The AppStatus object is used to manage and track the loading status and socket errors within the application. It uses MutableStateFlow to hold and observe state changes.
Properties
isLoading: A
MutableStateFlowthat tracks whether the application is in a loading state. Default isfalse.socketError: A
MutableStateFlowthat tracks whether there is a socket error. Default isfalse.socketErrorTitle: A
MutableStateFlowthat holds the title of the socket error.socketErrorMessage: A
MutableStateFlowthat holds the message of the socket error.socketErrorConfirm: A
MutableStateFlowthat holds a lambda function to be executed on socket error confirmation.
Methods
showLoading(): Sets
isLoadingtotrue.hideLoading(): Sets
isLoadingtofalse.showSocketError(title: String, message: String, onConfirm: () -> Unit): Displays a socket error with the specified title, message, and confirmation action.
hideSocketError(): Resets the socket error state.
Example Usage:
To show a loading indicator:
AppStatus.showLoading()To hide a loading indicator:
AppStatus.hideLoading()To show a socket error:
AppStatus.showSocketError("Error Title", "Error Message") {
// Confirmation action
}