pragma solidity 0.5.6;

// * xether.io - is a gambling ecosystem, which makes a difference by caring about its users.
// It’s our passion for perfection, as well as finding and creating neat solutions,
// that keeps us driven towards our goals.
//
// * Uses hybrid commit-reveal + block hash random number generation that is immune
//   to tampering by players, house and miners. Apart from being fully transparent,
//   this also allows arbitrarily high bets.


interface xEtherTokensContractInterface {
  function ecosystemDividends() external payable;
}

contract XetherGames {
    uint256 constant HOUSE_EDGE_MINIMUM_AMOUNT = 0.0003 ether;
    uint256 constant MIN_JACKPOT_BET = 0.1 ether;
    uint16 constant JACKPOT_MODULO = 1000;
    uint256 constant JACKPOT_FEE = 0.001 ether;
    uint256 constant MIN_BET = 0.01 ether;
    uint256 constant MAX_AMOUNT = 300000 ether;
    uint8 constant MAX_MODULO = 100;
    uint8 constant MAX_MASK_MODULO = 40;
    uint256 constant MAX_BET_MASK = 2 ** uint256(MAX_MASK_MODULO);
    uint8 constant BET_EXPIRATION_BLOCKS = 250;
    uint256 public DIVIDENDS_LIMIT = 1 ether;
    uint16 constant PERCENTAGES_BASE = 1000;
    address constant DUMMY_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
    uint16 public luckyNumber = 777;

    uint8 public DIVIDENDS_PERCENT = 10; // 1% example: 15 will be 1.5%
    uint8 public ADVERTISE_PERCENT = 0; // 0%
    uint8 public HOUSE_EDGE_PERCENT = 10; // 1%

    uint8 constant ROULETTE_ID = 37;
    uint8 constant ROULETTE_STAKES_LIMIT = 36;
    uint8 public rouletteSkipComission = 1;
    uint256 public rouletteTableLimit = 1.8 ether;
    uint8 public ROULETTE_PERCENT = 10;

    uint8 constant PLINKO_BYTES = 16;
    uint256 constant PLINKO_ID = 2 ** uint256(PLINKO_BYTES);
    uint16[17] PLINKO1Ratios = [1000,800,600,300,200,130,100,80,50,80,100,130,200,300,600,800,1000];
    uint16[17] PLINKO2Ratios = [2000,700,500,300,200,110,100,60,100,60,100,110,200,300,500,700,2000];
    uint16[17] PLINKO3Ratios = [5000,800,300,200,140,120,110,100,40,100,110,120,140,200,300,800,5000];
    uint8 public plinkoSkipComission = 2;
    uint8 public PLINKO_PERCENT = HOUSE_EDGE_PERCENT;

    uint16 constant SLOTS_ID = 999;
    uint8 constant SLOTS_COUNT = 5;
    uint16[] SLOTSWinsRatios = [0, 50, 120, 200, 1500];
    uint32[] SLOTSWildRatios = [0, 110, 250, 400, 3000, 10000];

    xEtherTokensContractInterface public xEtherTokensContract;

    address payable public owner;
    address payable private nextOwner;

    uint256 public totalDividends = 0;
    uint256 public totalAdvertise = 0;

    uint256 public maxProfit = 5 ether;
    uint256 public maxProfitPlinko = 10 ether;
    uint256 public maxProfitRoulette = 3.6 ether;

    address public secretSigner;
    address public moderator;
    address public croupier;
    uint128 public jackpotSize;
    uint128 public lockedInBets;

    struct Bet {
        uint256 amount;
        uint128 locked;
        uint32 modulo;
        uint8 rollUnder;
        uint40 placeBlockNumber;
        uint256 clientSeed;
        uint40 mask;
        address payable gambler;
    }

    struct BetRoulette {
      uint256 totalBetAmount;
      uint128 locked;
      mapping (uint8 => uint256) amount;
      mapping (uint8 => uint8) rollUnder;
      uint40 placeBlockNumber;
      uint256 clientSeed;
      mapping (uint8 => uint40) mask;
      address payable gambler;
      uint8 betsCount;
    }

    mapping (uint => Bet) bets;
    mapping (uint => BetRoulette) betsRoulette;
    mapping (address => uint256) public bonusProgrammAccumulated;

    event FailedPayment(address beneficiary, uint commit, uint amount, string paymentType);
    event Payment(address beneficiary, uint commit, uint amount, string paymentType);
    event JackpotPayment(address indexed beneficiary, uint commit, uint amount);

    event PayDividendsSuccess(uint time, uint amount);
    event PayDividendsFailed(uint time, uint amount);

    event Commit(uint commit, uint clietSeed, uint amount);

    constructor () public {
        owner = msg.sender;
        secretSigner = DUMMY_ADDRESS;
        croupier = DUMMY_ADDRESS;
    }

    modifier onlyOwner {
        require (msg.sender == owner, "OnlyOwner methods called by non-owner.");
        _;
    }

    modifier onlyModeration {
        require (msg.sender == owner || msg.sender == moderator, "Moderation methods called by non-moderator.");
        _;
    }

    modifier onlyCroupier {
        require (msg.sender == croupier, "OnlyCroupier methods called by non-croupier.");
        _;
    }

    function approveNextOwner(address payable _nextOwner) external onlyOwner {
        require (_nextOwner != owner, "Cannot approve current owner.");
        nextOwner = _nextOwner;
    }

    function acceptNextOwner() external {
        require (msg.sender == nextOwner, "Can only accept preapproved new owner.");
        owner = nextOwner;
    }

    function () external payable {
    }

    function setNewPercents(
      uint8 newHouseEdgePercent,
      uint8 newDividendsPercent,
      uint8 newAdvertPercent
    ) external onlyOwner {
        // We guarantee that dividends will be minimum 0.5%
        require(newDividendsPercent >= 5);
        // Total percentages not greater then 3%
        require(newHouseEdgePercent + newDividendsPercent + newAdvertPercent <= 30);

        HOUSE_EDGE_PERCENT = newHouseEdgePercent;
        ADVERTISE_PERCENT = newAdvertPercent;
        DIVIDENDS_PERCENT = newDividendsPercent;
    }

    function setNewRoulettePercents(uint8 newRoulettePercent) external onlyModeration {
        require(0 <= newRoulettePercent && newRoulettePercent <= 10);
        ROULETTE_PERCENT = newRoulettePercent;
    }

    function setNewPlinkoPercents(uint8 newPlinkoPercent) external onlyModeration {
        require(0 <= newPlinkoPercent && newPlinkoPercent <= 10);
        PLINKO_PERCENT = newPlinkoPercent;
    }

    function setXEtherContract(address payable xEtherContract) external onlyOwner{
        xEtherTokensContract = xEtherTokensContractInterface(xEtherContract);
    }

    function setAddresses(address newCroupier, address newSecretSigner, address newModerator) external onlyOwner {
        secretSigner = newSecretSigner;
        croupier = newCroupier;
        moderator = newModerator;
    }

    function changeDividendsLimit(uint _newDividendsLimit) public onlyModeration {
        DIVIDENDS_LIMIT = _newDividendsLimit;
    }

    function setMaxProfit(uint _maxProfit) public onlyModeration {
        require (_maxProfit < MAX_AMOUNT, "maxProfit cant be great then top limit.");
        maxProfit = _maxProfit;
    }

    function setMaxProfitPlinko(uint _maxProfitPlinko) public onlyModeration {
        require (_maxProfitPlinko < MAX_AMOUNT, "maxProfitPlinko cant be great then top limit.");
        maxProfitPlinko = _maxProfitPlinko;
    }

    function setMaxProfitRoulette(uint _maxProfitRoulette) public onlyModeration {
        require (_maxProfitRoulette < MAX_AMOUNT, "maxProfitRoulette cant be great then top limit.");
        maxProfitRoulette = _maxProfitRoulette;
    }

    function setRouletteTableLimit(uint _newRouletteTableLimit) public onlyModeration {
        require (_newRouletteTableLimit < MAX_AMOUNT, "roultteTableLimit cant be great then top limit.");
        rouletteTableLimit = _newRouletteTableLimit;
    }

    function setComissionState(uint8 _newRouletteState, uint8 _newPlinkoState) public onlyModeration {
        rouletteSkipComission = _newRouletteState;
        plinkoSkipComission = _newPlinkoState;
    }

    function releaseLockedInBetAmount() external onlyModeration {
        lockedInBets = 0;
    }

    function increaseJackpot(uint increaseAmount) external onlyModeration {
        require (increaseAmount <= address(this).balance, "Increase amount larger than balance.");
        require (jackpotSize + lockedInBets + increaseAmount <= address(this).balance, "Not enough funds.");
        jackpotSize += uint128(increaseAmount);
    }

    function withdrawFunds(address payable beneficiary, uint withdrawAmount) external onlyOwner {
        require (withdrawAmount <= address(this).balance, "Increase amount larger than balance.");
        require (jackpotSize + lockedInBets + withdrawAmount <= address(this).balance, "Not enough funds.");
        sendFunds(beneficiary, withdrawAmount, withdrawAmount, 0, 'withdraw');
    }

    function withdrawAdvertiseFunds(address payable beneficiary, uint withdrawAmount) external onlyOwner {
        require (withdrawAmount <= totalAdvertise, "Increase amount larger than balance.");
        totalAdvertise -= withdrawAmount;
        sendFunds(beneficiary, withdrawAmount, withdrawAmount, 0, 'withdraw');
    }

    function getBonusProgrammLevel(address gambler) public view returns (uint8 discount) {
      uint accumulated = bonusProgrammAccumulated[gambler];
      discount = 0;

      if (accumulated >= 20 ether && accumulated < 100 ether) {
        discount = 1;
      } else if (accumulated >= 100 ether && accumulated < 500 ether) {
        discount = 2;
      } else if (accumulated >= 500 ether && accumulated < 1000 ether) {
        discount = 3;
      } else if (accumulated >= 1000 ether && accumulated < 5000 ether) {
        discount = 4;
      } else if (accumulated >= 5000 ether) {
        discount = 5;
      }
    }

    function kill() external onlyOwner {
        require (lockedInBets == 0, "All bets should be processed (settled or refunded) before self-destruct.");
        selfdestruct(owner);
    }

    function sendDividends() public payable {
        if (address(xEtherTokensContract) != address(0)) {
            uint tmpDividends = totalDividends;
            xEtherTokensContract.ecosystemDividends.value(tmpDividends)();
            totalDividends = 0;

            emit PayDividendsSuccess(now, tmpDividends);
        }
    }

    function placeBet(
        uint betMask,
        uint32 modulo,
        uint commitLastBlock,
        uint commit, uint256 clientSeed,
        bytes32 r, bytes32 s
    ) external payable {
        Bet storage bet = bets[commit];
        require (bet.gambler == address(0), "Bet should be in a 'clean' state.");

        uint amount = msg.value;
        if (modulo > MAX_MODULO) {
            require (modulo == PLINKO_ID || modulo == SLOTS_ID, "Modulo should be within range.");
        } else {
            require (modulo > 1 && modulo <= MAX_MODULO, "Modulo should be within range.");
        }

        require (amount >= MIN_BET && amount <= MAX_AMOUNT, "Amount should be within range.");
        require (betMask > 0 && betMask < MAX_BET_MASK, "Mask should be within range.");
        require (block.number <= commitLastBlock, "Commit has expired.");
        require (
          secretSigner == ecrecover(keccak256(abi.encodePacked(uint40(commitLastBlock), commit)), 27, r, s) ||
          secretSigner == ecrecover(keccak256(abi.encodePacked(uint40(commitLastBlock), commit)), 28, r, s),
          "ECDSA signature is not valid."
        );

        if (totalDividends >= DIVIDENDS_LIMIT) {
          sendDividends();
        }

        uint rollUnder;
        uint mask;

        (mask, rollUnder, bet.locked) = prepareBet(betMask, modulo, amount, commit, clientSeed, msg.sender);

        bet.amount = amount;
        bet.modulo = uint32(modulo);
        bet.rollUnder = uint8(rollUnder);
        bet.placeBlockNumber = uint40(block.number);
        bet.mask = uint40(mask);
        bet.clientSeed = clientSeed;
        bet.gambler = msg.sender;
    }

    function placeBetRoulette(
        uint[] calldata betMask,
        uint[] calldata betAmount,
        uint commitLastBlock,
        uint commit, uint256 clientSeed,
        bytes32 r, bytes32 s
    ) external payable {
        BetRoulette storage betRoulette = betsRoulette[commit];

        require(msg.value <= rouletteTableLimit, "Bets sum must be LTE table limit");
        betRoulette.betsCount = uint8(betMask.length);

        require (betRoulette.gambler == address(0), "Bet should be in a 'clean' state.");
        require (block.number <= commitLastBlock, "Commit has expired.");
        require (
          secretSigner == ecrecover(keccak256(abi.encodePacked(uint40(commitLastBlock), commit)), 27, r, s) ||
          secretSigner == ecrecover(keccak256(abi.encodePacked(uint40(commitLastBlock), commit)), 28, r, s),
          "ECDSA signature is not valid."
        );

        if (totalDividends >= DIVIDENDS_LIMIT) {
          sendDividends();
        }

        (betRoulette.betsCount, betRoulette.locked) = placeBetRouletteProcess(commit, betMask, betAmount);

        lockedInBets += betRoulette.locked;
        require (lockedInBets <= address(this).balance, "Cannot afford to lose this bet.");

        if (rouletteSkipComission != 1) {
          bonusProgrammAccumulated[msg.sender] += msg.value;
        }

        betRoulette.totalBetAmount = msg.value;
        betRoulette.placeBlockNumber = uint40(block.number);
        betRoulette.clientSeed = clientSeed;
        betRoulette.gambler = msg.sender;

        jackpotSize += msg.value >= MIN_JACKPOT_BET ? uint128(JACKPOT_FEE) : 0;

        emit Commit(commit, clientSeed, msg.value);
    }

    function placeBetRouletteProcess (
      uint commit, uint[] memory betMask, uint[] memory betAmount
    ) internal returns (uint8 betsCount, uint128 locked) {
      BetRoulette storage betRoulette = betsRoulette[commit];
      betsCount = 0;
      uint totalBetAmount = 0;
      uint8 addBets = betRoulette.betsCount;
      uint8 tmpBetCount = betRoulette.betsCount - 1;
      uint128 curLocked = 0;
      uint128 tmpLocked = 0;
      bool numIsAlredyLocked = false;
      uint8 bonuses = getBonusProgrammLevel(betRoulette.gambler);

      while (0 <= tmpBetCount) {
        require (betMask[tmpBetCount] > 0 && betMask[tmpBetCount] < MAX_BET_MASK, "Mask should be within range.");

        // Check track sectors bets
        if (betMask[tmpBetCount] == 38721851401) {  // Jeu 0
          require (betAmount[tmpBetCount] >= MIN_BET * 4 && betAmount[tmpBetCount] <= MAX_AMOUNT, "Amount should be within range.");

          totalBetAmount += betAmount[tmpBetCount];
          require (totalBetAmount <= msg.value, "Total bets amount should be LTE amount");

          // 12/15
          (betRoulette.mask[tmpBetCount], betRoulette.rollUnder[tmpBetCount], curLocked) = prepareBetRoulette(36864, betAmount[tmpBetCount] / 4, bonuses);
          betRoulette.amount[tmpBetCount] = betAmount[tmpBetCount] / 4;

          // 35/32
          (betRoulette.mask[addBets], betRoulette.rollUnder[addBets], tmpLocked) = prepareBetRoulette(38654705664, betAmount[tmpBetCount] / 4, bonuses);
          betRoulette.amount[addBets] = betAmount[tmpBetCount] / 4;
          curLocked = (tmpLocked > curLocked) ? tmpLocked : curLocked;

          // 3/0
          addBets += 1;
          (betRoulette.mask[addBets], betRoulette.rollUnder[addBets], tmpLocked) = prepareBetRoulette(9, betAmount[tmpBetCount] / 4, bonuses);
          betRoulette.amount[addBets] = betAmount[tmpBetCount] / 4;
          curLocked = (tmpLocked > curLocked) ? tmpLocked : curLocked;

          // 26
          addBets += 1;
          (betRoulette.mask[addBets], betRoulette.rollUnder[addBets], tmpLocked) = prepareBetRoulette(67108864, betAmount[tmpBetCount] / 4, bonuses);
          betRoulette.amount[addBets] = betAmount[tmpBetCount] / 4;
          curLocked = (tmpLocked > curLocked) ? tmpLocked : curLocked;

          locked += curLocked;

          addBets += 1;
          betsCount += 4;
        } else if (betMask[tmpBetCount] == 39567790237) { // Voisins mask
          require (betAmount[tmpBetCount] >= MIN_BET * 9 && betAmount[tmpBetCount] <= MAX_AMOUNT, "Amount should be within range.");

          totalBetAmount += betAmount[tmpBetCount];
          require (totalBetAmount <= msg.value, "Total bets amount should be LTE amount");

          // 4/7
          (betRoulette.mask[tmpBetCount], betRoulette.rollUnder[tmpBetCount], curLocked) = prepareBetRoulette(
            144, betAmount[tmpBetCount] / 9, bonuses);
          betRoulette.amount[tmpBetCount] = betAmount[tmpBetCount] / 9;

          // 12/15
          (betRoulette.mask[addBets], betRoulette.rollUnder[addBets], tmpLocked) = prepareBetRoulette(
            36864, betAmount[tmpBetCount] / 9, bonuses);
          betRoulette.amount[addBets] = betAmount[tmpBetCount] / 9;
          curLocked = (tmpLocked > curLocked) ? tmpLocked : curLocked;

          // 18/21
          addBets += 1;
          (betRoulette.mask[addBets], betRoulette.rollUnder[addBets], tmpLocked) = prepareBetRoulette(
            2359296, betAmount[tmpBetCount] / 9, bonuses);
          betRoulette.amount[addBets] = betAmount[tmpBetCount] / 9;
          curLocked = (tmpLocked > curLocked) ? tmpLocked : curLocked;

          // 19/22
          addBets += 1;
          (betRoulette.mask[addBets], betRoulette.rollUnder[addBets], tmpLocked) = prepareBetRoulette(
            4718592, betAmount[tmpBetCount] / 9, bonuses);
          betRoulette.amount[addBets] = betAmount[tmpBetCount] / 9;
          curLocked = (tmpLocked > curLocked) ? tmpLocked : curLocked;

          // 35/32
          addBets += 1;
          (betRoulette.mask[addBets], betRoulette.rollUnder[addBets], tmpLocked) = prepareBetRoulette(
            38654705664, betAmount[tmpBetCount] / 9, bonuses);
          betRoulette.amount[addBets] = betAmount[tmpBetCount] / 9;
          curLocked = (tmpLocked > curLocked) ? tmpLocked : curLocked;

          // 25/26/28/29 (x2)
          addBets += 1;
          (betRoulette.mask[addBets], betRoulette.rollUnder[addBets], tmpLocked) = prepareBetRoulette(
            905969664, betAmount[tmpBetCount] * 2 / 9, bonuses);
          betRoulette.amount[addBets] = betAmount[tmpBetCount] * 2 / 9;
          curLocked = (tmpLocked > curLocked) ? tmpLocked : curLocked;

          // 0/2/3 (x2)
          addBets += 1;
          (betRoulette.mask[addBets], betRoulette.rollUnder[addBets], tmpLocked) = prepareBetRoulette(
            13, betAmount[tmpBetCount] * 2 / 9, bonuses);
          betRoulette.amount[addBets] = betAmount[tmpBetCount] * 2 / 9;
          curLocked = (tmpLocked > curLocked) ? tmpLocked : curLocked;

          locked += curLocked;

          addBets += 1;
          betsCount += 7;
        } else if (betMask[tmpBetCount] == 19328549442) { // Orphelins mask
          require (betAmount[tmpBetCount] >= MIN_BET * 5 && betAmount[tmpBetCount] <= MAX_AMOUNT, "Amount should be within range.");

          totalBetAmount += betAmount[tmpBetCount];
          require (totalBetAmount <= msg.value, "Total bets amount should be LTE amount");

          // 14/17
          (betRoulette.mask[addBets], betRoulette.rollUnder[addBets], curLocked) = prepareBetRoulette(
            147456, betAmount[tmpBetCount] / 5, bonuses);
          betRoulette.amount[addBets] = betAmount[tmpBetCount] / 5;

          // 17/20
          addBets += 1;
          (betRoulette.mask[addBets], betRoulette.rollUnder[addBets], tmpLocked) = prepareBetRoulette(
            1179648, betAmount[tmpBetCount] / 5, bonuses);
          betRoulette.amount[addBets] = betAmount[tmpBetCount] / 5;
          curLocked += tmpLocked;

          // 6/9
          (betRoulette.mask[tmpBetCount], betRoulette.rollUnder[tmpBetCount], tmpLocked) = prepareBetRoulette(
            576, betAmount[tmpBetCount] / 5, bonuses);
          betRoulette.amount[tmpBetCount] = betAmount[tmpBetCount] / 5;
          curLocked = (tmpLocked > curLocked) ? tmpLocked : curLocked;

          // 31/34
          addBets += 1;
          (betRoulette.mask[addBets], betRoulette.rollUnder[addBets], tmpLocked) = prepareBetRoulette(
            19327352832, betAmount[tmpBetCount] / 5, bonuses);
          betRoulette.amount[addBets] = betAmount[tmpBetCount] / 5;
          curLocked = (tmpLocked > curLocked) ? tmpLocked : curLocked;

          // 1
          addBets += 1;
          (betRoulette.mask[addBets], betRoulette.rollUnder[addBets], tmpLocked) = prepareBetRoulette(
            2, betAmount[tmpBetCount] / 5, bonuses);
          betRoulette.amount[addBets] = betAmount[tmpBetCount] / 5;
          curLocked = (tmpLocked > curLocked) ? tmpLocked : curLocked;

          locked += curLocked;

          addBets += 1;
          betsCount += 5;
        } else if (betMask[tmpBetCount] == 78542613792) { // Tier mask
          require (betAmount[tmpBetCount] >= MIN_BET * 6 && betAmount[tmpBetCount] <= MAX_AMOUNT, "Amount should be within range.");

          totalBetAmount += betAmount[tmpBetCount];
          require (totalBetAmount <= msg.value, "Total bets amount should be LTE amount");

          // 5/8
          (betRoulette.mask[tmpBetCount], betRoulette.rollUnder[tmpBetCount], curLocked) = prepareBetRoulette(
            288, betAmount[tmpBetCount] / 6, bonuses);
          betRoulette.amount[tmpBetCount] = betAmount[tmpBetCount] / 6;

          // 10/11
          (betRoulette.mask[addBets], betRoulette.rollUnder[addBets], tmpLocked) = prepareBetRoulette(
            3072, betAmount[tmpBetCount] / 6, bonuses);
          betRoulette.amount[addBets] = betAmount[tmpBetCount] / 6;
          curLocked = (tmpLocked > curLocked) ? tmpLocked : curLocked;

          // 13/16
          addBets += 1;
          (betRoulette.mask[addBets], betRoulette.rollUnder[addBets], tmpLocked) = prepareBetRoulette(
            73728, betAmount[tmpBetCount] / 6, bonuses);
          betRoulette.amount[addBets] = betAmount[tmpBetCount] / 6;
          curLocked = (tmpLocked > curLocked) ? tmpLocked : curLocked;

          // 23/24
          addBets += 1;
          (betRoulette.mask[addBets], betRoulette.rollUnder[addBets], tmpLocked) = prepareBetRoulette(
            25165824, betAmount[tmpBetCount] / 6, bonuses);
          betRoulette.amount[addBets] = betAmount[tmpBetCount] / 6;
          curLocked = (tmpLocked > curLocked) ? tmpLocked : curLocked;

          // 27/30
          addBets += 1;
          (betRoulette.mask[addBets], betRoulette.rollUnder[addBets], tmpLocked) = prepareBetRoulette(
            1207959552, betAmount[tmpBetCount] / 6, bonuses);
          betRoulette.amount[addBets] = betAmount[tmpBetCount] / 6;
          curLocked = (tmpLocked > curLocked) ? tmpLocked : curLocked;

          // 33/36
          addBets += 1;
          (betRoulette.mask[addBets], betRoulette.rollUnder[addBets], tmpLocked) = prepareBetRoulette(
            77309411328, betAmount[tmpBetCount] / 6, bonuses);
          betRoulette.amount[addBets] = betAmount[tmpBetCount] / 6;
          curLocked = (tmpLocked > curLocked) ? tmpLocked : curLocked;

          locked += curLocked;

          addBets += 1;
          betsCount += 6;
        } else {
          require (betAmount[tmpBetCount] >= MIN_BET && betAmount[tmpBetCount] <= MAX_AMOUNT, "Amount should be within range.");
          totalBetAmount += betAmount[tmpBetCount];
          require (totalBetAmount <= msg.value, "Total bets amount should be LTE amount");
          (betRoulette.mask[tmpBetCount], betRoulette.rollUnder[tmpBetCount], tmpLocked) = prepareBetRoulette(
            betMask[tmpBetCount], betAmount[tmpBetCount], bonuses);

          if (uint8(((betRoulette.mask[tmpBetCount] * POPCNT_MULT) & POPCNT_MASK) % POPCNT_MODULO) != 1) {
            locked += tmpLocked;
          } else {
            if (!numIsAlredyLocked) {
              numIsAlredyLocked = true;
              locked += tmpLocked;
            }
          }

          betRoulette.amount[tmpBetCount] = betAmount[tmpBetCount];
          betsCount += 1;
        }

        if (tmpBetCount == 0) break;
        tmpBetCount -= 1;
      }
    }

    function prepareBet(uint betMask, uint32 modulo, uint amount, uint commit, uint clientSeed, address gambler) private returns (uint mask, uint8 rollUnder, uint128 possibleWinAmount) {
        if (modulo <= MAX_MASK_MODULO) {
            rollUnder = uint8(((betMask * POPCNT_MULT) & POPCNT_MASK) % POPCNT_MODULO);
            mask = betMask;
        } else {
            require (betMask > 0 && betMask <= modulo, "High modulo range, betMask larger than modulo.");
            rollUnder = uint8(betMask);
        }

        uint jackpotFee;

        (possibleWinAmount, jackpotFee) = getDiceWinAmount(amount, modulo, rollUnder, gambler, true);
        require (possibleWinAmount <= amount + maxProfitPlinko, "maxProfitPlinko limit violation.");

        bonusProgrammAccumulated[gambler] += amount;
        lockedInBets += uint128(possibleWinAmount);
        jackpotSize += uint128(jackpotFee);

        require (jackpotSize + lockedInBets <= address(this).balance, "Cannot afford to lose this bet.");

        emit Commit(commit, clientSeed, amount);
    }

    function prepareBetRoulette(uint betMask, uint amount, uint8 bonuses) private returns (uint40 retMask, uint8 retRollUnder, uint128 possibleWinAmount) {
        uint8 rollUnder = uint8(((betMask * POPCNT_MULT) & POPCNT_MASK) % POPCNT_MODULO);
        uint mask = betMask;

        possibleWinAmount = getRouletteWinAmount(amount, 36, rollUnder, bonuses, true);
        require (possibleWinAmount <= amount + maxProfitRoulette, "maxProfitRoulette limit violation.");

        retMask = uint40(mask);
        retRollUnder = rollUnder;
    }

    function settleBet(uint reveal) external onlyCroupier {
        uint commit = uint(keccak256(abi.encodePacked(reveal)));

        Bet storage bet = bets[commit];
        uint placeBlockNumber = bet.placeBlockNumber;

        require (block.number > placeBlockNumber, "settleBet in the same block as placeBet, or before.");
        require (block.number <= placeBlockNumber + BET_EXPIRATION_BLOCKS, "Can't be queried by EVM.");

        if (bet.modulo == PLINKO_ID) {
            settleBetPlinko(bet, reveal);
        } else if (bet.modulo == SLOTS_ID) {
            settleBetSlots(bet, reveal);
        } else {
            settleBetCommon(bet, reveal);
        }

    }

    function settleBetRoulette(uint reveal) external onlyCroupier {
        uint commit = uint(keccak256(abi.encodePacked(reveal)));

        BetRoulette storage betRoulette = betsRoulette[commit];
        uint placeBlockNumber = betRoulette.placeBlockNumber;

        require (betRoulette.totalBetAmount > 0, "Bet already processed");
        require (block.number > placeBlockNumber, "settleBet in the same block as placeBet, or before.");
        require (block.number <= placeBlockNumber + BET_EXPIRATION_BLOCKS, "Can't be queried by EVM.");

        settleBetRoulette(betRoulette, reveal);
    }

    // Common bets
    function settleBetCommon(Bet storage bet, uint reveal) private {
        uint amount = bet.amount;
        uint8 rollUnder = bet.rollUnder;

        require (amount != 0, "Bet should be in an 'active' state");

        bet.amount = 0;
        bytes32 entropy = keccak256(abi.encodePacked(reveal, bet.clientSeed));
        uint dice = uint(entropy) % bet.modulo;
        uint diceWinAmount;
        uint _jackpotFee;
        uint diceWin;

        if (bet.modulo <= MAX_MASK_MODULO) {
            if ((2 ** dice) & bet.mask != 0) {
                (diceWinAmount, _jackpotFee) = getDiceWinAmount(amount, bet.modulo, rollUnder, bet.gambler, false);
                diceWin = diceWinAmount;
            }
        } else {
            if (dice < rollUnder) {
                (diceWinAmount, _jackpotFee) = getDiceWinAmount(amount, bet.modulo, rollUnder, bet.gambler, false);
                diceWin = diceWinAmount;
            }
        }

        lockedInBets -= uint128(bet.locked);

        uint jackpotWin = checkJackPotWin(entropy, amount, bet.modulo);
        if (jackpotWin > 0) {
            emit JackpotPayment(bet.gambler, uint(keccak256(abi.encodePacked(reveal))), jackpotWin);
        }

        sendFunds(
          bet.gambler,
          diceWin + jackpotWin == 0 ? 1 wei : diceWin + jackpotWin,
          diceWin,
          uint(keccak256(abi.encodePacked(reveal))),
          'payment'
        );
    }

    // Plinko
    function settleBetPlinko(Bet storage bet, uint reveal) private {
        uint amount = bet.amount;
        uint rollUnder = bet.rollUnder;

        require (amount != 0, "Bet should be in an 'active' state");

        bet.amount = 0;
        bytes32 entropy = keccak256(abi.encodePacked(reveal, bet.clientSeed));
        uint dice = uint(entropy) % bet.modulo;
        uint diceWin = _plinkoGetDiceWin(dice, amount, rollUnder, bet.gambler);

        lockedInBets -= uint128(bet.locked);

        uint jackpotWin = checkJackPotWin(entropy, amount, bet.modulo);
        if (jackpotWin > 0) {
            emit JackpotPayment(bet.gambler, uint(keccak256(abi.encodePacked(reveal))), jackpotWin);
        }

        sendFunds(
          bet.gambler,
          diceWin + jackpotWin == 0 ? 1 wei : diceWin + jackpotWin,
          diceWin,
          uint(keccak256(abi.encodePacked(reveal))),
          'payment'
        );
    }

    function _plinkoGetDiceWin(uint dice, uint amount, uint rollUnder, address gambler) internal view returns (uint) {
        uint bytesCount = 0;
        uint diceWin = 1;

        for (uint byteNum = 0; byteNum < PLINKO_BYTES; byteNum += 1) {
            if ((2 ** byteNum) & dice != 0) {
                bytesCount += 1;
            }
        }

        uint inCellRatio = _getPlinkoCellRatio(rollUnder, bytesCount);
        uint jackpotFee = amount >= MIN_JACKPOT_BET ? JACKPOT_FEE : 0;
        uint totalPercentages;

        if (plinkoSkipComission == 2) {
          totalPercentages = PLINKO_PERCENT + ADVERTISE_PERCENT + DIVIDENDS_PERCENT;
        } else {
          totalPercentages = HOUSE_EDGE_PERCENT + ADVERTISE_PERCENT + DIVIDENDS_PERCENT;
        }

        uint houseEdge = amount * (totalPercentages - getBonusProgrammLevel(gambler)) / PERCENTAGES_BASE;
        if (houseEdge < HOUSE_EDGE_MINIMUM_AMOUNT) {
            houseEdge = HOUSE_EDGE_MINIMUM_AMOUNT;
        }

        diceWin = (amount - houseEdge - jackpotFee) * inCellRatio / 100;

        return diceWin;
    }

    function _getPlinkoCellRatio(uint plinkoNum, uint cell) internal view returns (uint cellRatio) {
      if (plinkoNum == 1) {
        cellRatio = PLINKO1Ratios[cell];
      } else if (plinkoNum == 2) {
        cellRatio = PLINKO2Ratios[cell];
      } else {
        cellRatio = PLINKO3Ratios[cell];
      }
    }

    // Slots
    function settleBetSlots(Bet storage bet, uint reveal) private {
        uint amount = bet.amount;
        uint modulo = bet.modulo;

        require (amount != 0, "Bet should be in an 'active' state");

        bet.amount = 0;
        bytes32 entropy = keccak256(abi.encodePacked(reveal, bet.clientSeed));
        uint diceWin = _slotsWinAmount(entropy, amount, bet.gambler);

        lockedInBets -= uint128(bet.locked);

        uint jackpotWin = checkJackPotWin(entropy, amount, modulo);
        if (jackpotWin > 0) {
            emit JackpotPayment(bet.gambler, uint(keccak256(abi.encodePacked(reveal))), jackpotWin);
        }

        sendFunds(
          bet.gambler,
          diceWin + jackpotWin == 0 ? 1 wei : diceWin + jackpotWin,
          diceWin,
          uint(keccak256(abi.encodePacked(reveal))),
          'payment'
        );
    }

    function _slotsWinAmount(bytes32 entropy, uint amount, address gambler) internal view returns (uint winAmount) {
        uint8 wins;
        uint8 wild;

        (wins, wild) = _slotsCheckWin(entropy);

        winAmount = 0;

        uint jackpotFee = amount >= MIN_JACKPOT_BET ? JACKPOT_FEE : 0;
        uint totalPercentages = HOUSE_EDGE_PERCENT + ADVERTISE_PERCENT + DIVIDENDS_PERCENT;
        uint houseEdge = amount * (totalPercentages - getBonusProgrammLevel(gambler)) / PERCENTAGES_BASE;

        if (houseEdge < HOUSE_EDGE_MINIMUM_AMOUNT) {
            houseEdge = HOUSE_EDGE_MINIMUM_AMOUNT;
        }

        winAmount = (amount - houseEdge - jackpotFee) / 100 * SLOTSWinsRatios[wins];
        winAmount += (amount - houseEdge - jackpotFee) / 100 * SLOTSWildRatios[wild];
    }

    function _slotsCheckWin(bytes32 slots) internal pure returns(uint8 wins, uint8 wild) {
        uint curNum;
        uint prevNum;
        bytes1 charAtPos;
        uint8 firstNums;
        uint8 prevWins = 0;
        uint8 curWins = 0;

        wins = 0;
        wild = 0;

        for(uint8 i = 0; i < SLOTS_COUNT; i++) {
            charAtPos = charAt(slots, i + 2);
            firstNums = getLastN(charAtPos, 4);
            curNum = uint(firstNums);

            if (curNum > 8) {
                curNum = 16 - curNum;
            }

            if (curNum == 7) wild += 1;

            if (i == 0) {
                prevNum = curNum;
                continue;
            }

            if (prevNum == curNum) {
                curWins += 1;
            } else {
                prevWins = (curWins > prevWins) ? curWins : prevWins;
                curWins = 0;
            }

            prevNum = curNum;
        }

        wins = (curWins > prevWins) ? curWins : prevWins;
    }

    function settleBetRoulette(BetRoulette storage betRoulette, uint reveal) private {
        require (betRoulette.totalBetAmount != 0, "Bet should be in an 'active' state");
        address payable gambler = betRoulette.gambler;
        bytes32 entropy = keccak256(abi.encodePacked(reveal, betRoulette.clientSeed));
        uint diceWin = 0;
        uint diceWinAmount;
        uint feeToJP = betRoulette.totalBetAmount >= MIN_JACKPOT_BET ? JACKPOT_FEE / betRoulette.betsCount : 0;

        uint dice = uint(entropy) % ROULETTE_ID;

        uint8 bonuses = getBonusProgrammLevel(betRoulette.gambler);

        for(uint8 index = 0; index < betRoulette.betsCount; index += 1) {
          if ((2 ** dice) & betRoulette.mask[index] != 0) {
              diceWinAmount = getRouletteWinAmount(betRoulette.amount[index] - feeToJP, ROULETTE_ID - 1, betRoulette.rollUnder[index], bonuses, false);
              diceWin += diceWinAmount;
          }
        }

        lockedInBets -= betRoulette.locked;

        uint jackpotWin = checkJackPotWin(entropy, betRoulette.totalBetAmount, ROULETTE_ID);
        if (jackpotWin > 0) {
            emit JackpotPayment(gambler, uint(keccak256(abi.encodePacked(reveal))), jackpotWin);
        }

        sendFunds(
          gambler,
          diceWin + jackpotWin == 0 ? 1 wei : diceWin + jackpotWin,
          diceWin,
          uint(keccak256(abi.encodePacked(reveal))),
          'payment'
        );

        betRoulette.totalBetAmount = 0;
    }

    function refundBet(uint commit) external {
        Bet storage bet = bets[commit];
        uint amount = bet.amount;

        require (amount != 0, "Bet should be in an 'active' state");
        require (block.number > bet.placeBlockNumber + BET_EXPIRATION_BLOCKS, "Blockhash can't be queried by EVM.");

        bet.amount = 0;
        lockedInBets -= uint128(bet.locked);

        if (amount >= MIN_JACKPOT_BET && jackpotSize > JACKPOT_FEE) {
          jackpotSize -= uint128(JACKPOT_FEE);
        }

        sendFunds(bet.gambler, amount, amount, commit, 'refund');
    }

    function refundBetRoulette(uint commit) external {
        BetRoulette storage betRoulette = betsRoulette[commit];
        uint amount = betRoulette.totalBetAmount;

        require (amount != 0, "Bet should be in an 'active' state");
        require (block.number > betRoulette.placeBlockNumber + BET_EXPIRATION_BLOCKS, "Blockhash can't be queried by EVM.");

        betRoulette.totalBetAmount = 0;

        for(uint8 index = 0; index < betRoulette.betsCount; index += 1) {
          betRoulette.amount[index] = 0;
        }

        lockedInBets -= betRoulette.locked;

        if (amount >= MIN_JACKPOT_BET && jackpotSize > JACKPOT_FEE) {
          jackpotSize -= uint128(JACKPOT_FEE);
        }

        sendFunds(betRoulette.gambler, amount, amount, commit, 'refund');
    }

    function getDiceWinAmount(uint amount, uint32 modulo, uint8 rollUnder, address gambler, bool init) private returns (uint128 winAmount, uint jackpotFee) {
        require (0 < rollUnder && rollUnder <= modulo, "Win probability out of range.");

        jackpotFee = amount >= MIN_JACKPOT_BET ? JACKPOT_FEE : 0;
        uint8 totalPercentages;

        if (plinkoSkipComission == 2) { // Plinko
          totalPercentages = PLINKO_PERCENT + ADVERTISE_PERCENT + DIVIDENDS_PERCENT;
        } else { // All other games
          totalPercentages = HOUSE_EDGE_PERCENT + ADVERTISE_PERCENT + DIVIDENDS_PERCENT;
        }

        uint houseEdge = amount * (totalPercentages - getBonusProgrammLevel(gambler)) / PERCENTAGES_BASE;
        if (houseEdge < HOUSE_EDGE_MINIMUM_AMOUNT) {
            houseEdge = HOUSE_EDGE_MINIMUM_AMOUNT;
        }

        require (houseEdge + jackpotFee <= amount, "Bet doesn't even cover house edge.");

        if (init) {
          totalDividends += amount * DIVIDENDS_PERCENT / PERCENTAGES_BASE;
          totalAdvertise += amount * ADVERTISE_PERCENT / PERCENTAGES_BASE;
        }

        if (modulo == PLINKO_ID) {
          // We lock maximum for selected plinko row
          if (rollUnder == 1) {
            modulo = 10; // equal to  PLINKO1Ratios[0] / 100
          } else if (rollUnder == 2) {
            modulo = 20; // equal to  PLINKO2Ratios[0] / 100
          } else {
            modulo = 50; // equal to  PLINKO3Ratios[0] / 100
          }

          rollUnder = 1;
        }

        if (modulo == SLOTS_ID) {
          modulo = 5; // We lock x5 for slots
          rollUnder = 1;
        }

        winAmount = uint128((amount - houseEdge - jackpotFee) * modulo / rollUnder);
    }

    function getRouletteWinAmount(uint amount, uint32 modulo, uint8 rollUnder, uint8 bonuses, bool init) private returns (uint128 winAmount) {
        require (0 < rollUnder && rollUnder <= modulo, "Win probability out of range.");
        uint houseEdge;

        if (rouletteSkipComission == 1) { // Roulette
          houseEdge = amount * ROULETTE_PERCENT / PERCENTAGES_BASE;
        } else {
          if (init) {
            totalDividends += amount * DIVIDENDS_PERCENT / PERCENTAGES_BASE;
            totalAdvertise += amount * ADVERTISE_PERCENT / PERCENTAGES_BASE;
          }

          houseEdge = amount * (HOUSE_EDGE_PERCENT + ADVERTISE_PERCENT + DIVIDENDS_PERCENT - bonuses) / PERCENTAGES_BASE;
        }

        if (houseEdge < HOUSE_EDGE_MINIMUM_AMOUNT) {
            houseEdge = HOUSE_EDGE_MINIMUM_AMOUNT;
        }
        require (houseEdge <= amount, "Bet doesn't even cover house edge.");

        winAmount = uint128((amount - houseEdge) * modulo / rollUnder);
    }

    function sendFunds(address payable beneficiary, uint amount, uint successLogAmount, uint commit, string memory paymentType) private {
        if (beneficiary.send(amount)) {
            emit Payment(beneficiary, commit, successLogAmount, paymentType);
        } else {
            emit FailedPayment(beneficiary, commit, amount, paymentType);
        }
    }

    function checkJackPotWin(bytes32 entropy, uint amount, uint modulo) internal returns (uint jackpotWin) {
        jackpotWin = 0;
        if (amount >= MIN_JACKPOT_BET) {
            uint jackpotRng = (uint(entropy) / modulo) % JACKPOT_MODULO;

            if (jackpotRng == luckyNumber) {
                jackpotWin = jackpotSize;
                jackpotSize = 0;
            }
        }
    }

    uint constant POPCNT_MULT = 0x0000000000002000000000100000000008000000000400000000020000000001;
    uint constant POPCNT_MASK = 0x0001041041041041041041041041041041041041041041041041041041041041;
    uint constant POPCNT_MODULO = 0x3F;

    function charAt(bytes32 b, uint char) private pure returns (bytes1) {
        return bytes1(uint8(uint(b) / (2**((31 - char) * 8))));
    }

    function getLastN(bytes1 a, uint8 n) private pure returns (uint8) {
        uint8 lastN = uint8(a) % uint8(2) ** n;
        return lastN;
    }
}