Introduction to Cryptogs Reborn Gaming Contract v0.1
0x25df
March 3rd, 2022

Author: 0xAA

Development / Design / Coordination: @cosmoburn, @0xAA_Science, @deepe_eth, @steve0xp, TylerS, @beastadon, @OrHalldor, @Thisisnottap

What is Cryptogs Reborn?

Cryptogs Reborn is an on-chain NFT game version of POGs (circa 90’s). The game deployment and the NFT “Cryptogs (Togs)” (ERC1155) is minted on the Polygon (MATIC) network. Minting and game-play are all free. Just pay gas on Polygon (MATIC). Contract address: https://polygonscan.com/address/0xa3b9f81828f0e552648cd82eb1b217ccf38c7339

Game Logic

  1. Player 0 inits a Pog game and send TOG NFT to the gaming contract;
  2. Player 1 joins the game and send TOG NFT to the gaming contract;
  3. Player 0 slammers the Tog stack, randomly flips some of the Togs. The flipped Togs are transferred to Player 0.
  4. Player 1 slammers the Tog stack, randomly flips some of the Togs. The flipped Togs are transferred to Player 0.
  5. Continue Steps 3. and 4. if there is still Tog remaining in stacks.

Cryptogs Reborn Gaming Contract v0.1

Elements

Game struct:

    struct Game {
        uint256 gameState; 
        address creator;
        address opponent;
        uint256[] creatorTogs;
        uint256[] creatorTogsAmount;
        uint256[] opponentTogs;
        uint256[] opponentTogsAmount;
        address nextMover;
        uint256 expirationBlock; 
        uint256 round; // game round
        bool withdrawed; 
    }
  1. gameState: initialized to 0; 1 stands for player0 create a game; 2 stands for player1 join the game; 3 stands for finished game; 4 stands for withdrawed game.
  2. creator is the address for player 0
  3. opponent is the address for player 1
  4. creatorTogs and creatorTogsAmount for Tog id and amounts sent by player 0
  5. opponentTogs and opponentTogsAmount for Tog id and amounts sent by player 1
  6. nextMover is the address that will throw slammer in the next round
  7. expirationBlock is the block number that the game will expire
  8. withdrawed is whether the Togs have been withdrawed from the game.

The games are saved in a mapping (gameId to game struct)

    mapping(uint256 => Game) public games;
    uint256 public gameId = 0;

Chainlink VRF params

// Chainlink VRF
    bytes32 internal keyHash;
    uint256 internal fee;
    struct SlammerRequest {
        uint256 gameId;
        address player;
    }
    mapping(bytes32 => SlammerRequest) private _vrfRequestIdToSlammerRequest;

    // constructor
    /**
     * Constructor inherits VRFConsumerBase
     * 
     * Network: Polygon
     * Chainlink VRF Coordinator address: 0x3d2341ADb2D31f1c5530cDC622016af293177AE0
     * LINK token address:                0xb0897686c545045aFc77CF20eC7A532E3120E0F1
     * Key Hash: 0xf86195cf7690c55907b2b611ebb7343a6f649bff128701cc542f0569e2c549da
     */

Constructer: set Chainlink VRF params and Cryptogs NFT object.

    constructor(address _vrfCoordinator, address _linkToken, bytes32 _keyHash, address _Cryptogs)
        VRFConsumerBase(_vrfCoordinator, _linkToken)
    {
        keyHash = _keyHash;
        fee = 0.0001 * 10 ** 18; // 0.0001 LINK (on polygon network)
        CryptogsReborn = _Cryptogs; // set cryptogsReborn contract address
        Cryptogs = IERC1155(_Cryptogs);
    }

Helpful function

// function to get sum of array
    function getArraySum(uint[] _array) 
            public 
            pure 
            returns (uint sum_) 
        {
            sum_ = 0;
            for (uint i = 0; i < _array.length; i++) {
                sum_ += _array[i];
            }
        }

Game Steps

Step 1: Player 0 init game, send Togs to contract, and emit gameInit event

 // gameInit event: player 0 set up the game
    event gameInit(uint256 indexed gameId, address indexed sender, uint256 indexed expirationBlock);

    // player 0 init Game 
    function initGame(uint256[] calldata _creatorTogs, uint256[] calldata _amounts) public {
        require(_creatorTogs.length == _amounts.length);
        require(_creatorTogs.length <= 5);
        require(getArraySum(_amounts) == 5);
        // transfer 5 Pogs to game
        Cryptogs.safeTransferBatch(_msgSender(), this, _creatorTogs, _amounts, 0x0);
        // set gameState to 1: initGame state
        gameState = 1;
        expirationBlock = block.number + blocksUntilExpiration;
        // create game struct
        Game memory game0 = Game({
            gameState: gameState,
            creator: _msgSender(),
            opponent: 0x0,
            creatorTogs: _creatorTogs,
            creatorTogsAmount: _amounts,
            opponentTogs: uint256[],
            opponentTogsAmount: uint256[],
            nextMover: _msgSender(),
            expirationBlock: expirationBlock,
            round: 1,
            withdrawed: false
            });
        // push current game to array
        games[gameId] = game0;

        emit gameInit(gameId, _msgSender(), expirationBlock);

        gameId += 1;

    }

Step 2: Player 2 join a game, send Togs to contract, and emit gameJoin event

    // gameJoin event: player 1 joins the game
    event gameJoin(uint256 indexed GameId, address indexed sender, uint256 indexed expirationBlock);

    // player 1 join Game 
    function joinGame(uint256 _gameId, uint256[] calldata _joinTogs, uint256[] calldata _amounts) public {
        require(_gameId <= gameId);
        require(games[_gameId].gameState == 1); // game state check
        require(block.number <= games[_gameId].expirationBlock); // game not expiration
        require(_joinTogs.length == _amounts.length); // two array have same length
        require(_joinTogs.length <= 5);
        require(getArraySum(_amounts) == 5); // transfer amount = 5
        require(games[_gameId].creator != _msgSender()); // player0 != player1

        expirationBlock = block.number + blocksUntilExpiration;

        // fill opponent information to game
        Game storage game0 = games[_gameId];
        game0.opponent = _msgSender();
        game0.opponentTogs = _joinTogs;
        game0.opponentTogsAmount = _amounts;
        game0.expirationBlock = expirationBlock; // update expirationBlock
        game0.gameState = 2; // change game state to 2: game ready to SLAMMER!

        emit gameJoin(_gameId, _msgSender(), expirationBlock);
    }

Step 3: Player 0 slammers the Tog stack, randomly flips some of the Togs. The flipped Togs are transferred to Player 0.

Slammer will emit Slammer event. Chainlink VRF callback function will emit FlipTogs event.

    // Slammer event:
    event Slammer(address indexed Sender, bytes32 indexed RequestId, uint256 indexed ExpirationBlock);

    // Slammer Time: Next Mover Throw Slammer, which generate random number from Chainlink VRF and flip POGs randomly
    function playGame(uint256 _gameId) public {
        require(_gameId <= gameId);
        require(games[_gameId].gameState == 2); // game state check
        require(block.number <= games[_gameId].expirationBlock); // game not expiration
        require(games[_gameId].nextMover == _msgSender()); // next Player is msg.sender
        require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK");

        Game storage game0 = games[_gameId];

        // SLAMMER TIME!
        // get random number
        bytes32 requestId = requestRandomness(keyHash, fee);
        _vrfRequestIdToSlammerRequest[requestId] = SlammerRequest(_gameId,_msgSender());

        expirationBlock = block.number + blocksUntilExpiration;
        game0.expirationBlock = expirationBlock; // update expirationBlock

        // update next mover
        if(game0.creater == _msgSender()){
            game0.nextMover = game0.opponent;
        }else{
            game0.nextMover = game0.creater;
        }

        emit Slammer(_msgSender(), requestId, expirationBlock);
    }


    event FlipTogs(address indexed player, uint256 indexed gameId, uint256 indexed gameRound, uint256[] flippedTogs, uint256[] flippedAmounts);     
    // Once VRF generates a random number, this function will be called to continue the game logic
    function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
        // get slammer request by vrf requestId
        SlammerRequest memory slammerRequest = _vrfRequestIdToSlammerRequest[requestId];
        address player = slammerRequest.player;
        uint256 gameId = slammerRequest.gameId;
        // get game object
        Game storage game0 = games[gameId];

        // flipped togs
        uint256[] flippedTogs;
        uint256[] flippedAmounts;
        uint256 _rand;
        // flip togs of player 0
        for (uint256 i = 0; i < game0.creatorTogs.length; i++) {
            if(game0.creatorTogsAmount[i] >0){
                uint256 _rand = uint256(keccak256(abi.encode(randomness, i))) % flippiness_total;
                if(_rand < randomness){
                    flippedTogs.push(game0.creatorTogs[i]);
                    flippedAmounts.push(1);
                    game0.creatorTogsAmount[i] -= 1;
                }
            }
        }

        // flip togs of player 1
        for (uint256 i = 0; i < game0.opponentPogs.length; i++) {
            if(game0.creatorTogsAmount[i] >0){
                _rand = uint256(keccak256(abi.encode(randomness, i))) % flippiness_total;
                if(_rand < randomness){
                    flippedTogs.push(game0.creatorTogs[i]);
                    flippedAmounts.push(1);
                    game0.creatorTogsAmount[i] -= 1;
                }
            }
        }


        for (uint256 i = 0; i < game0.opponentTogs.length; i++) {
            if(game0.opponentTogsAmount[i] >0){
                _rand = uint256(keccak256(abi.encode(randomness, i+5))) % flippiness_total;
                if(_rand < randomness){
                    // when flip
                    flippedTogs.push(game0.opponentTogs[i]);
                    flippedAmounts.push(1);
                    game0.opponentTogsAmount[i] -= 1;
                }
            }
        }

        if(getArraySum(game0.creatorTogsAmount) == 0 || getArraySum(game0.opponentTogsAmount == 0)){
            game0.gameState = 3;
        }
        game0.round += 1;
        // transfer flipped Pog 

        Cryptogs.safeTransferBatch(this, player, flippedTogs, flippedAmounts, 0x0);
        emit FlipTogs(player, gameId, game0.round, flippedTogs, flippedAmounts);
    }

Step 4: The other player (Player 1) slammers the Tog stack, randomly flips some of the Togs. The flipped Togs are transferred to Player 1. Similar as Step 3.

Step 5: If the game is not finished, repeat Step 3. and 4. If the game expires, players can withdraw Togs with withdrawTogs() function, which will emit WithdrawTogs event.

    event WithdrawTogs (address indexed GameId, address indexed creater, address indexed opponent);
    // Withdraw Togs when game completed or expired
    function withdrawTogs (uint256 _gameId) public {
        require(_gameId <= gameId);
        require(games[_gameId].gameState == 3 || games[_gameId].expirationBlock < block.number); // game finished or expired
        require(games[_gameId].withdrawed == false);

        Cryptogs.safeTransferBatch(this, games[_gameId].creater, games[_gameId].creatorTogs, games[_gameId].creatorTogsAmounts, 0x0);
        Cryptogs.safeTransferBatch(this, games[_gameId].opponent, games[_gameId].opponentTogs, games[_gameId].opponentTogsAmounts, 0x0);
        
        // end the game
        games[_gameId].withdrawed = true;
        games[_gameId].gameState = 4;
        games[_gameId].creatorTogsAmounts = uint256[];
        games[_gameId].opponentTogsAmounts = uint256[];

        emit WithdrawTogs(_gameId, games[_gameId].creater, games[_gameId].opponent);

    }
Arweave TX
DYlx7q9XcZxBEQPt8geDpGW5i4jFLh36NbhQDk9x8QI
Ethereum Address
0x25df6DA2f4e5C178DdFF45038378C0b08E0Bce54
Content Digest
j34z4oKK2bqYml7jJR-ErK-Gv530R-RR9-pvZSgR6CA