PokerSnowie Mobile App Docs

Table of Contents

  1. Application Entry Components

    1.1. MainActivity

    1.2. SnowieApp

    1.3. SharedViewModel

  2. Socket and API Code

    2.1. Overview

    2.2. Utilities

    2.3. Exceptions

    2.4. Main API Class

    2.5 Additional Classes

  3. 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

  4. 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 Router composable.

    • 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.kt

  • Purpose: 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 PokerSnowieAPI

    • isFreeTrial: Manages the free trial state using a StateFlow

SharedViewModel

  • File: SharedViweModel.kt

  • Purpose: 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 StateFlow to 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.kt

  • Purpose: 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.salt
  • Details:

    • Properties: host, port, salt

    • Initialization: Reads config.propertiesfrom the assets folder.

    • Exception Handling: Throws RuntimeException if the configuration file cannot be read.

Md5

  • File: socket/util/Md5.kt

  • Purpose: 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): String

      • Computes and returns the MD5 hash of the input string.

      • Throws NoSuchAlgorithmException or IOException

Exceptions

APIException

  • File: socket/APIException.kt

  • Purpose: 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.kt

  • Purpose: Represents authentication-related exceptions.

  • Usage: throw AuthException("Authentication failed")

  • Details: Similar to APIExceptionbut specifically for authentication issues.

ConnectionException

  • File: socket/ConnectionException.kt

  • Purpose: 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 PokerSnowieAPI instance 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 DataInputStream for input and OutputStream for 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: ConnectionException on 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 Declaration object.

    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.kt

  • Purpose: Represents a poker hand with a unique ID and a deck of cards.

  • Usage: val hand = PokerSnowieHand(handId, deck)

  • Details:

    • Properties: id, deck

    • Constructors: Initializes a poker hand with ID and deck of cards.

Reachability

  • File: socket/Reachability.kt

  • Purpose: Monitors network connectivity status.

  • Usage:

    val reachability = Reachability.getInstance(context)
    val isNetworkAvailable = reachability.getNetworkStatus().value
  • Details:

    • NetworkCallback: Listens for network changes and updates the status.

    • StateFlow: Provides a flow of network status updates.

Engine

AnimationsEngine

  • File: AnimationsEngine.kt

  • Purpose: Manages animations and sound effects during the game.

  • Detailed Explanation: The AnimationsEngine class 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:

    1. 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.

    2. Queue Management:

      • Queue: A queue (LinkedList) to manage the animations.

      • Flags: Boolean flags to manage the state of animations and sounds.

    3. 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.

      • releaseMediaPlayer():

        • 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.kt

  • Purpose: Represents an ante action taken by a player.

  • Detailed Explanation: The AnteAction class models an ante (a forced bet) in poker, specifying which player posted the ante and the amount posted.

  • Core Components:

    1. Properties:

      • player: The player who posted the ante

      • amount: The amount of the ante

    2. Methods:

      • deepCopy():

        • Creates a deep copy of the AnteAction instance

      • toString()

        • Provides a string representation of the AnteAction.

  • Example Usage:

    val anteAction = AnteAction(player, 100)
    val anteActionCopy = anteAction.deepCopy()

BettingAction

  • File: BettingAction.kt

  • Purpose: Represents a betting action taken by a player.

  • Detailed Explanation: The BettingAction class 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:

    1. 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.

    2. Methods:

      • deepCopy():

        • Creates a deep copy of the BettingAction instance.

      • 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.kt

  • Purpose: Represents a blind action taken by a player.

  • Detailed Explanation: The BlindAction class 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:

    1. Properties:

      • type: The type of blind (SmallBlind, BigBlind, ExtraBlind).

      • amount: The amount of the blind.

      • player: The player posting the blind.

    2. Methods:

      • deepCopy():

        • Creates a deep copy of the BlindAction instance.

      • 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.kt

  • Purpose: Defines card ranks and suits, and provides a class for handling a single card.

  • Detailed Explanation: The EngineCard class models a playing card, including its rank and suit. It provides methods for creating, copying, and representing cards.

  • Core Components:

    1. Enums:

      • CardRank: Defines card ranks (Ace, 2, 3, ..., King).

      • CardSuit: Defines card suits (Club, Diamond, Heart, Spade).

    2. Properties:

      • suit: The suit of the card.

      • rank: The rank of the card.

    3. 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.kt

  • Purpose: Represents a deal action during the game.

  • Detailed Explanation: The DealAction class and its subclasses (HoleDealAction, CommunityAction) model the dealing of cards during different stages of the game.

  • Core Components:

    1. 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.

    2. HoleDealAction Class:

      • Represents dealing hole cards to a player.

      • Properties: player, card1, card2

      • Methods:

        • deepCopy(): Creates a deep copy of the hole deal action.

        • toString(): Provides a string representation of the hole deal action.

    3. CommunityAction Class:

      • Represents dealing community cards.

      • Properties: internalCards

      • Methods:

        • 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.kt

  • Purpose: Manages the deck of cards for the game.

  • Detailed Explanation:

    The EngineDeck class models a deck of cards, providing methods for deep copying the deck and drawing cards from it.

  • Core Components:

    1. Properties:

      • cards: A mutable list of EngineCard objects representing the deck.

    2. 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.kt

  • Purpose: Manages player moves during the game.

  • Detailed Explanation:

    The MoveManager class handles the logic for requesting and processing player moves (both human and AI) during a poker hand.

  • Core Components:

    1. 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.

    2. 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.kt

  • Purpose: Represents a player's rank and associated cards during a showdown.

  • Detailed Explanation:

    The PlayerRank class models a player's rank during a showdown, including the cards that form the rank and the player's position.

  • Core Components:

    1. 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.

    2. 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 PokerAction class models a generic action in poker, including the player making the action and the type of move.

  • Core Components:

    1. 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 PokerHand class is the core representation of a poker hand, managing all aspects of the game, including player actions, rounds, and determining the outcome.

  • Core Components:

    1. 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.

    2. 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.kt

  • Purpose: Represents a player in the poker game.

  • Detailed Explanation:

    The PokerPlayer class models a player, including their name, starting amount, and status. It has two subclasses: HumanPokerPlayer and AIPokerPlayer.

  • Core Components:

    1. Enums:

      • GameStatus: Defines player statuses (Playing, Allin, Folded).

    2. Properties:

      • name: Player's name.

      • startingAmount: Player's starting amount.

      • status: Player's status.

    3. Methods:

      • deepCopy():

        • Creates a deep copy of the player.

      • toString():

        • Provides a string representation of the player.

    4. 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 PokerRound class and its subclasses (PreFlop, Flop, Turn, River) model a round of poker, including actions and deals.

  • Core Components:

    1. Properties:

      • deals: List of deal actions.

      • actions: List of betting actions.

      • pokerHand: The current poker hand.

      • viewModel: Shared view model for the application.

    2. 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.

    3. 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.kt

  • Purpose: Computes the showdown rankings for players.

  • Detailed Explanation:

    The Showdowns class models the showdown phase, where player ranks are determined based on their hands.

  • Core Components:

    1. Properties:

      • playerRanks: List of player ranks.

      • pokerHand: The current poker hand.

      • community: Community cards.

    2. 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.kt

  • Purpose: Determines the winners and their respective pots in the poker game.

  • Detailed Explanation:

    The Winners class models the computation of pots and the determination of winners in the poker game.

  • Core Components:

    1. Properties:

      • mainPot: The main pot.

      • sidePots: List of side pots.

    2. 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.kt

  • Purpose:

    The Router composable function sets up the navigation graph and handles navigation transitions within the PokerSnowie application.

  • Detailed Explanation:

    The Router function 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:

    1. Route Enum:

      Defines the different routes available in the application.

      enum class Route {
          Login,
          Home,
          Options,
          OptionsList,
          OptionsSelect,
          ChallengeSetup,
          TrainingSetup,
          Scoreboard,
          GameController,
      }
    2. Router Composable 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
          }
      }
    3. Navigation Transitions:

      Defines common enter and exit transitions for navigation.

      fun commonEnterTransition() = slideInHorizontally(initialOffsetX = { it })
      fun commonExitTransition() = slideOutHorizontally()
    4. State Management:

      Use state to manage loading spinner and error dialogue visibility,

      var showSpinner by remember { mutableStateOf(false) }
      var showSocketError by remember { mutableStateOf(false) }
    5. 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

  1. User Authentication:

    • Connects to the PokerSnowieAPI to perform user login

    • Handles authentication state changes, displaying messages or navigating to the home screen upon successful login.

  2. Form Validation:

    • Validate email and password inputs.

    • Displays alert dialogues for invalid inputs or errors.

  3. State Management:

    • Uses Jetpack Compose state management for form inputs and loading states.

    • Utilizes SharedViewModel to share data across composables.

Complex Parts:

  1. Handling Login Status Changes:

    • The LaunchedEffect block reacts to changes in the loginStatus, tooManySessions, and other states from the PokerSnowieAPI.

    • 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
      }
  2. Asynchronous Operations with Coroutines:

    • Uses CoroutineScope to 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:

  1. Main Navigation:

    • Displays the navigation bar (NavBar) for easy access to settings and navigation.

    • Contains the main body (HomeBody) and footer (HomeFooter) sections.

  2. State Management:

    • Uses SharedViewModel to manage and share the application state.

  3. Responsive UI:

    • Adjusts the layout and element sizes based on the device screen size using sharedViewModel.percentageReduction.

Complex Parts:

  1. Navigation Handling:

    • Integrates the NavController for handling navigation.

    • Example:

      NavBar(navigateTo = navigateTo, navController = navController, alternativeIcon = true)
  2. 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:

  1. 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)
          }
      )
  2. Navigation:

    • Provides a link to navigate to the OptionsList page for additional settings.

Complex Parts:

  1. State Management for Switch:

    • Manages the state of the switch using remember and mutableStateOf.

    • Example:

      var switchState by remember { mutableStateOf(getOptionEnabled(soundsKey, context)) }
  2. 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()
          }
      }
      
  3. Navigation Handling:

    • Handles navigation to the OptionsList page.

    • 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:

  1. Display Options:

    • Lists available options for extra buttons.

    • Allows users to select an option to customize their button layout.

  2. State Management:

    • Manages the state of selected buttons using remember and mutableStateOf.

Complex Parts:

  1. State Management:

    • Manages the state of selected buttons.

    • Example:

      val initialSelectedButtons = remember { mutableStateOf(getSelectedButtons(context)) }
  2. Navigation Handling:

    • Navigates to the OptionsSelect page when an option is clicked.

    • Example:

      .clickable {
          sharedViewModel.selectedButton = buttonString
          navigateTo(Route.OptionsSelect)
      }
      
  3. 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:

  1. Display Available Options:

    • Lists all available button options.

    • Indicates the currently selected option with a checkmark.

  2. Update Selected Option:

    • Allows users to update the selected option and saves it in shared preferences.

Complex Parts:

  1. State Management:

    • Manages the state of the selected buttons.

    • Example:

      var selectedButtons = getSelectedButtons(context)
  2. 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:

  1. ViewModel Initialization:

    • Initializes the SharedViewModel with default values for a new challenge session.

    • Example:

      sharedViewModel.sessionMode = SessionMode.Challenge
      sharedViewModel.performLiveAdvice = true
      sharedViewModel.gameFinished = false
      sharedViewModel.handsCounter = 0
      sharedViewModel.score = 0f
      
  2. Component Composition:

    • Composes the ChallengeHeader and ChallengeSettings components.

    • 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:

  1. ViewModel Initialization:

    • Initializes the SharedViewModel with 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
      
  2. Component Composition:

    • Composes the NavBar and TrainingBody components.

    • 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 ScoreboardScores composable.

  • 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:

  1. Customization:

    • Supports different button states (Primary, Secondary, etc.) that alter the button's appearance.

    • Displays different text styles and optional icons.

  2. Dynamic Styling:

    • Adjusts its size and shape based on parameters and SharedViewModel state.

Complex Parts:

  1. 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)
      }
  2. Content Arrangement:

    • Uses Row and Column composables 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:

  1. Display Alerts:

    • Shows a dialogue with a title, message, and buttons for user actions.

    • Customizable with different button texts and actions.

  2. User Interaction:

    • Provides callback functions (onConfirm, onDismissRequest) to handle user actions.

Complex Parts:

  1. Dialog Properties:

    • Uses DialogProperties to customize the dialogue's behaviour.

    • Example:

    Dialog(
        onDismissRequest = onDismissRequest,
        properties = DialogProperties(usePlatformDefaultWidth = false)
    ) {
        // Dialog content
    }
    
  2. 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:

  1. Display Logo:

    • Shows the application logo.

  2. 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:

  1. Customizable Input Field:

    • Supports hints, input values, and validation.

  2. Form Validation:

    • Validates email input and displays an error message if the email is invalid.

Complex Parts:

  1. 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:

  1. Display Content:

    • Shows a header, description, and buttons with text from HomeData.

  2. Navigation:

    • Provides buttons to navigate to the Challenge Setup and Training Setup pages.

Complex Parts:

  1. Dynamic Scaling:

    • Adjusts the size of text and buttons based on sharedViewModel.percentageReduction.

    • Example:

      val scaleFactor = 1 - (sharedViewModel.percentageReduction / 100)
  2. 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:

  1. Display Footer Content:

    • Shows a header, body text, and a link with text from HomeData.

  2. External Links:

    • Provides clickable text and an icon to navigate to an external website.

Complex Parts:

  1. Dynamic Scaling:

    • Adjusts the size of text and icons based on sharedViewModel.percentageReduction.

    • Example:

      val scaleFactor = 1 - (sharedViewModel.percentageReduction / 100)
  2. External Links Handling:

    • Uses Intent to 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:

  1. Navigation Handling:

    • Provides back navigation and access to settings.

    • Displays the app logo if required.

  2. Dynamic Configuration:

    • Configurable to show different icons or handle different actions based on parameters.

Complex Parts:

  1. 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:

  1. 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

    • animateColorAsState is used to change the background colour based on the isChecked state. When isChecked is true, the background colour transitions to snwGreenApple, and when false, it transitions to snwWhite.

    • Example:

      val backgroundColor by animateColorAsState(
          if (isChecked) snwGreenApple else snwWhite, label = "switchBgColorAnimation"
      )
  • Position Animation:

    • animateDpAsState is used to animate the horizontal position of the switch button. The button moves to 18.5.dp when isChecked is true and returns to 0.dp when false.

    • Example:

      val position by animateDpAsState(
          if (isChecked) 18.5.dp else 0.dp, label = "switchButtonAnimation"
      )
  1. 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) }
    )
  1. 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 Box adjusts its color and shape based on the isChecked state.

    • 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 Box moves 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:

  1. Header Layout:

    • Uses Box and Column to 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)
          )
      }
  2. Navigation Bar Integration:

    • Includes the NavBar component for navigation.

    • Example:

      NavBar(navController = navController)
  3. Dynamic Scaling:

    • Adjusts the scale of UI elements based on the percentageReduction value in the SharedViewModel.

    • 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:

  1. Dynamic Scaling:

    • Similar to the header, the ChallengeSettings component scales UI elements based on the percentageReduction value.

    • Example:

      val scaleFactor = 1 - (sharedViewModel.percentageReduction / 100)
  2. Table Size Selection:

    • Provides options for selecting the table size using GenericButton components.

    • 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()
          }
      }
      
  3. Challenge Mode Selection:

    • Allows users to choose between different challenge modes using GenericButton components.

    • 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
          }
      }
      
  4. Starting the Challenge:

    • Includes a button to start the challenge, navigating to the GameController route.

    • 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) }
      )
      
  5. 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) }
      )
      
  6. 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:

  1. Dynamic Scaling:

    • Adjusts the scale of UI elements based on the percentageReduction value in the SharedViewModel.

    • Example:

      val scaleFactor = 1 - (sharedViewModel.percentageReduction / 100)
  2. 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)
              }
          )
      }
  3. Table Size Selection:

    • Provides options for selecting the table size using GenericButton components.

    • 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)
              }
          }
      }
  4. Stakes Selection:

    • Allows users to choose stakes using the SelectorBox component.

    • 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)
          }
      )
  5. Stack Size Selection:

    • Allows users to choose the stack size using the SelectorBox component.

    • 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})"
      }
  6. 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)
          }
      )
  7. Starting the Training:

    • Includes a button to start the training session, navigating to the GameController route.

    • 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:

  1. Dynamic Scaling:

    • The component uses the percentageReduction value from the SharedViewModel to 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)
  2. Custom Switch Handling:

    • When isSwitch is true, a CustomSwitch component is rendered. The switch state is managed locally using a remember block. When the switch state changes, the provided onSwitchValueChange callback 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)
              }
          )
      }
      
  3. Arrow Buttons for Navigation:

    • The left and right arrow buttons allow users to navigate through a list of options. The onLeftTap and onRightTap functions handle the logic for updating the currentIndex when 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)
          )
      }
      
  4. Popup Selector Handling:

    • Displays additional text and handles layout differently when isPopupSelector is true. When isPopupSelector is 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:

  1. State Management:

    • The component uses SnapshotStateList to 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)
          }
      }
  2. 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)
          }
      }
  3. Custom Grid Layout:

    • The grid layout is managed using the Layout composable, 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:

  1. State Initialization:

    • The component initializes the state of each button using the SnapshotStateList to ensure that the selection states are managed efficiently and reactively. The initial states are loaded using a LaunchedEffect, which ensures that the initialization code runs only once when the composable is first composed.

  2. 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.

  3. Custom Layout:

    • The Layout composable 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 like Row or Column. 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:

  • curPlayer and curRound: 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.

  • foldIsHidden and betRaiseIsHidden: 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 humanMovesTrigger state 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:

  1. Clear player bet/raise probability data if the best raise was made.

  2. Populate bestMoveString and probability data for fold, check/call, and bet/raise moves.

  3. Set isVisible to 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 Box and Column elements manage the layout and styling.

  • Text elements display the advice.

  • GenericButton components 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

  1. Displaying Session Information

  2. Handling Scoreboard Display

  3. 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 showScoreboardDialog is 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

  1. Animating Card Movements and Flips

  2. Displaying Player Information

  3. Handling Chip Animations

  4. 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.shouldFlipCard state.

  • Animation Effect: Triggers an animation when shouldFlipCard state 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

  1. Displaying the Initial End Game Popup

  2. Handling User Actions

  3. Displaying Final Results

  4. 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 Card component 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

  1. Displaying Move Information

  2. Visual Representation of Move Probability

  3. Handling Double Text for Special Moves

  4. 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

  1. Displaying the PopUp Dialog

  2. Table Size Selection

  3. 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

  1. Handling End Game Confirmation

  2. Rendering the Button

  3. 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 showEndConfirmDialog is 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:

  1. 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:

  1. 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:

  1. 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:

  1. 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:

  1. 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:

  1. 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 rememberSystemUiController from 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 SideEffect to apply the status bar colour setting. SideEffect ensures 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 SystemUiController which 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

  1. Color.kt: Defines the colour palette used in the application.

  2. Theme.kt: Sets up the light and dark themes using the colours defined in Color.kt and applies them throughout the app.

  3. 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:

  1. 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:

  1. 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 showDialog to 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 Long to Double and 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 true if 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 Score objects.

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 challengeType and seatNumber.

  • File Handling: Ensures the file exists, creating it if necessary.

  • Reading Content: Reads existing scores from the file.

  • Adding New Score: Creates a new Score object 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

  1. customShadow Modifier

  2. AppStatus Object

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
): Modifier

Parameters

  • 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 MutableStateFlow that tracks whether the application is in a loading state. Default is false.

  • socketError: A MutableStateFlow that tracks whether there is a socket error. Default is false.

  • socketErrorTitle: A MutableStateFlow that holds the title of the socket error.

  • socketErrorMessage: A MutableStateFlow that holds the message of the socket error.

  • socketErrorConfirm: A MutableStateFlow that holds a lambda function to be executed on socket error confirmation.

Methods

  • showLoading(): Sets isLoading to true.

  • hideLoading(): Sets isLoading to false.

  • 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
}