contract BMSport is BMEvents, Ownable {
    using BMKeyCalc for *;
    using SafeMath for *;

    // TODO: check address!!
    BMForwarderInterface  private Banker_Address;
    BMPlayerBookInterface private BMBook;

    string constant public name_ = "BMSport";
    uint256 public gameIDIndex_;
    
    // (gameID => gameData)
    mapping(uint256 => BMDatasets.Game) public game_;

    // (gameID => gameStatus)
    mapping(uint256 => BMDatasets.GameStatus) public gameStatus_;

    // (gameID => (teamID => teamData))
    mapping(uint256 => mapping(uint256 => BMDatasets.Team)) public teams_;

    // (playerID => (gameID => playerData))
    mapping(uint256 => mapping(uint256 => BMDatasets.Player)) public players_;

    // (playerID => (gameID => (teamID => playerTeamData)))
    mapping(uint256 => mapping(uint256 => mapping(uint256 => BMDatasets.PlayerTeam))) public playerTeams_;


    constructor(BMPlayerBookInterface book_addr) public {
        require(book_addr != address(0), "need a playerbook address");
        BMBook = book_addr;
        gameIDIndex_ = 1;
    }


    /// @notice Create a game. Only owner(s) can call this function.
    /// Emits "onGameCreated" event.
    /// @param _name Name of the new game.
    /// @param _teamNames Array consisting names of all teams in the game.
    /// The size of the array indicates the number of teams in this game.
    /// @return Game ID of the newly created game.
    function createGame(string _name, bytes32[] _teamNames)
        external
        isHuman()
        onlyDevOrOwner()
        returns(uint256)
    {
        uint256 _gameID = gameIDIndex_;
        gameIDIndex_++;

        // initialize game
        game_[_gameID].name = _name;

        // initialize each team
        uint256 _nt = _teamNames.length;
        require(_nt > 0, "number of teams must be larger than 0");

        game_[_gameID].numberOfTeams = _nt;
        for (uint256 i = 0; i < _nt; i++) {
            teams_[_gameID][i] = BMDatasets.Team(_teamNames[i], 0, 0, 0, 0);
        }

        emit onGameCreated(_gameID, now);

        return _gameID;
    }


    /// @notice Activate a game. Only owner(s) can do this.
    /// Players can start buying keys after start time.
    /// Emits "onGameActivated" event.
    /// @param _gameID Game ID of the game.
    /// @param _startTime Timestamp of the start time.
    function activate(uint256 _gameID, uint256 _startTime)
        external
        isHuman()
        onlyDevOrOwner()
    {
        require(_gameID < gameIDIndex_, "incorrect game id");
        require(game_[_gameID].gameStartTime == 0, "already activated");
        
        // TODO: do some initialization
        game_[_gameID].gameStartTime = _startTime;

        emit onGameActivated(_gameID, _startTime, now);
    }


    /// @notice Buy keys for each team.
    /// Emits "onPurchase" for each team with a purchase.
    /// @param _gameID Game ID of the game to buy tickets.
    /// @param _teamEth Array consisting amount of ETH for each team to buy tickets.
    /// The size of the array must be the same as the number of teams.
    /// The paid ETH along with this function call must be the same as the sum of all
    /// ETH in this array.
    /// @param _affCode Affiliate code used for this transaction. Use 0 if no affiliate
    /// code is used.
    function buysXid(uint256 _gameID, uint256[] memory _teamEth)
        public
        payable
        isActivated(_gameID)
        isOngoing(_gameID)
        isNotPaused(_gameID)
        isNotClosed(_gameID)
        isHuman()
        isWithinLimits(msg.value)
    {
        // fetch player id
        uint256 _pID = BMBook.getPlayerID(msg.sender);
        
        // purchase keys for each team
        buysCore(_gameID, _pID, _teamEth);
    }


    /// @notice Pause a game. Only owner(s) can do this.
    /// Players can't buy tickets if a game is paused.
    /// Emits "onGamePaused" event.
    /// @param _gameID Game ID of the game.
    /// @param _paused "true" to pause this game, "false" to unpause.
    function pauseGame(uint256 _gameID, bool _paused)
        external
        isActivated(_gameID)
        isOngoing(_gameID)
        onlyDevOrOwner()
    {
        game_[_gameID].paused = _paused;

        emit onGamePaused(_gameID, _paused, now);
    }


    /// @notice Set a closing time for betting. Only owner(s) can do this.
    /// Players can't buy tickets for this game once the closing time is passed.
    /// Emits "onChangeCloseTime" event.
    /// @param _gameID Game ID of the game.
    /// @param _closeTime Timestamp of the closing time.
    function setCloseTime(uint256 _gameID, uint256 _closeTime)
        external
        isActivated(_gameID)
        isOngoing(_gameID)
        onlyDevOrOwner()
    {
        game_[_gameID].closeTime = _closeTime;

        emit onChangeCloseTime(_gameID, _closeTime, now);
    }


    /// @notice Select a winning team. Only owner(s) can do this.
    /// Players can't no longer buy tickets for this game once a winning team is selected.
    /// Players who bought tickets for the winning team are able to withdraw winnings.
    /// Emits "onGameEnded" event.
    /// @param _gameID Game ID of the game.
    /// @param _team Team ID of the winning team.
    /// @param _comment A closing comment to describe the conclusion of the game.
    /// @param _deadline Timestamp of the withdraw deadline of the game
    function settleGame(uint256 _gameID, uint256 _team, string _comment, uint256 _deadline)
        external
        isActivated(_gameID)
        isOngoing(_gameID)
        isValidTeam(_gameID, _team)
        onlyDevOrOwner()
    {
        // TODO: check deadline limit
        require(_deadline >= now + 86400, "deadline must be more than one day later.");

        game_[_gameID].ended = true;
        game_[_gameID].winnerTeam = _team;
        game_[_gameID].gameEndComment = _comment;
        game_[_gameID].withdrawDeadline = _deadline;

        if (teams_[_gameID][_team].keys == 0) {
            // no one bought winning keys, send pot to community
            uint256 _totalPot = (gameStatus_[_gameID].winningVaultInst).add(gameStatus_[_gameID].winningVaultFinal);
            gameStatus_[_gameID].totalWithdrawn = _totalPot;
            if (_totalPot > 0) {
                Banker_Address.deposit.value(_totalPot)();
            }
        }

        emit BMEvents.onGameEnded(_gameID, _team, _comment, now);
    }


    /// @notice Cancel a game. Only owner(s) can do this.
    /// Players can't no longer buy tickets for this game once a winning team is selected.
    /// Players who bought tickets can get back 95% of the ETH paid.
    /// Emits "onGameCancelled" event.
    /// @param _gameID Game ID of the game.
    /// @param _comment A closing comment to describe the conclusion of the game.
    /// @param _deadline Timestamp of the withdraw deadline of the game
    function cancelGame(uint256 _gameID, string _comment, uint256 _deadline)
        external
        isActivated(_gameID)
        isOngoing(_gameID)
        onlyDevOrOwner()
    {
        // TODO: check deadline limit
        require(_deadline >= now + 86400, "deadline must be more than one day later.");

        game_[_gameID].ended = true;
        game_[_gameID].canceled = true;
        game_[_gameID].gameEndComment = _comment;
        game_[_gameID].withdrawDeadline = _deadline;

        emit BMEvents.onGameCancelled(_gameID, _comment, now);
    }


    /// @notice Withdraw winnings. Only available after a game is ended
    /// (winning team selected or game canceled).
    /// Emits "onWithdraw" event.
    /// @param _gameID Game ID of the game.
    function withdraw(uint256 _gameID)
        external
        isHuman()
        isActivated(_gameID)
        isEnded(_gameID)
    {
        require(now < game_[_gameID].withdrawDeadline, "withdraw deadline already passed");
        require(gameStatus_[_gameID].fundCleared == false, "fund already cleared");

        uint256 _pID = BMBook.pIDxAddr_(msg.sender);

        require(_pID != 0, "player has not played this game");
        require(players_[_pID][_gameID].withdrawn == false, "player already cashed out");

        players_[_pID][_gameID].withdrawn = true;

        if (game_[_gameID].canceled) {
            // game is canceled
            // withdraw 95% of the original payments
            uint256 _totalInvestment = players_[_pID][_gameID].eth.mul(95) / 100;
            if (_totalInvestment > 0) {
                // send to player
                BMBook.getPlayerAddr(_pID).transfer(_totalInvestment);
                gameStatus_[_gameID].totalWithdrawn = _totalInvestment.add(gameStatus_[_gameID].totalWithdrawn);
            }

            emit BMEvents.onWithdraw(_gameID, _pID, msg.sender, BMBook.getPlayerName(_pID), _totalInvestment, now);
        } else {
            uint256 _totalWinnings = getPlayerInstWinning(_gameID, _pID, game_[_gameID].winnerTeam).add(getPlayerPotWinning(_gameID, _pID, game_[_gameID].winnerTeam));
            if (_totalWinnings > 0) {
                // send to player
                BMBook.getPlayerAddr(_pID).transfer(_totalWinnings);
                gameStatus_[_gameID].totalWithdrawn = _totalWinnings.add(gameStatus_[_gameID].totalWithdrawn);
            }

            emit BMEvents.onWithdraw(_gameID, _pID, msg.sender, BMBook.getPlayerName(_pID), _totalWinnings, now);
        }
    }


    /// @notice Clear funds of a game. Only owner(s) can do this, after withdraw deadline
    /// is passed.
    /// Emits "onFundCleared" event.
    /// @param _gameID Game ID of the game.
    function clearFund(uint256 _gameID)
        external
        isHuman()
        isEnded(_gameID)
        onlyDevOrOwner()
    {
        require(now >= game_[_gameID].withdrawDeadline, "withdraw deadline not passed yet");
        require(gameStatus_[_gameID].fundCleared == false, "fund already cleared");

        gameStatus_[_gameID].fundCleared = true;

        // send remaining fund to community
        uint256 _totalPot = (gameStatus_[_gameID].winningVaultInst).add(gameStatus_[_gameID].winningVaultFinal);
        uint256 _amount = _totalPot.sub(gameStatus_[_gameID].totalWithdrawn);
        if (_amount > 0) {
            Banker_Address.deposit.value(_amount)();
        }

        emit onFundCleared(_gameID, _amount, now);
    }


    /// @notice Get a player's current instant pot winnings.
    /// @param _gameID Game ID of the game.
    /// @param _pID Player ID of the player.
    /// @param _team Team ID of the team.
    /// @return Instant pot winnings of the player for this game and this team.
    function getPlayerInstWinning(uint256 _gameID, uint256 _pID, uint256 _team)
        public
        view
        isActivated(_gameID)
        isValidTeam(_gameID, _team)
        returns(uint256)
    {
        return ((((teams_[_gameID][_team].mask).mul(playerTeams_[_pID][_gameID][_team].keys)) / (1000000000000000000)).sub(playerTeams_[_pID][_gameID][_team].mask));
    }


    /// @notice Get a player's current final pot winnings.
    /// @param _gameID Game ID of the game.
    /// @param _pID Player ID of the player.
    /// @param _team Team ID of the team.
    /// @return Final pot winnings of the player for this game and this team.
    function getPlayerPotWinning(uint256 _gameID, uint256 _pID, uint256 _team)
        public
        view
        isActivated(_gameID)
        isValidTeam(_gameID, _team)
        returns(uint256)
    {
        if (teams_[_gameID][_team].keys > 0) {
            return gameStatus_[_gameID].winningVaultFinal.mul(playerTeams_[_pID][_gameID][_team].keys) / teams_[_gameID][_team].keys;
        } else {
            return 0;
        }
    }


    /// @notice Get current game status.
    /// @param _gameID Game ID of the game.
    /// @return (number of teams, names, keys, eth, current key price for 1 key)
    function getGameStatus(uint256 _gameID)
        public
        view
        isActivated(_gameID)
        returns(uint256, bytes32[] memory, uint256[] memory, uint256[] memory, uint256[] memory)
    {
        uint256 _nt = game_[_gameID].numberOfTeams;
        bytes32[] memory _names = new bytes32[](_nt);
        uint256[] memory _keys = new uint256[](_nt);
        uint256[] memory _eth = new uint256[](_nt);
        uint256[] memory _keyPrice = new uint256[](_nt);
        uint256 i;

        for (i = 0; i < _nt; i++) {
            _names[i] = teams_[_gameID][i].name;
            _keys[i] = teams_[_gameID][i].keys;
            _eth[i] = teams_[_gameID][i].eth;
            _keyPrice[i] = getBuyPrice(_gameID, i, 1000000000000000000);
        }

        return (_nt, _names, _keys, _eth, _keyPrice);
    }


    /// @notice Get player status of a game.
    /// @param _gameID Game ID of the game.
    /// @param _pID Player ID of the player.
    /// @return (name, eth for each team, keys for each team, inst win for each team, pot win for each team)
    function getPlayerStatus(uint256 _gameID, uint256 _pID)
        public
        view
        isActivated(_gameID)
        returns(bytes32, uint256[] memory, uint256[] memory, uint256[] memory, uint256[] memory)
    {
        uint256 _nt = game_[_gameID].numberOfTeams;
        uint256[] memory _eth = new uint256[](_nt);
        uint256[] memory _keys = new uint256[](_nt);
        uint256[] memory _instWin = new uint256[](_nt);
        uint256[] memory _potWin = new uint256[](_nt);
        uint256 i;

        for (i = 0; i < _nt; i++) {
            _eth[i] = playerTeams_[_pID][_gameID][i].eth;
            _keys[i] = playerTeams_[_pID][_gameID][i].keys;
            _instWin[i] = getPlayerInstWinning(_gameID, _pID, i);
            _potWin[i] = getPlayerPotWinning(_gameID, _pID, i);
        }
        
        return (BMBook.getPlayerName(_pID), _eth, _keys, _instWin, _potWin);
    }


    /// @notice Get the price buyer have to pay for next keys.
    /// @param _gameID Game ID of the game.
    /// @param _team Team ID of the team.
    /// @param _keys Number of keys (in wei).
    /// @return Price for the number of keys to buy (in wei).
    function getBuyPrice(uint256 _gameID, uint256 _team, uint256 _keys)
        public 
        view
        isActivated(_gameID)
        isValidTeam(_gameID, _team)
        returns(uint256)
    {                  
        return ((teams_[_gameID][_team].keys.add(_keys)).ethRec(_keys));
    }


    /// @notice Get the prices buyer have to pay for next keys for all teams.
    /// @param _gameID Game ID of the game.
    /// @param _keys Array of number of keys (in wei) for all teams.
    /// @return (total eth, array of prices in wei).
    function getBuyPrices(uint256 _gameID, uint256[] memory _keys)
        public
        view
        isActivated(_gameID)
        returns(uint256, uint256[])
    {
        uint256 _totalEth = 0;
        uint256 _nt = game_[_gameID].numberOfTeams;
        uint256[] memory _eth = new uint256[](_nt);
        uint256 i;

        require(_nt == _keys.length, "Incorrect number of teams");

        for (i = 0; i < _nt; i++) {
            if (_keys[i] > 0) {
                _eth[i] = getBuyPrice(_gameID, i, _keys[i]);
                _totalEth = _totalEth.add(_eth[i]);
            }
        }

        return (_totalEth, _eth);
    }
    

    /// @notice Get the number of keys can be bought with an amount of ETH.
    /// @param _gameID Game ID of the game.
    /// @param _team Team ID of the team.
    /// @param _eth Amount of ETH in wei.
    /// @return Number of keys can be bought (in wei).
    function getKeysfromETH(uint256 _gameID, uint256 _team, uint256 _eth)
        public 
        view
        isActivated(_gameID)
        isValidTeam(_gameID, _team)
        returns(uint256)
    {                  
        return (teams_[_gameID][_team].eth).keysRec(_eth);
    }


    /// @notice Get all numbers of keys can be bought with amounts of ETH.
    /// @param _gameID Game ID of the game.
    /// @param _eths Array of amounts of ETH in wei.
    /// @return (total keys, array of number of keys in wei).
    function getKeysFromETHs(uint256 _gameID, uint256[] memory _eths)
        public
        view
        isActivated(_gameID)
        returns(uint256, uint256[])
    {
        uint256 _totalKeys = 0;
        uint256 _nt = game_[_gameID].numberOfTeams;
        uint256[] memory _keys = new uint256[](_nt);
        uint256 i;

        require(_nt == _eths.length, "Incorrect number of teams");

        for (i = 0; i < _nt; i++) {
            if (_eths[i] > 0) {
                _keys[i] = getKeysfromETH(_gameID, i, _eths[i]);
                _totalKeys = _totalKeys.add(_keys[i]);
            }
        }

        return (_totalKeys, _keys);
    }

    /// @dev Buy keys for all teams.
    /// @param _gameID Game ID of the game.
    /// @param _pID Player ID of the player.
    /// @param _teamEth Array of eth paid for each team.
    function buysCore(uint256 _gameID, uint256 _pID, uint256[] memory _teamEth)
        private
    {
        uint256 _nt = game_[_gameID].numberOfTeams;
        uint256[] memory _keys = new uint256[](_nt);
        bytes32 _name = BMBook.getPlayerName(_pID);
        uint256 _totalEth = 0;
        uint256 i;

        require(_teamEth.length == _nt, "Number of teams is not correct");

        // for all teams...
        for (i = 0; i < _nt; i++) {
            if (_teamEth[i] > 0) {
                // compute total eth
                _totalEth = _totalEth.add(_teamEth[i]);

                // compute number of keys to buy
                _keys[i] = (teams_[_gameID][i].eth).keysRec(_teamEth[i]);

                // update player data
                playerTeams_[_pID][_gameID][i].eth = _teamEth[i].add(playerTeams_[_pID][_gameID][i].eth);
                playerTeams_[_pID][_gameID][i].keys = _keys[i].add(playerTeams_[_pID][_gameID][i].keys);

                // update team data
                teams_[_gameID][i].eth = _teamEth[i].add(teams_[_gameID][i].eth);
                teams_[_gameID][i].keys = _keys[i].add(teams_[_gameID][i].keys);

                emit BMEvents.onPurchase(_gameID, _pID, msg.sender, _name, i, _teamEth[i], _keys[i], now);
            }
        }

        // check assigned ETH for each team is the same as msg.value
        require(_totalEth == msg.value, "Total ETH is not the same as msg.value");        
            
        // update game data and player data
        gameStatus_[_gameID].totalEth = _totalEth.add(gameStatus_[_gameID].totalEth);
        players_[_pID][_gameID].eth = _totalEth.add(players_[_pID][_gameID].eth);

        distributeAll(_gameID, _pID, _totalEth, _keys);
    }


    /// @dev Distribute paid ETH to different pots.
    /// @param _gameID Game ID of the game.
    /// @param _pID Player ID of the player.
    /// @param _totalEth Total ETH paid.
    /// @param _keys Array of keys bought for each team.
    function distributeAll(uint256 _gameID, uint256 _pID, uint256 _totalEth, uint256[] memory _keys)
        private
    {
        // community 5%
        uint256 _com = _totalEth / 20;

        // instant pot (15%)
        uint256 _instPot = _totalEth.mul(15) / 100;

        // winning pot (80%)
        uint256 _pot = _totalEth.mul(80) / 100;

        // Send community to forwarder

        Banker_Address.deposit.value(_com)();

        gameStatus_[_gameID].winningVaultInst = _instPot.add(gameStatus_[_gameID].winningVaultInst);
        gameStatus_[_gameID].winningVaultFinal = _pot.add(gameStatus_[_gameID].winningVaultFinal);

        // update masks for instant winning vault
        uint256 _nt = _keys.length;
        for (uint256 i = 0; i < _nt; i++) {
            uint256 _newPot = _instPot.add(teams_[_gameID][i].dust);
            uint256 _dust = updateMasks(_gameID, _pID, i, _newPot, _keys[i]);
            teams_[_gameID][i].dust = _dust;
        }
    }


    /// @dev Updates masks for instant pot.
    /// @param _gameID Game ID of the game.
    /// @param _pID Player ID of the player.
    /// @param _team Team ID of the team.
    /// @param _gen Amount of ETH to be added into instant pot.
    /// @param _keys Number of keys bought.
    /// @return Dust left over.
    function updateMasks(uint256 _gameID, uint256 _pID, uint256 _team, uint256 _gen, uint256 _keys)
        private
        returns(uint256)
    {
        /* MASKING NOTES
            earnings masks are a tricky thing for people to wrap their minds around.
            the basic thing to understand here.  is were going to have a global
            tracker based on profit per share for each round, that increases in
            relevant proportion to the increase in share supply.
            
            the player will have an additional mask that basically says "based
            on the rounds mask, my shares, and how much i've already withdrawn,
            how much is still owed to me?"
        */
        
        // calc profit per key & round mask based on this buy:  (dust goes to pot)
        if (teams_[_gameID][_team].keys > 0) {
            uint256 _ppt = (_gen.mul(1000000000000000000)) / (teams_[_gameID][_team].keys);
            teams_[_gameID][_team].mask = _ppt.add(teams_[_gameID][_team].mask);

            updatePlayerMask(_gameID, _pID, _team, _ppt, _keys);

            // calculate & return dust
            return(_gen.sub((_ppt.mul(teams_[_gameID][_team].keys)) / (1000000000000000000)));
        } else {
            return _gen;
        }
    }


    /// @dev Updates masks for the player.
    /// @param _gameID Game ID of the game.
    /// @param _pID Player ID of the player.
    /// @param _team Team ID of the team.
    /// @param _ppt Amount of unit ETH.
    /// @param _keys Number of keys bought.
    /// @return Dust left over.
    function updatePlayerMask(uint256 _gameID, uint256 _pID, uint256 _team, uint256 _ppt, uint256 _keys)
        private
    {
        if (_keys > 0) {
            // calculate player earning from their own buy (only based on the keys
            // they just bought).  & update player earnings mask
            uint256 _pearn = (_ppt.mul(_keys)) / (1000000000000000000);
            playerTeams_[_pID][_gameID][_team].mask = (((teams_[_gameID][_team].mask.mul(_keys)) / (1000000000000000000)).sub(_pearn)).add(playerTeams_[_pID][_gameID][_team].mask);
        }
    }

    /**
    * @dev Allows the current owner to transfer Banker_Address to a new banker.
    * @param banker The address to transfer Banker_Address to.
    */
    function transferBanker(BMForwarderInterface banker) 
    public
    onlyOwner()
    {
        if (banker != address(0)) {
            Banker_Address = banker;
        }
    }


    /// @dev Check if a game is activated.
    /// @param _gameID Game ID of the game.
    modifier isActivated(uint256 _gameID) {
        require(game_[_gameID].gameStartTime > 0, "Not activated yet");
        require(game_[_gameID].gameStartTime <= now, "game not started yet");
        _;
    }


    /// @dev Check if a game is not paused.
    /// @param _gameID Game ID of the game.
    modifier isNotPaused(uint256 _gameID) {
        require(game_[_gameID].paused == false, "game is paused");
        _;
    }


    /// @dev Check if a game is not closed.
    /// @param _gameID Game ID of the game.
    modifier isNotClosed(uint256 _gameID) {
        require(game_[_gameID].closeTime == 0 || game_[_gameID].closeTime > now, "game is closed");
        _;
    }


    /// @dev Check if a game is not settled.
    /// @param _gameID Game ID of the game.
    modifier isOngoing(uint256 _gameID) {
        require(game_[_gameID].ended == false, "game is ended");
        _;
    }


    /// @dev Check if a game is settled.
    /// @param _gameID Game ID of the game.
    modifier isEnded(uint256 _gameID) {
        require(game_[_gameID].ended == true, "game is not ended");
        _;
    }


    /// @dev Check if caller is not a smart contract.
    modifier isHuman() {
        address _addr = msg.sender;
        require (_addr == tx.origin, "Human only");

        uint256 _codeLength;
        assembly { _codeLength := extcodesize(_addr) }
        require(_codeLength == 0, "Human only");
        _;
    }


    /// @dev Check if purchase is within limits.
    /// (between 0.000000001 ETH and 100000 ETH)
    /// @param _eth Amount of ETH
    modifier isWithinLimits(uint256 _eth) {
        require(_eth >= 1000000000, "too little money");
        require(_eth <= 100000000000000000000000, "too much money");
        _;    
    }


    /// @dev Check if team ID is valid.
    /// @param _gameID Game ID of the game.
    /// @param _team Team ID of the team.
    modifier isValidTeam(uint256 _gameID, uint256 _team) {
        require(_team < game_[_gameID].numberOfTeams, "there is no such team");
        _;
    }
}
/// @title A raffle system for sports betting, designed with FOMO elements
/// @notice This contract manages multiple games. Owner(s) can create games and
/// assign winning team for each game. Players can withdraw their winnings before
/// the deadline set by the owner(s). If there's no winning team, the owner(s)
/// can also cancel a game so the players get back their bettings (minus fees).
/// @dev The address of the forwarder, player book, and owner(s) are hardcoded.
/// Check 'TODO' before deploy.
function updateMasks(uint256 _gameID, uint256 _pID, uint256 _team, uint256 _gen, uint256 _keys)
        private
        returns(uint256)
    {
        /* MASKING NOTES
            earnings masks are a tricky thing for people to wrap their minds around.
            the basic thing to understand here.  is were going to have a global
            tracker based on profit per share for each round, that increases in
            relevant proportion to the increase in share supply.
            
            the player will have an additional mask that basically says "based
            on the rounds mask, my shares, and how much i've already withdrawn,
            how much is still owed to me?"
        */
        
        // calc profit per key & round mask based on this buy:  (dust goes to pot)
        if (teams_[_gameID][_team].keys > 0) {
            uint256 _ppt = (_gen.mul(1000000000000000000)) / (teams_[_gameID][_team].keys);
            teams_[_gameID][_team].mask = _ppt.add(teams_[_gameID][_team].mask);

            updatePlayerMask(_gameID, _pID, _team, _ppt, _keys);

            // calculate & return dust
            return(_gen.sub((_ppt.mul(teams_[_gameID][_team].keys)) / (1000000000000000000)));
        } else {
            return _gen;
        }
    }
/// @dev Updates masks for instant pot.
/// @param _gameID Game ID of the game.
/// @param _pID Player ID of the player.
/// @param _team Team ID of the team.
/// @param _gen Amount of ETH to be added into instant pot.
/// @param _keys Number of keys bought.
/// @return Dust left over.