peterduhon / skia-poker-morph

Trustless Tricksters: A decentralized poker game built on Morph zkEVM. Featuring secure card shuffling, Chainlink VRF for fairness, Galadriel AI agents, XMTP chat, and Web3Auth login. Experience the future of poker on the blockchain. 🃏🔐🚀
3 stars 0 forks source link

Develop GameLogic smart contract #2

Open peterduhon opened 2 months ago

peterduhon commented 2 months ago

[SP-2] Develop GameLogic smart contract

Summary:
Develop the core GameLogic smart contract for Skia Poker, including the structure, state variables, and game functions.

Description:
This task involves defining the structure and state variables of the GameLogic contract, implementing the core functions for dealing cards, managing turns, and determining winners, and integrating Chainlink VRF for provably fair random number generation.

Acceptance Criteria:

Tasks:

  1. Define Contract Structure:
    • Outline the core structure of the GameLogic contract.
    • Define the necessary state variables, such as player states, card decks, and game phases.
  2. Implement Core Functions:
    • Implement functions for dealing cards to players.
    • Develop logic for managing player turns and game phases.
    • Implement winner determination logic based on poker hand rankings.
  3. Integrate Chainlink VRF:
    • Set up and integrate Chainlink VRF for random number generation.
    • Ensure the randomness is securely and efficiently used in the game logic.
  4. Testing:
    • Write unit tests for all functions to ensure correct functionality.
    • Test the integration of Chainlink VRF for provably fair outcomes.

NOTES:

Min 2 player, Max 10

Resources:

Initial GameLogic Smart Contract (Prototype) - Google Docs

Priority: High

Assignee: @tetyana-pol and @JW-dev0505 2024 Due Date: 8/29/

peterduhon commented 2 months ago

Great work! Here are a few more tweaks to make the contract even more resilient.

Gas Optimization:

Specific suggestions for the shuffleDeck and initializeDeck functions, as well as general optimization tips:

a) shuffleDeck function:

function shuffleDeck(uint256 randomness) internal {
    for (uint256 i = deck.length - 1; i > 0; i--) {
        uint256 j = randomness % (i + 1);
        (deck[i], deck[j]) = (deck[j], deck[i]);
    }
}

Optimization suggestions:

Optimized version:

function shuffleDeck(uint256 randomness) internal {
    for (uint256 i = 51; i > 0; i--) {
        uint256 j = randomness % (i + 1);
        assembly {
            let ptr := deck.slot
            let iValue := sload(add(ptr, i))
            let jValue := sload(add(ptr, j))
            sstore(add(ptr, i), jValue)
            sstore(add(ptr, j), iValue)
        }
        randomness = uint256(keccak256(abi.encode(randomness)));
    }
}

b) initializeDeck function:

function initializeDeck() internal {
    uint256 index = 0;
    for (uint256 suit = 0; suit < 4; suit++) {
        for (uint256 value = 0; value < 13; value++) {
            deck[index] = suit * 13 + value;
            index++;
        }
    }
}

Optimization suggestions:

Optimized version:

uint256[52] private constant INITIAL_DECK = [
    0,1,2,3,4,5,6,7,8,9,10,11,12,
    13,14,15,16,17,18,19,20,21,22,23,24,25,
    26,27,28,29,30,31,32,33,34,35,36,37,38,
    39,40,41,42,43,44,45,46,47,48,49,50,51
];

function initializeDeck() internal {
    deck = INITIAL_DECK;
}

General optimization tips:

Testing:

Crucial for complex logic like hand evaluation and winner determination. Here's an approach to testing these functions:

a) Set up a testing framework: Use a framework like Hardhat or Truffle for testing.

b) Write unit tests for hand evaluation functions:

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("PokerGame", function () {
  let PokerGame;
  let pokerGame;

  beforeEach(async function () {
    PokerGame = await ethers.getContractFactory("PokerGame");
    pokerGame = await PokerGame.deploy(/* constructor arguments */);
    await pokerGame.deployed();
  });

  describe("Hand Evaluation", function () {
    it("Should correctly identify a Royal Flush", async function () {
      const hand = [
        {suit: 0, value: 9},  // 10 of Spades
        {suit: 0, value: 10}, // Jack of Spades
        {suit: 0, value: 11}, // Queen of Spades
        {suit: 0, value: 12}, // King of Spades
        {suit: 0, value: 13}, // Ace of Spades
        {suit: 1, value: 0},  // Two of Hearts (irrelevant)
        {suit: 2, value: 1}   // Three of Diamonds (irrelevant)
      ];
      expect(await pokerGame.evaluateHand(hand)).to.equal(ethers.BigNumber.from(HandRanking.RoyalFlush));
    });

    // Add similar tests for other hand types
  });

  describe("Winner Determination", function () {
    it("Should correctly determine the winner with different hand rankings", async function () {
      // Set up a game state with multiple players and known hands
      // Call the determineWinners function
      // Assert that the correct winner(s) are returned
    });

    it("Should correctly split the pot in case of a tie", async function () {
      // Set up a game state with multiple players and identical best hands
      // Call the determineWinners function
      // Assert that all players with the best hand are returned as winners
    });

    // Add more tests for complex scenarios, including side pots
  });
});

c) Test edge cases:

d) Fuzz testing: Consider implementing fuzz testing, where random inputs are generated to test the contract's robustness.

e) Integration tests: Write tests that simulate entire game flows, from start to finish, including betting rounds and winner determination.

f) Gas usage tests: Implement tests to measure and assert on the gas usage of key functions, ensuring they remain within acceptable limits.

By implementing these optimizations and comprehensive tests, we ensure that the PokerGame contract is not only functionally correct but also efficient and robust.

peterduhon commented 2 months ago

3 Key Areas of focus in this comment: Documentation, Event Emissions, Access Control.

  1. Documentation (NatSpec format):

NatSpec (Ethereum Natural Language Specification Format) is a standard way to provide rich documentation for Solidity contracts. Here's how to improve documentation using NatSpec:

/// @title PokerGame Smart Contract
/// @author CoolMan
/// @notice This contract manages a decentralized poker game
/// @dev Implements core poker logic, including hand evaluation and winner determination

contract PokerGame is VRFConsumerBase, Ownable, ReentrancyGuard {
    // ... existing code ...

    /// @notice Evaluates a poker hand and returns its ranking
    /// @dev Uses bitwise operations for efficient hand evaluation
    /// @param hand An array of 7 cards (2 hole cards + 5 community cards)
    /// @return A uint256 representing the hand's value, higher is better
    function evaluateHand(Card[] memory hand) internal pure returns (uint256) {
        // ... existing code ...
    }

    /// @notice Determines the winner(s) of the current game
    /// @dev Compares hands of all active players
    /// @return An array of addresses representing the winner(s)
    function determineWinners() internal view returns (address[] memory) {
        // ... existing code ...
    }

    // ... more functions ...
}
  1. Event Emissions:

Events are crucial for off-chain applications to track on-chain state changes. Here are some examples of where to add or improve event emissions:

// Add these event definitions
event GamePhaseChanged(GamePhase newPhase);
event PlayerActionTaken(address indexed player, PlayerAction action, uint256 amount);
event PotUpdated(uint256 mainPot, uint256[] sidePots);

// In the advancePhase function
function advancePhase() external onlyOwner {
    // ... existing code ...
    emit GamePhaseChanged(currentPhase);
}

// In the manageTurn function
function manageTurn(uint256 betAmount) external isGameActive isPlayer nonReentrant {
    // ... existing code ...
    emit PlayerActionTaken(msg.sender, player.action, betAmount);
}

// After updating pots
function updatePots() internal {
    // ... pot update logic ...
    uint256[] memory sidePotAmounts = new uint256[](sidePots.length);
    for (uint i = 0; i < sidePots.length; i++) {
        sidePotAmounts[i] = sidePots[i].amount;
    }
    emit PotUpdated(mainPot, sidePotAmounts);
}
  1. Access Control:

Review the use of onlyOwner and consider if more granular access control is needed. Here are some considerations:

// Consider creating more specific roles
bytes32 public constant DEALER_ROLE = keccak256("DEALER_ROLE");
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");

// In the constructor
constructor(...) {
    // ... existing code ...
    _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
    _setupRole(DEALER_ROLE, msg.sender);
}

// Replace onlyOwner with more specific roles where appropriate
function startGame() external onlyRole(DEALER_ROLE) {
    // ... existing code ...
}

function setMinimumBuyIn(uint256 amount) external onlyRole(ADMIN_ROLE) {
    // ... implementation ...
}

// For functions that should only be callable by the contract itself
modifier onlyInternal() {
    require(msg.sender == address(this), "PokerGame: caller is not the contract");
    _;
}

function someInternalFunction() internal onlyInternal {
    // ... implementation ...
}

By implementing these improvements:

  1. Your contract becomes self-documenting, making it easier for other developers (including future you) to understand and maintain the code.
  2. Off-chain applications can easily track important state changes, improving the overall user experience and enabling more complex integrations.
  3. The contract becomes more secure by ensuring that only authorized addresses can perform sensitive operations, and the authorization is more granular and specific to the required permissions.

Remember to test thoroughly after making these changes to ensure that the new modifiers and event emissions don't introduce any unexpected behaviors or gas cost increases.

peterduhon commented 2 months ago

1. BettingAndPotManagement.sol:

2. GameMechanics.sol:

Improvements and suggestions:

1. Event Emissions:

2. Access Control:

3. Error Handling:

4. Gas Optimization:

5. Consistency:

6. Documentation:

7. Testing:

8. Upgradability:

Overall, the separation of concerns is well done, spectacular! The next steps would be to:

  1. Implement the suggested improvements.
  2. Review and optimize the CardManagement and UserManagement contracts.
  3. Develop a comprehensive test suite.
  4. Consider implementing an upgradability pattern.

This structure provides a solid foundation for our poker game. Great progress!

peterduhon commented 2 months ago

FAQ: "uint256 public roundNumber" and "GamePhase public currentPhase", are they the same thing?

No.

The roundNumber and currentPhase serve different purposes in the game, and they are not the same thing, although they are related. Here's an explanation to clarify:

  1. roundNumber (uint256 public roundNumber):

    • Purpose: This variable tracks the number of rounds that have taken place in the game. Each time a new round starts, this number is incremented. It's useful for keeping a historical record of how many rounds have occurred, which can be helpful for analytics, game history, or tracking player performance over multiple rounds.
    • Usage Example: You might use roundNumber to display information to players about which round they are currently playing, or to trigger specific events after a certain number of rounds have passed.
  2. currentPhase (GamePhase public currentPhase):

    • Purpose: This variable represents the current phase of the game within a single round. The game typically goes through several phases in each round (e.g., PreFlop, Flop, Turn, River, Showdown). currentPhase indicates which stage of the round the game is currently in.
    • Usage Example: The game logic uses currentPhase to determine what actions are allowed at any given time (e.g., you can only deal community cards during certain phases, or betting might only be allowed in specific phases).

Why Both Are Needed:

In summary, while both roundNumber and currentPhase help in managing the game state, they serve different roles in tracking and controlling the game's progress. If you only used one, you would lose the ability to either track the number of rounds (if you removed roundNumber) or manage the phases of a round (if you removed currentPhase).

tetyana-pol commented 2 months ago

thank you

peterduhon commented 2 months ago

Hi @tetyana-pol ,

Great to hear you’re working on splitting the determineWinners function! For the evaluateHand function, I have a couple of suggestions on how you can break it down into smaller, more manageable functions:

  1. Separate Hand Type Evaluation:
    Consider breaking down the evaluateHand function into smaller, specific functions for each hand type. This way, each function focuses on evaluating one specific hand, making the code more modular and easier to maintain. Here are two examples:

    • Example 1: isRoyalFlush()

      function isRoyalFlush(Card[] memory hand) internal pure returns (bool) {
       // Logic to check if the hand is a Royal Flush
       // Example: return (suit is the same and contains 10, J, Q, K, A)
      }
    • Example 2: isStraightFlush()

      function isStraightFlush(Card[] memory hand) internal pure returns (bool) {
       // Logic to check if the hand is a Straight Flush
       // Example: return (suit is the same and the values are consecutive)
      }

    Then, in the evaluateHand function, you can simply call these smaller functions in sequence to determine the hand's rank. For example:

    function evaluateHand(Card[] memory hand) internal pure returns (uint256) {
       if (isRoyalFlush(hand)) return HandRanking.RoyalFlush;
       if (isStraightFlush(hand)) return HandRanking.StraightFlush;
       // Continue with other hand evaluations...
       return HandRanking.HighCard;
    }

This approach will make the code more readable and easier to debug or extend in the future.

Please let me know if this approach works for you or if you need any further assistance! Happy to discuss more if needed.

Best Pete

tetyana-pol commented 2 months ago

thank you

peterduhon commented 2 months ago

Skia Poker: Game Phase Progression Analysis

Current Implementation

Based on @tetyana-pol analysis

Analysis

  1. Single Call Point:

    • Having advancePhase() called from only one place (within call()) might be limiting.
    • Other actions like fold() or raise() might also need to trigger phase advancement in certain scenarios.
  2. Conditional Advancement:

    • The check if isRoundComplete before calling advancePhase() is good practice.
    • It ensures that the phase only advances when all players have acted.
  3. Missing Scenarios:

    • The current implementation might miss advancing the phase if the last player folds or if all but one player have folded.
  4. Flexibility Concerns:

    • Tying phase advancement solely to the call() function might not account for all poker game scenarios.

Recommendations

  1. Centralize Phase Advancement:

    • Create a separate function to check if the phase should advance, which can be called after any player action.
      function checkAndAdvancePhase() internal {
      if (isRoundComplete()) {
         advancePhase();
      }
      }
  2. Call from Multiple Points:

    • Call checkAndAdvancePhase() at the end of call(), fold(), raise(), and any other relevant player actions.
  3. Handle Edge Cases:

    • Implement logic to advance the phase if all but one player have folded.
    • Ensure the phase advances correctly when the last player in the round acts.
  4. Add Phase Transitions:

    • Clearly define the different phases (e.g., pre-flop, flop, turn, river, showdown).
    • Implement specific actions for each phase transition (e.g., dealing community cards).
  5. Event Emission:

    • Emit an event when the phase changes to facilitate frontend updates and logging.
      event PhaseAdvanced(GamePhase newPhase);

Example Implementation

enum GamePhase { PreFlop, Flop, Turn, River, Showdown }

GamePhase public currentPhase;

function checkAndAdvancePhase() internal {
    if (isRoundComplete() || allButOnePlayerFolded()) {
        advancePhase();
    }
}

function advancePhase() internal {
    if (currentPhase == GamePhase.PreFlop) {
        currentPhase = GamePhase.Flop;
        dealCommunityCards(3); // Deal flop
    } else if (currentPhase == GamePhase.Flop) {
        currentPhase = GamePhase.Turn;
        dealCommunityCards(1); // Deal turn
    } else if (currentPhase == GamePhase.Turn) {
        currentPhase = GamePhase.River;
        dealCommunityCards(1); // Deal river
    } else if (currentPhase == GamePhase.River) {
        currentPhase = GamePhase.Showdown;
        initiateShowdown();
    }
    emit PhaseAdvanced(currentPhase);
}

function manageTurn(/* parameters */) external {
    // Existing logic
    checkAndAdvancePhase();
}

function call() internal {
    // Existing logic
    checkAndAdvancePhase();
}

function fold() internal {
    // Existing logic
    checkAndAdvancePhase();
}

function raise(uint256 amount) internal {
    // Existing logic
    checkAndAdvancePhase();
}

This structure ensures that the game phase advances correctly after any player action, handling all possible scenarios in a Texas Hold'em poker game.

Summary:

This analysis addresses the current implementation of game phase progression in Skia Poker. The existing approach, which relies on a single call to advancePhase() within the call() function, may be too limited to handle all possible scenarios in the game. This document recommends centralizing phase advancement logic into a new function, checkAndAdvancePhase(), which can be called after any player action. This ensures that the game phase advances correctly, even in edge cases where all but one player has folded or when the last player in the round acts. The proposed solution also includes example code for clear implementation.

These changes aim to enhance the flexibility, reliability, and clarity of the game’s phase progression, ensuring that all potential scenarios in a Texas Hold'em game are adequately handled. Please review the recommendations and example implementation provided, and feel free to share any feedback or questions.

tetyana-pol commented 2 months ago

thank you for suggestion

peterduhon commented 2 months ago

Chainlink VRF Integration in the HandEvaluator.sol

Contract

Overview of Chainlink VRF Integration

Chainlink's Verifiable Random Function (VRF) is used in the HandEvaluator.sol contract to generate provably fair random numbers, which is crucial for card shuffling and dealing in a poker game.

The fulfillRandomness() Function

The fulfillRandomness() function is a key part of the Chainlink VRF integration. It's the callback function that Chainlink's VRF Coordinator calls to deliver the requested random number to your contract.

In the HandEvaluator.sol contract:

function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
    if (!pendingRequests[requestId] || !gameActive) {
        emit RandomnessFailed(requestId);
        return; // Request ID is not recognized or game is inactive
    }

    pendingRequests[requestId] = false; // Mark the request as fulfilled
    dealCards(randomness);
}

Ensuring Proper Integration

To ensure that fulfillRandomness() is properly called in the Chainlink VRF flow:

  1. Request Randomness: The contract should call requestRandomness() when it needs a random number. In Tetiana's contract, this is done in the startGame() function:

    function startGame() external onlyOwner {
       // ... other checks ...
       bytes32 requestId = requestRandomness(keyHash, fee);
       pendingRequests[requestId] = true;
       emit RandomnessRequested(requestId);
       // ...
    }
  2. Handling the Response: The VRF Coordinator will call fulfillRandomness() with the result. The contract should then use this randomness, which Tetiana's contract does by calling dealCards(randomness).

  3. Security Checks: The contract includes checks to ensure the request is valid and the game is active before processing the randomness.

Recommendations

  1. Error Handling: Consider adding more detailed error handling in fulfillRandomness(). For example, emit different events for different failure scenarios.

  2. Gas Considerations: Be aware that fulfillRandomness() is called as part of an external transaction, so it should be gas-efficient.

  3. Testing: Implement thorough tests to ensure that the VRF flow works correctly, including edge cases like multiple simultaneous requests.

  4. Monitoring: Set up monitoring for the RandomnessRequested and RandomnessFailed events to track the success rate of VRF requests.

  5. Fallback Mechanism: Consider implementing a fallback mechanism in case the VRF request fails or takes too long.

By following these recommendations and ensuring that the fulfillRandomness() function is properly integrated, the contract can maintain a high level of randomness and fairness in the poker game.

cc: @tetyana-pol

peterduhon commented 2 months ago

Hey @tetyana-pol ,

Great catch on those unused functions! You're right that placeBet() and handleAllIn() aren't currently being called within the contract. Here's what we can do to address this:

  1. For placeBet():

    • Let's modify the manageTurn() function to use placeBet() for handling bets. This will make our betting logic more modular and easier to maintain.
  2. For handleAllIn():

    • We need to integrate this into our main game flow. Let's add a check in manageTurn() to call handleAllIn() when a player's bet equals their entire balance.

Here's a quick example of how we might modify manageTurn():

function manageTurn(uint256 betAmount) external isGameActive isPlayer {
    // ... existing checks ...

    if (betAmount == players[msg.sender].balance) {
        handleAllIn(msg.sender, betAmount);
    } else if (betAmount == 0) {
        fold();
    } else if (betAmount == currentRound.betAmount) {
        call();
    } else if (betAmount > currentRound.betAmount) {
        raise(betAmount);
    } else {
        placeBet(betAmount);
    }

    // ... rest of the function ...
}

Could you implement these changes and then run through some test scenarios to make sure everything works as expected?

Also, great job on the overall structure of the contract! As we continue to develop, we might want to consider splitting some of this functionality into separate contracts to make it more manageable.

Let me know if you have any questions about these changes or if you notice anything else that seems out of place.

Great work!

Pete

peterduhon commented 2 months ago

const { expect } = require("chai"); const { ethers } = require("hardhat");

describe("SkiaPoker", function () { let SkiaPoker; let skiaPoker; let owner; let addr1; let addr2; let addrs;

// We define a fixture to reuse the same setup in every test. // We use loadFixture to run this setup once, snapshot that state, // and reset Hardhat Network to that snapshot in every test. async function deploySkiaPokerFixture() { // Get the ContractFactory and Signers here. SkiaPoker = await ethers.getContractFactory("SkiaPoker"); [owner, addr1, addr2, ...addrs] = await ethers.getSigners();

// To deploy our contract, we just have to call SkiaPoker.deploy() and await
// its deployed() method, which happens once its transaction has been mined.
const vrfCoordinator = "0x2Ca8E0C643bDe4C2E08ab1fA0da3401AdAD7734D"; // Example VRF Coordinator address
const linkToken = "0x326C977E6efc84E512bB9C30f76E30c160eD06FB"; // Example LINK token address
const keyHash = "0x6c3699283bda56ad74f6b855546325b68d482e983852a7a82979cc4807b641f4"; // Example key hash
const fee = ethers.utils.parseEther("0.1"); // 0.1 LINK

skiaPoker = await SkiaPoker.deploy(vrfCoordinator, linkToken, keyHash, fee);
await skiaPoker.deployed();

return { SkiaPoker, skiaPoker, owner, addr1, addr2, addrs };

}

beforeEach(async function () { // We use loadFixture to run the deployment fixture and get the necessary objects ({ SkiaPoker, skiaPoker, owner, addr1, addr2, addrs } = await deploySkiaPokerFixture()); });

describe("Deployment", function () { it("Should set the right owner", async function () { expect(await skiaPoker.owner()).to.equal(owner.address); });

it("Should set the correct buy-in amount", async function () {
  const buyInAmount = await skiaPoker.buyInAmount();
  expect(buyInAmount).to.equal(ethers.utils.parseEther("0.1")); // Assuming 0.1 ETH buy-in
});

});

describe("Game Logic", function () { it("Should allow a player to register", async function () { await skiaPoker.connect(addr1).registerPlayer(); const player = await skiaPoker.players(addr1.address); expect(player.registered).to.equal(true); });

it("Should not allow a player to register twice", async function () {
  await skiaPoker.connect(addr1).registerPlayer();
  await expect(skiaPoker.connect(addr1).registerPlayer()).to.be.revertedWith("Player already registered");
});

// Add more tests for game logic here

});

// You can add more describe blocks for different aspects of your contract });

peterduhon commented 2 months ago

Skia Poker: Refined Showdown Logic and Additional Events

Refining initiateShowdown() Function

Here's an improved version of the initiateShowdown() function:

function initiateShowdown() internal {
    require(currentPhase == GamePhase.Showdown, "Not in showdown phase");

    // Determine winners for main pot
    address[] memory mainPotWinners = determineWinners();
    uint256 mainPotShare = mainPot / mainPotWinners.length;

    for (uint256 i = 0; i < mainPotWinners.length; i++) {
        players[mainPotWinners[i]].balance += mainPotShare;
        emit MainPotDistributed(mainPotWinners[i], mainPotShare);
    }

    // Handle side pots
    for (uint256 i = 0; i < sidePots.length; i++) {
        address[] memory sidePotWinners = determineWinnersForSidePot(sidePots[i].eligiblePlayers);
        uint256 sidePotShare = sidePots[i].amount / sidePotWinners.length;

        for (uint256 j = 0; j < sidePotWinners.length; j++) {
            players[sidePotWinners[j]].balance += sidePotShare;
            emit SidePotDistributed(i, sidePotWinners[j], sidePotShare);
        }
    }

    // Reset game state
    resetGameState();

    emit ShowdownCompleted(mainPotWinners);
}

Additional Events for Important State Changes

Here are some additional events to consider adding:

// Game flow events
event GameStarted(uint256 gameId, address[] players);
event GamePhaseChanged(GamePhase newPhase);
event PlayerTurnStarted(address player);

// Betting events
event BettingRoundStarted(uint256 roundNumber);
event BettingRoundEnded(uint256 roundNumber, uint256 potSize);

// Card-related events
event CommunityCardsDealt(uint256 phase, Card[] cards);
event PlayerCardsDealt(address player); // Don't emit actual cards for privacy

// Showdown events
event MainPotDistributed(address winner, uint256 amount);
event SidePotDistributed(uint256 potIndex, address winner, uint256 amount);
event ShowdownCompleted(address[] winners);

// Player action events
event PlayerChecked(address player);
event PlayerCalled(address player, uint256 amount);
event PlayerRaised(address player, uint256 amount);
event PlayerFolded(address player);
event PlayerWentAllIn(address player, uint256 amount);

// Other important events
event PlayerJoinedGame(address player);
event PlayerLeftGame(address player);
event PotCreated(uint256 potIndex, uint256 amount);

Implementation Guide

  1. Showdown Logic:

    • Implement the refined initiateShowdown() function.
    • Ensure determineWinners() and determineWinnersForSidePot() functions are correctly implemented.
    • Add proper error handling and require statements.
  2. Event Emissions:

    • Add the new events to the contract.
    • Emit these events at appropriate points in the contract:
      • GameStarted: In the startGame() function
      • GamePhaseChanged: Whenever currentPhase is updated
      • PlayerTurnStarted: At the beginning of each player's turn
      • BettingRoundStarted/Ended: At the start and end of each betting round
      • CommunityCardsDealt: When dealing flop, turn, and river
      • PlayerCardsDealt: When dealing cards to players (don't emit actual card values)
      • Other events: At relevant points in the game logic
  3. Testing:

    • Write unit tests for the showdown logic, ensuring correct pot distribution.
    • Test event emissions to make sure they're triggered at the right times with correct parameters.
  4. Frontend Integration:

    • Update the frontend to listen for these events and update the UI accordingly.
    • Use events to keep the game state synchronized between the blockchain and the frontend.

By implementing these changes, you'll have a more robust showdown process and better visibility into the game's state changes, which will significantly improve the frontend integration and overall user experience. @tetyana-pol

peterduhon commented 2 months ago
  1. Integration of Contracts:

In the GameManagement contract, add:

import "./HandEvaluator.sol";
import "./UserManagement.sol";

contract GameManagement is Ownable, AccessControl {
    HandEvaluator public handEvaluator;
    UserManagement public userManagement;

    constructor(address _handEvaluator, address _userManagement) {
        handEvaluator = HandEvaluator(_handEvaluator);
        userManagement = UserManagement(_userManagement);
    }

    function createGameRoom(uint256 _buyInAmount, uint256 _maxPlayers) external onlyRole(ADMIN_ROLE) {
        // ... existing code ...

        // Initialize game in HandEvaluator
        handEvaluator.initializeGame(gameId, _buyInAmount, _maxPlayers);
    }

    function startGame(uint256 _gameId) external onlyRole(ADMIN_ROLE) {
        require(gameRooms[_gameId].status == GameStatus.Waiting, "Game not in waiting state");
        handEvaluator.startGame(_gameId);
        gameRooms[_gameId].status = GameStatus.Active;
    }
}
  1. Consistent Player Management:

In the HandEvaluator contract, modify the betting functions to update balances in UserManagement:

import "./UserManagement.sol";

contract HandEvaluator is VRFConsumerBase, Ownable, ReentrancyGuard {
    UserManagement public userManagement;

    constructor(address _userManagement, /* other parameters */) {
        userManagement = UserManagement(_userManagement);
    }

    function placeBet(uint256 amount) public isGameActive isPlayer {
        require(amount >= currentRound.betAmount, "PokerGame: Bet amount too low");
        require(userManagement.getUserBalance(msg.sender) >= amount, "PokerGame: Insufficient balance");

        userManagement.updateBalance(msg.sender, -int256(amount));
        currentRound.playerBets[msg.sender] += amount;
        currentRound.totalPot += amount;

        emit PlayerBetPlaced(msg.sender, amount);
    }

    function distributePots() internal {
        // ... existing code ...

        for (uint256 i = 0; i < mainWinners.length; i++) {
            userManagement.updateBalance(mainWinners[i], int256(mainPotShare));
            emit PayoutProcessed(mainWinners[i], mainPotShare);
        }

        // ... handle side pots ...
    }
}

@tetyana-pol please update your contract. thank you!

peterduhon commented 1 month ago

@JW-dev0505 pls have a look:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.19;

import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; import "./Common.sol";

contract UserManagement is Ownable, AccessControl { bytes32 public constant GAME_CONTRACT_ROLE = keccak256("GAME_CONTRACT_ROLE");

struct User {
    address userAddress;
    string username;
    uint256 balance;
    bool isRegistered;
}

mapping(address => User) public users;

event UserRegistered(address indexed user, string username);
event BalanceUpdated(address indexed user, uint256 newBalance);

constructor() {
    _setupRole(GAME_CONTRACT_ROLE, msg.sender);
}

function registerUser(string calldata _username) external payable {
    require(bytes(_username).length > 0, "Username cannot be empty");
    require(!users[msg.sender].isRegistered, "User already registered");

    users[msg.sender] = User({
        userAddress: msg.sender,
        username: _username,
        balance: msg.value,
        isRegistered: true
    });

    emit UserRegistered(msg.sender, _username);
    emit BalanceUpdated(msg.sender, msg.value);
}

function isUserRegistered(address _user) external view returns (bool) {
    return users[_user].isRegistered;
}

function getUsernameOrAddress(address _user) external view returns (string memory) {
    if (users[_user].isRegistered) {
        return users[_user].username;
    } else {
        return addressToString(_user);
    }
}

function addressToString(address _addr) internal pure returns (string memory) {
    bytes32 value = bytes32(uint256(uint160(_addr)));
    bytes memory alphabet = "0123456789abcdef";
    bytes memory str = new bytes(42);
    str[0] = '0';
    str[1] = 'x';
    for (uint256 i = 0; i < 20; i++) {
        str[2+i*2] = alphabet[uint8(value[i + 12] >> 4)];
        str[3+i*2] = alphabet[uint8(value[i + 12] & 0x0f)];
    }
    return string(str);
}

// ... (rest of the existing functions)

}