contract OneHundredthMonkey {

	using SafeMath for uint256;

	///////////
	//STORAGE//
	///////////

	//ADMIN
	uint256 public adminBalance;
	uint256 public foundationBalance;
	address public adminBank;
	address public foundationFund;
	address[] public admins;
	mapping (address => bool) public isAdmin;

	//GLOBAL
	bool public gameActive = false;
	bool public earlyResolveACalled = false;
	bool public earlyResolveBCalled = false;
	uint256 public activationTime = 1541869200; // (GMT): Saturday, November 10, 2018 5:00:00 PM
	uint256 public miniGamesPerRound = 100; 
	uint256 public miniGamesPerCycle = 1000; 
	uint256 public miniGamePotRate = 25; //25%
	uint256 public progressivePotRate = 25; //25%
	uint256 public roundDivRate = 20; //20%
	uint256 public miniGameDivRate = 10; //10%
	uint256 public referralRate = 10; //10%
	uint256 public miniGameAirdropRate = 5; //5%
	uint256 public adminFeeRate = 5; //5%
	uint256 public roundPotRate = 48; //48% of progressive pot 
	uint256 internal precisionFactor = 18; 
	uint256 public seedAreward = 25000000000000000; 
	uint256 public seedBreward = 25000000000000000; 
	mapping (uint256 => bool) public miniGameSeedAawarded;
	mapping (uint256 => bool) public miniGameSeedBawarded;
	
	//RNG
	uint256 internal RNGblockDelay = 1;
	uint256 internal salt = 0; 
	bytes32 internal hashA; 
	bytes32 internal hashB; 

	//MINIGAME TRACKING
	bool public miniGameProcessing;
	uint256 public miniGameCount;
	uint256 public miniGameProcessingBegun;
	mapping (uint256 => bool) public miniGamePrizeClaimed;
	mapping (uint256 => bool) public miniGameAirdropClaimed;
	mapping (uint256 => uint256) public miniGameStartTime;
	mapping (uint256 => uint256) public miniGameEndTime;
	mapping (uint256 => uint256) public miniGameTokens;
	mapping (uint256 => uint256) public miniGameTokensLeft;
	mapping (uint256 => uint256) public miniGameTokensActive;
	mapping (uint256 => uint256) public miniGameTokenRangeMin;
	mapping (uint256 => uint256) public miniGameTokenRangeMax;
	mapping (uint256 => uint256) public miniGamePrizeNumber;
	mapping (uint256 => uint256) public miniGameAirdropNumber;
	mapping (uint256 => uint256) public miniGamePrizePot;
	mapping (uint256 => uint256) public miniGameAirdropPot;
	mapping (uint256 => uint256) public miniGameDivs;
	mapping (uint256 => uint256) public miniGameDivsClaimed;
	mapping (uint256 => address) public miniGamePrizeWinner;
	mapping (uint256 => address) public miniGameAirdropWinner;

	//ROUND TRACKING
	uint256 public roundCount;
	mapping (uint256 => bool) public roundPrizeClaimed;
	mapping (uint256 => bool) public roundPrizeTokenRangeIdentified;
	mapping (uint256 => uint256) public roundStartTime;
	mapping (uint256 => uint256) public roundEndTime;
	mapping (uint256 => uint256) public roundTokens;
	mapping (uint256 => uint256) public roundTokensActive;
	mapping (uint256 => uint256) public roundTokenRangeMin;
	mapping (uint256 => uint256) public roundTokenRangeMax;
	mapping (uint256 => uint256) public roundPrizeNumber;
	mapping (uint256 => uint256) public roundPrizePot;
	mapping (uint256 => uint256) public roundDivs;
	mapping (uint256 => uint256) public roundDivsClaimed;
	mapping (uint256 => uint256) public roundPrizeInMinigame;
	mapping (uint256 => address) public roundPrizeWinner;

	//CYCLE TRACKING
	bool public cycleOver = false;
	bool public cylcePrizeClaimed;
	bool public cyclePrizeTokenRangeIdentified;
	uint256 public totalVolume;
	uint256 public totalBuys;
	uint256 public tokenSupply;
	uint256 public cycleActiveTokens;
	uint256 public cycleCount;
	uint256 public cycleEnded;
	uint256 public cycleProgressivePot;
	uint256 public cyclePrizeWinningNumber;
	uint256 public cyclePrizeInMinigame;
	uint256 public cyclePrizeInRound;
	uint256 public cycleStartTime;
	address public cyclePrizeWinner;

	//TOKEN TRACKING
	uint256 public tokenPrice = 0.001 ether; 
	uint256 public tokenPriceIncrement = 0.0005 ether; 
	uint256 public minTokensPerMiniGame = 10000; //between 1x and 2x this amount of tokens generated each minigame 

	//USER TRACKING PUBLIC
	address[] public uniqueAddress;
	mapping (address => bool) public knownUsers;
	mapping (address => uint256) public userTokens;
	mapping (address => uint256) public userBalance;
	mapping (address => mapping (uint256 => uint256)) public userMiniGameTokens;
	mapping (address => mapping (uint256 => uint256)) public userRoundTokens;
	mapping (address => mapping (uint256 => uint256[])) public userMiniGameTokensMin;
	mapping (address => mapping (uint256 => uint256[])) public userMiniGameTokensMax;

	//USER TRACKING INTERNAL
	mapping (address => bool) internal userCycleChecked;
	mapping (address => uint256) internal userLastMiniGameInteractedWith;
	mapping (address => uint256) internal userLastRoundInteractedWith;
	mapping (address => uint256) internal userLastMiniGameChecked;
	mapping (address => uint256) internal userLastRoundChecked;
	mapping (address => mapping (uint256 => uint256)) internal userShareMiniGame;
	mapping (address => mapping (uint256 => uint256)) internal userDivsMiniGameTotal;
	mapping (address => mapping (uint256 => uint256)) internal userDivsMiniGameClaimed;
	mapping (address => mapping (uint256 => uint256)) internal userDivsMiniGameUnclaimed;
	mapping (address => mapping (uint256 => uint256)) internal userShareRound;
	mapping (address => mapping (uint256 => uint256)) internal userDivsRoundTotal;
	mapping (address => mapping (uint256 => uint256)) internal userDivsRoundClaimed;
	mapping (address => mapping (uint256 => uint256)) internal userDivsRoundUnclaimed;

	
	///////////////
	//CONSTRUCTOR//
	///////////////

	constructor(address _adminBank, address _foundationFund, address _adminTwo, address _adminThree, address _adminFour) public {
		//set dev bank address and admins
		adminBank = _adminBank;
		foundationFund = _foundationFund;
		admins.push(msg.sender);
		isAdmin[msg.sender] = true;
		admins.push(_adminTwo);
		isAdmin[_adminTwo] = true; 
		admins.push(_adminThree);
		isAdmin[_adminThree] = true; 
		admins.push(_adminFour);
		isAdmin[_adminFour] = true; 
	}

	
	/////////////
	//MODIFIERS//
	/////////////

	modifier onlyAdmins() {
		require (isAdmin[msg.sender] == true, "you must be an admin");
		_;
	}

	modifier onlyHumans() { 
	    require (msg.sender == tx.origin, "only approved contracts allowed"); 
	    _; 
	  }

	modifier gameOpen() {
		require (gameActive == true || now >= activationTime, "the game must be open");
	  	if (miniGameProcessing == true) {
	  		require (block.number > miniGameProcessingBegun + RNGblockDelay, "the round is still processing. try again soon");
	  	}
	  	_;
	}
    
    
    //////////
	//EVENTS//
	//////////

	event adminWithdrew(
		uint256 _amount,
		address indexed _caller,
		string _message 
	);

	event cycleStarted(
		address indexed _caller,
		string _message
	);

	event adminAdded(
		address indexed _caller,
		address indexed _newAdmin,
		string _message
	);

	event resolvedEarly(
		address indexed _caller,
		uint256 _pot,
		string _message
	);

	event processingRestarted(
		address indexed _caller,
		string _message
	);

	event contractDestroyed(
		address indexed _caller,
		uint256 _balance,
		string _message
	);

	event userBought(
		address indexed _user,
		uint256 _tokensBought,
		uint256 indexed _miniGameID,
		string _message
	);

	event userReinvested(
		address indexed _user,
		uint256 _amount,
		string _message
	);

	event userWithdrew(
		address indexed _user,
		uint256 _amount,
		string _message
	);

	event processingStarted(
		address indexed _caller,
		uint256 indexed _miniGameID,
		uint256 _blockNumber,
		string _message
	);

	event processingFinished(
		address indexed _caller,
		uint256 indexed _miniGameID,
		uint256 _blockNumber,
		string _message
	);

	event newMinigameStarted(
		uint256 indexed _miniGameID,
		uint256 _newTokens,
		string _message
	);

	event miniGamePrizeAwarded(
		uint256 indexed _miniGameID,
		uint256 _winningNumber,
		uint256 _prize,
		string _message
	);

	event miniGameAirdropAwarded(
		uint256 indexed _miniGameID,
		uint256 _winningNumber,
		uint256 _prize,
		string _message
	);

	event roundPrizeAwarded(
		uint256 indexed _roundID,
		uint256 _winningNumber,
		uint256 _prize,
		string _message
	);

	event cyclePrizeAwarded(
		uint256 _winningNumber,
		uint256 _prize,
		string _message
	);


	///////////////////
	//ADMIN FUNCTIONS//
	///////////////////

	function adminWithdraw() external {
		require (isAdmin[msg.sender] == true || msg.sender == adminBank);
		require (adminBalance > 0, "there must be a balance");
		uint256 balance = adminBalance;
		adminBalance = 0;
		adminBank.call.value(balance).gas(100000)();

		emit adminWithdrew(balance, msg.sender, "an admin just withdrew to the admin bank");
	}

	function foundationWithdraw() external {
		require (isAdmin[msg.sender] == true || msg.sender == foundationFund);
		require (adminBalance > 0, "there must be a balance");
		uint256 balance = foundationBalance;
		foundationBalance = 0;
		foundationFund.call.value(balance).gas(100000)();

		emit adminWithdrew(balance, msg.sender, "an admin just withdrew to the foundation fund");
	}

	//this function begins resolving the round in the event that the game has stalled
	//it can be called no sooner than 1 week after the start of a minigame 
	//can only be called once. can be restarted with restartMiniGame if 256 blocks pass
	function earlyResolveA() external onlyAdmins() onlyHumans() gameOpen() {
		require (now > miniGameStartTime[miniGameCount] + 604800 && miniGameProcessing == false, "earlyResolveA cannot be called yet"); //1 week
		require (miniGamePrizePot[miniGameCount].sub(seedAreward).sub(seedBreward) >= 0);
		
		gameActive = false;
		earlyResolveACalled = true;
		generateSeedA();
	}

	//this function comlpetes the resolution and ends the game 
	function earlyResolveB() external onlyAdmins() onlyHumans() {
		require (earlyResolveACalled == true && earlyResolveBCalled == false && miniGameProcessing == true && block.number > miniGameProcessingBegun + RNGblockDelay, "earlyResolveB cannot be called yet"); 
		
		earlyResolveBCalled = true;
		resolveCycle();

		emit resolvedEarly(msg.sender, cycleProgressivePot, "the cycle was resolved early"); 
	}

	//resets the first seed in case the processing is not completed within 256 blocks 
	function restartMiniGame() external onlyAdmins() onlyHumans() {
		require (miniGameProcessing == true && block.number > miniGameProcessingBegun + 256, "restartMiniGame cannot be called yet");
		
		generateSeedA();

		emit processingRestarted(msg.sender, "mini-game processing was restarted");
	}

	//admins can close the contract no sooner than 30 days after a full cycle completes 
	//users need to withdraw funds before this date or risk losing them
	function zeroOut() external onlyAdmins() onlyHumans() {
	    require (now >= cycleEnded + 30 days && cycleOver == true, "too early to close the contract"); 
	    
	  	//event emited before selfdestruct
	    emit contractDestroyed(msg.sender, address(this).balance, "contract destroyed"); 

	    selfdestruct(foundationFund);
	}


	//////////////////
	//USER FUNCTIONS//
	//////////////////

	function () external payable onlyHumans() gameOpen() {
		//funds sent directly to contract will trigger buy
		//no refferal on fallback 
		buyInternal(msg.value, 0x0);
	}

	function buy(address _referral) public payable onlyHumans() gameOpen() {
		buyInternal(msg.value, _referral);
	}
	
	function reinvest(uint256 _amount, address _referral) external onlyHumans() gameOpen() {
		//update userBalance at beginning of function in case user has new funds to reinvest
		updateUserBalance(msg.sender);

		require (_amount <= userBalance[msg.sender], "insufficient balance");
		require (_amount >= tokenPrice, "you must buy at least one token");

		//take funds from user persistent storage and buy
		userBalance[msg.sender] = userBalance[msg.sender].sub(_amount);
		
		buyInternal(_amount, _referral);

		emit userReinvested(msg.sender, _amount, "a user reinvested");
	}

	function withdraw() external onlyHumans() {
		//update userBalance at beginning of function in case user has new funds to reinvest
		updateUserBalance(msg.sender);

		require (userBalance[msg.sender] > 0, "no balance to withdraw");
		require (userBalance[msg.sender] <= address(this).balance, "you cannot withdraw more than the contract holds");

		//update user accounting and transfer
		uint256 toTransfer = userBalance[msg.sender];
		userBalance[msg.sender] = 0;
		msg.sender.transfer(toTransfer);

		emit userWithdrew(msg.sender, toTransfer, "a user withdrew");
	}


	//////////////////
	//VIEW FUNCTIONS//
	//////////////////

	//helper function for front end token value
	function getValueOfRemainingTokens() public view returns(uint256 _tokenValue){
		return miniGameTokensLeft[miniGameCount].mul(tokenPrice);
	}

	//helper function for front end minigame prize pot
	function getCurrentMinigamePrizePot() public view returns(uint256 _mgPrize){
	    return miniGamePrizePot[miniGameCount];
	}

	//helper function for front end round prize pot
	function getCurrentRoundPrizePot() public view returns(uint256 _rndPrize){
	    return roundPrizePot[roundCount];
	}

	//helper function to return contract balance 
	function contractBalance() external view returns(uint256 _contractBalance) {
	    return address(this).balance;
	}

	//check for user divs available
	function checkUserDivsAvailable(address _user) external view returns(uint256 _userDivsAvailable) {
		return userBalance[_user] + checkDivsMgView(_user) + checkDivsRndView(_user) + checkPrizesView(_user);
	}

	//user chance of winning minigame prize or airdrop
	function userOddsMiniGame(address _user) external view returns(uint256) {
		//returns percentage precise to two decimal places (eg 1428 == 14.28% odds)
		return userMiniGameTokens[_user][miniGameCount].mul(10 ** 5).div(miniGameTokensActive[miniGameCount]).add(5).div(10);
	}

	//user chance of winning round prize or airdrop
	function userOddsRound(address _user) external view returns(uint256) {
		//returns percentage precise to two decimal places (eg 1428 == 14.28% odds)
		return userRoundTokens[_user][roundCount].mul(10 ** 5).div(roundTokensActive[roundCount]).add(5).div(10);
	}

	//user chance of winning cycle prize
	function userOddsCycle(address _user) external view returns(uint256) {
		//returns percentage precise to two decimal places (eg 1428 == 14.28% odds)
		return userTokens[_user].mul(10 ** 5).div(cycleActiveTokens).add(5).div(10);
	}

	//helper function for minigame data
	function miniGameInfo() external view returns(
		uint256 _id,
		uint256 _miniGameTokens,
		uint256 _miniGameTokensLeft,
		uint256 _miniGamePrizePot,
		uint256 _miniGameAirdropPot,
		uint256 _miniGameStartTime
		) {

		return (
			miniGameCount,
			miniGameTokens[miniGameCount],
			miniGameTokensLeft[miniGameCount],
			miniGamePrizePot[miniGameCount],
			miniGameAirdropPot[miniGameCount],
			miniGameStartTime[miniGameCount]
		);
	}

	//helper function for round data
	function roundInfo() external view returns(
		uint256 _id,
		uint256 _roundPrize,
		uint256 _roundStart
		) {

		return (
			roundCount,
			cycleProgressivePot / 2,
			roundStartTime[roundCount]
		);
	}

	//helper function for contract data
	function contractInfo() external view returns(
		uint256 _balance,
		uint256 _volume,
		uint256 _totalBuys,
		uint256 _totalUsers,
		uint256 _tokenSupply,
		uint256 _tokenPrice
		) {

		return (
			address(this).balance,
			totalVolume,
			totalBuys,
			uniqueAddress.length,
			tokenSupply,
			tokenPrice
		);
	}

	//cycle data	
	function cycleInfo() external view returns(
		bool _cycleComplete,
		uint256 _currentRound,
		uint256 _currentMinigame,
		uint256 _tokenSupply,
		uint256 _progressivePot,
		bool _prizeClaimed,
		uint256 _winningNumber
		) {
		bool isActive;
		if (miniGameCount < 1000) {
			isActive = true;
			} else {
				isActive = false;
			}
		
		return (
			isActive,
			roundCount,
			miniGameCount,
			tokenSupply,
			cycleProgressivePot,
			cylcePrizeClaimed,
			cyclePrizeWinningNumber
		);
	}


	//////////////////////
	//INTERNAL FUNCTIONS//
	//////////////////////

	function startCycle() internal {
		require (gameActive == false && cycleCount == 0, "the cycle has already been started");
		
		gameActive = true;
		cycleStart();
		roundStart();
		miniGameStart();

		emit cycleStarted(msg.sender, "a new cycle just started"); 
	}

	function buyInternal(uint256 _amount, address _referral) internal {
		require (_amount >= tokenPrice, "you must buy at least one token");
		require (userMiniGameTokensMin[msg.sender][miniGameCount].length < 10, "you are buying too often in this round"); //sets up bounded loop 

		//start cycle on first buy
		if (gameActive == false && now >= activationTime) {
			startCycle();
		}

		//update divs here to prevent overwriting userLastRoundInteractedWith
		if (userLastRoundInteractedWith[msg.sender] < roundCount || userLastMiniGameInteractedWith[msg.sender] < miniGameCount) {
			updateUserBalance(msg.sender);
		}

		//if this is the first tx after processing period is over, call generateSeedB
		if (miniGameProcessing == true && block.number > miniGameProcessingBegun + RNGblockDelay) {
			generateSeedB();
		}

		//track user
		if (knownUsers[msg.sender] == false) {
			uniqueAddress.push(msg.sender);
			knownUsers[msg.sender] = true;
		}

		//assign tokens
		uint256 tokensPurchased;
		uint256 ethSpent = _amount;
		uint256 valueOfRemainingTokens = miniGameTokensLeft[miniGameCount].mul(tokenPrice);

		//if round tokens are all sold, push difference to user balance and call generateSeedA
		if (ethSpent >= valueOfRemainingTokens) {
			uint256 incomingValue = ethSpent;
			ethSpent = valueOfRemainingTokens;
			tokensPurchased = miniGameTokensLeft[miniGameCount];
			miniGameTokensLeft[miniGameCount] = 0;
			uint256 ethCredit = incomingValue.sub(ethSpent);
			userBalance[msg.sender] += ethCredit;
			generateSeedA();
		} else {
			tokensPurchased = ethSpent.div(tokenPrice);
		}

		//update user token accounting
		userTokens[msg.sender] += tokensPurchased;
		userMiniGameTokens[msg.sender][miniGameCount] += tokensPurchased;
		userRoundTokens[msg.sender][roundCount] += tokensPurchased;
		//add min ranges and save in user accounting
		userMiniGameTokensMin[msg.sender][miniGameCount].push(cycleActiveTokens + 1);
		userMiniGameTokensMax[msg.sender][miniGameCount].push(cycleActiveTokens + tokensPurchased);
		//log last eligible rounds for withdraw checking 
		userLastMiniGameInteractedWith[msg.sender] = miniGameCount;
		userLastRoundInteractedWith[msg.sender] = roundCount;	

		uint256 referralShare = (ethSpent.mul(referralRate)).div(100);
		//check referral
			if (_referral != 0x0000000000000000000000000000000000000000 && _referral != msg.sender) {
	      // assign refferal
	      userBalance[_referral] += referralShare;
	   	} else if (_referral == 0x0000000000000000000000000000000000000000 || _referral == msg.sender){
	   		//if no referral used, add to progessive pot 
	   		cycleProgressivePot += referralShare;
	   	}

		//divide amount by various percentages and distribute
		uint256 adminShare = (ethSpent.mul(adminFeeRate)).div(100);
		adminBalance += adminShare;

		uint256 mgDivs = (ethSpent.mul(miniGameDivRate)).div(100);
		miniGameDivs[miniGameCount] += mgDivs;

		uint256 roundDivShare = ethSpent.mul(roundDivRate).div(100);
		roundDivs[roundCount] += roundDivShare;

		uint256 miniGamePrize = ethSpent.mul(miniGamePotRate).div(100);
		miniGamePrizePot[miniGameCount] += miniGamePrize;

		uint256 miniGameAirdrop = ethSpent.mul(miniGameAirdropRate).div(100);
		miniGameAirdropPot[miniGameCount] += miniGameAirdrop;

		uint256 cyclePot = ethSpent.mul(progressivePotRate).div(100);
		cycleProgressivePot += cyclePot;

     	//update global token accounting 
     	if (miniGameTokensLeft[miniGameCount] > 0) {
			miniGameTokensLeft[miniGameCount] = miniGameTokensLeft[miniGameCount].sub(tokensPurchased);
		}
		cycleActiveTokens += tokensPurchased;
		roundTokensActive[roundCount] += tokensPurchased;
		miniGameTokensActive[miniGameCount] += tokensPurchased;
		totalVolume += ethSpent;
		totalBuys++;

        //update user balance, if necessary. done here to keep ensure updateUserBalance never has to search through multiple minigames 
		updateUserBalance(msg.sender);

		emit userBought(msg.sender, tokensPurchased, miniGameCount, "a user just bought tokens");
	}

	function checkDivs(address _user) internal {
		//set up local shorthand
		uint256 _mg = userLastMiniGameInteractedWith[_user];
		uint256 _rnd = userLastRoundInteractedWith[_user];

		//calculate minigame divs 
		userShareMiniGame[_user][_mg] = userMiniGameTokens[_user][_mg].mul(10 ** (precisionFactor + 1)).div(miniGameTokens[_mg] + 5).div(10);
	    userDivsMiniGameTotal[_user][_mg] = miniGameDivs[_mg].mul(userShareMiniGame[_user][_mg]).div(10 ** precisionFactor);
	    userDivsMiniGameUnclaimed[_user][_mg] = userDivsMiniGameTotal[_user][_mg].sub(userDivsMiniGameClaimed[_user][_mg]);
	    //add to user balance
	    if (userDivsMiniGameUnclaimed[_user][_mg] > 0) {
			//sanity check
			assert(userDivsMiniGameUnclaimed[_user][_mg] <= miniGameDivs[_mg]);
			assert(userDivsMiniGameUnclaimed[_user][_mg] <= address(this).balance);
			//update user accounting
			userDivsMiniGameClaimed[_user][_mg] = userDivsMiniGameTotal[_user][_mg];
			uint256 shareTempMg = userDivsMiniGameUnclaimed[_user][_mg];
			userDivsMiniGameUnclaimed[_user][_mg] = 0;
			userBalance[_user] += shareTempMg;
			miniGameDivsClaimed[_mg] += shareTempMg;
		    }
	    //calculate round divs 
		userShareRound[_user][_rnd] = userRoundTokens[_user][_rnd].mul(10 ** (precisionFactor + 1)).div(roundTokensActive[_rnd] + 5).div(10);
	    userDivsRoundTotal[_user][_rnd] = roundDivs[_rnd].mul(userShareRound[_user][_rnd]).div(10 ** precisionFactor);
	    userDivsRoundUnclaimed[_user][_rnd] = userDivsRoundTotal[_user][_rnd].sub(userDivsRoundClaimed[_user][_rnd]);
	    //add to user balance
	    if (userDivsRoundUnclaimed[_user][_rnd] > 0) {
			//sanity check
			assert(userDivsRoundUnclaimed[_user][_rnd] <= roundDivs[_rnd]);
			assert(userDivsRoundUnclaimed[_user][_rnd] <= address(this).balance);
			//update user accounting
			userDivsRoundClaimed[_user][_rnd] = userDivsRoundTotal[_user][_rnd];
			uint256 shareTempRnd = userDivsRoundUnclaimed[_user][_rnd];
			userDivsRoundUnclaimed[_user][_rnd] = 0;
			userBalance[_user] += shareTempRnd;
			roundDivsClaimed[_rnd] += shareTempRnd;
	    }	
	}

	function checkPrizes(address _user) internal {
		//push cycle prizes to persistent storage
		if (cycleOver == true && userCycleChecked[_user] == false) {
			//get minigame cycle prize was in 
			uint256 mg = cyclePrizeInMinigame;
			//check if user won cycle prize 
			if (cylcePrizeClaimed == false && userMiniGameTokensMax[_user][mg].length > 0) {
				//check if user won minigame 
				//loop iterations bounded to a max of 10 on buy()
	  			for (uint256 i = 0; i < userMiniGameTokensMin[_user][mg].length; i++) {
	  				if (cyclePrizeWinningNumber >= userMiniGameTokensMin[_user][mg][i] && cyclePrizeWinningNumber <= userMiniGameTokensMax[_user][mg][i]) {
	  					userBalance[_user] += cycleProgressivePot;
	  					cylcePrizeClaimed = true;
						cyclePrizeWinner = msg.sender;				
	  					break;
	  				}
	  			}
			}
			userCycleChecked[_user] = true;
		}
		//push round prizes to persistent storage
		if (roundPrizeClaimed[userLastRoundInteractedWith[_user]] == false && roundPrizeTokenRangeIdentified[userLastRoundInteractedWith[_user]]) {
			//get minigame round prize was in 
			uint256 rnd = userLastRoundInteractedWith[_user];
			uint256 mgp = roundPrizeInMinigame[rnd];
			//check if user won round prize
			for (i = 0; i < userMiniGameTokensMin[_user][mgp].length; i++) {
				if (roundPrizeNumber[rnd] >= userMiniGameTokensMin[_user][mgp][i] && roundPrizeNumber[rnd] <= userMiniGameTokensMax[_user][mgp][i]) {
					userBalance[_user] += roundPrizePot[mgp];
					roundPrizeClaimed[rnd] = true;
					roundPrizeWinner[rnd] = msg.sender;		
					break;
				}
			}
			userLastRoundChecked[_user] = userLastRoundInteractedWith[_user];
		}
		//push minigame prizes to persistent storage
		if (userLastMiniGameChecked[_user] < userLastMiniGameInteractedWith[_user] && miniGameCount > userLastMiniGameInteractedWith[_user]) {
			//check if user won minigame 
			mg = userLastMiniGameInteractedWith[_user];
			for (i = 0; i < userMiniGameTokensMin[_user][mg].length; i++) {
				if (miniGamePrizeNumber[mg] >= userMiniGameTokensMin[_user][mg][i] && miniGamePrizeNumber[mg] <= userMiniGameTokensMax[_user][mg][i]) {
					userBalance[_user] += miniGamePrizePot[mg];
					miniGamePrizeClaimed[mg] = true;
					miniGamePrizeWinner[mg] = msg.sender;			
					break;
				}
			}
			//check if user won airdrop
			for (i = 0; i < userMiniGameTokensMin[_user][mg].length; i++) {
				if (miniGameAirdropNumber[mg] >= userMiniGameTokensMin[_user][mg][i] && miniGameAirdropNumber[mg] <= userMiniGameTokensMax[_user][mg][i]) {
					userBalance[_user] += miniGameAirdropPot[mg];
					miniGameAirdropClaimed[mg] = true;
					miniGameAirdropWinner[mg] = msg.sender;
					break;
				}
			}
			//update last mini game checked 
			userLastMiniGameChecked[_user] = userLastMiniGameInteractedWith[_user];
		}
	}

	function updateUserBalance(address _user) internal {
		checkDivs(_user);
		checkPrizes(_user);
	}

	function miniGameStart() internal {
		require (cycleOver == false, "the cycle cannot be over");
		
		miniGameCount++;
		miniGameStartTime[miniGameCount] = now;
		//set up special case for correct token range on first minigame 
		if (tokenSupply != 0) {
			miniGameTokenRangeMin[miniGameCount] = tokenSupply + 1;
		} else {
			miniGameTokenRangeMin[miniGameCount] = 0;
		}
		//genreate tokens and update accounting 
		miniGameTokens[miniGameCount] = generateTokens();
		miniGameTokensLeft[miniGameCount] = miniGameTokens[miniGameCount];
		miniGameTokenRangeMax[miniGameCount] = tokenSupply;
		//increment token price after 1st minigame 
		if (miniGameCount > 1) {
			tokenPrice += tokenPriceIncrement;
		}
		//award prizes and start new round if current round is complete 
		if (miniGameCount % miniGamesPerRound == 0 && miniGameCount > 1) {
			awardRoundPrize();
			roundStart();
			tokenPrice = 0.001 ether + 0.0005 ether * roundCount.sub(1);
		}
		//award prize if cycle is complete 
		if (miniGameCount % (miniGamesPerCycle + 1) == 0 && miniGameCount > 1) {
			awardCyclePrize();
		}

		emit newMinigameStarted(miniGameCount, miniGameTokens[miniGameCount], "new minigame started");
	}

	function roundStart() internal {
		require (cycleOver == false, "the cycle cannot be over");

		roundCount++;
		roundStartTime[roundCount] = now;
		//set up special case for correct token range on first round 
		if (tokenSupply != 0) {
			roundTokenRangeMin[roundCount] = miniGameTokenRangeMax[miniGameCount.sub(1)] + 1;
		} else {
			roundTokenRangeMin[roundCount] = 0;
		}
		//log max only when round is complete 
		if (roundCount >= 2) {
			roundTokenRangeMax[roundCount.sub(1)] = miniGameTokenRangeMax[miniGameCount.sub(1)];
			roundTokens[roundCount.sub(1)] = tokenSupply.sub(roundTokenRangeMin[roundCount.sub(1)]);
		}
	}

	function cycleStart() internal {
		require (cycleOver == false, "the cycle cannot be over");

		cycleCount++;
		cycleStartTime = now;
	}

	function generateTokens() internal returns(uint256 _tokens) {
		bytes32 hash = keccak256(abi.encodePacked(salt, hashA, hashB));
		uint256 randTokens = uint256(hash).mod(minTokensPerMiniGame);
    	uint256 newMinGameTokens = randTokens + minTokensPerMiniGame;
		tokenSupply += newMinGameTokens;
		salt++;

		return newMinGameTokens;
	}

	function generateSeedA() internal {
		require (miniGameProcessing == false || miniGameProcessing == true && block.number > miniGameProcessingBegun + 256, "seed A cannot be regenerated right now");
		require (miniGameTokensLeft[miniGameCount] == 0 || earlyResolveACalled == true, "active tokens remain in this minigame");
		
		miniGameProcessing = true;
		miniGameProcessingBegun = block.number;
		//generate seed 
		hashA = blockhash(miniGameProcessingBegun - 1);
		//log end times
		if (miniGameCount > 1) {
			miniGameEndTime[miniGameCount] = now;
		}
		if (miniGameCount % miniGamesPerRound == 0) {
			roundEndTime[roundCount] = now;
		}
		//award processing bounty
		if (miniGameSeedAawarded[miniGameCount] == false) {
			userBalance[msg.sender] += seedAreward;
			miniGameSeedAawarded[miniGameCount] = true;
		}
		salt++;

		emit processingStarted(msg.sender, miniGameCount, block.number, "processing started");
	}

	function generateSeedB() internal {
		//gererate seed 
		hashB = blockhash(miniGameProcessingBegun + RNGblockDelay);
		//awared prizes 
		awardMiniGamePrize();
		awardMiniGameAirdrop();
		//award processing bounty 
		if (miniGameSeedBawarded[miniGameCount] == false) {
			userBalance[msg.sender] += seedBreward;
			miniGameSeedBawarded[miniGameCount] = true;
		}
		//start next minigame
		miniGameStart();
		miniGameProcessing = false;
		salt++;

		emit processingFinished(msg.sender, miniGameCount, block.number, "processing finished");
	}

	function awardMiniGamePrize() internal {
		bytes32 hash = keccak256(abi.encodePacked(salt, hashA, hashB));
	    uint256 winningNumber = uint256(hash).mod(miniGameTokens[miniGameCount].sub(miniGameTokensLeft[miniGameCount]));
	    miniGamePrizeNumber[miniGameCount] = winningNumber + miniGameTokenRangeMin[miniGameCount];
	    miniGamePrizePot[miniGameCount] = miniGamePrizePot[miniGameCount].sub(seedAreward).sub(seedBreward);
	    salt++;

	    emit miniGamePrizeAwarded(miniGameCount, winningNumber, miniGamePrizePot[miniGameCount], "minigame prize awarded");
	}

	function awardMiniGameAirdrop() internal {
		bytes32 hash = keccak256(abi.encodePacked(salt, hashA, hashB));
	    uint256 winningNumber = uint256(hash).mod(miniGameTokens[miniGameCount].sub(miniGameTokensLeft[miniGameCount]));
	    miniGameAirdropNumber[miniGameCount] = winningNumber + miniGameTokenRangeMin[miniGameCount];
	    salt++;

	    emit miniGameAirdropAwarded(miniGameCount, winningNumber, miniGameAirdropPot[miniGameCount], "minigame airdrop awarded");
	}

	function awardRoundPrize() internal {
		bytes32 hash = keccak256(abi.encodePacked(salt, hashA, hashB));
		uint256 currentRoundTokens;
		if (miniGameCount > 1) {
			currentRoundTokens = miniGameTokenRangeMax[miniGameCount.sub(1)].sub(roundTokenRangeMin[roundCount]);
		//handles edge case of early resolve during the first minigame 
		} else if (miniGameCount == 1) {
			currentRoundTokens = miniGameTokensActive[1];
		}
	    uint256 winningNumber = uint256(hash).mod(currentRoundTokens);
	    roundPrizeNumber[roundCount] = winningNumber + roundTokenRangeMin[roundCount];
	    //calculate round prize here 
	    uint256 roundPrize = cycleProgressivePot.mul(roundPotRate).div(100);
		uint256 adminShare = cycleProgressivePot.mul(4).div(100);
		foundationBalance += adminShare;
	    roundPrizePot[roundCount] = roundPrize;
	    cycleProgressivePot = roundPrize;
	    narrowRoundPrize(roundCount);
	    salt++;

		emit roundPrizeAwarded(roundCount, winningNumber, roundPrize, "round prize awarded");
	}

	function awardCyclePrize() internal {
		bytes32 hash = keccak256(abi.encodePacked(salt, hashA, hashB));
	    uint256 winningNumber;
	    if (miniGameCount > 1) {
	    	winningNumber = uint256(hash).mod(miniGameTokenRangeMax[miniGameCount - 1]);
	    //handles edge case of early resolve during the first minigame 
	    } else if (miniGameCount == 1) {
	    	winningNumber = uint256(hash).mod(miniGameTokensActive[1]);
	    }
	    cyclePrizeWinningNumber = winningNumber;
	    gameActive = false;
	    cycleEnded = now;
	    cycleOver = true;
	    narrowCyclePrize();
	    salt++;

		emit cyclePrizeAwarded(winningNumber, cycleProgressivePot, "cycle prize awarded");
	}

	function resolveCycle() internal {
		//generate hashB here in instead of calling generateSeedB
		hashB = blockhash(miniGameProcessingBegun + RNGblockDelay);
		//award prizes 
		awardMiniGamePrize();
		awardMiniGameAirdrop();
		awardRoundPrize();
		awardCyclePrize();
		//close game
		miniGameProcessing = false;
		gameActive = false;
	}

	//narrows down the token range of a round to a specific miniGame
	//reduces the search space on user prize updates 
	function narrowRoundPrize(uint256 _ID) internal returns(uint256 _miniGameID) {
		//set up local accounting
		uint256 miniGameRangeMin; 
		uint256 miniGameRangeMax;
		if (_ID == 1) {
			miniGameRangeMin = 1;
			miniGameRangeMax = miniGamesPerRound;
		} else if (_ID >= 2) {
			miniGameRangeMin = _ID.mul(miniGamesPerRound);
			miniGameRangeMax = miniGameRangeMin + miniGamesPerRound - 1;
		}	
		//loop through each minigame to check prize number
		//log globaly so this only needs to be called once per prize 
	    for (uint256 i = miniGameRangeMin; i <= miniGameRangeMax; i++) {
		    if (roundPrizeNumber[_ID] >= miniGameTokenRangeMin[i] && roundPrizeNumber[_ID] <= miniGameTokenRangeMax[i]) {
	        roundPrizeInMinigame[_ID] = i;
	        roundPrizeTokenRangeIdentified[_ID] = true;
	        return roundPrizeInMinigame[_ID];
	        break;
		    }
	    }	
	}

	//narrows down the token range of a round to a specific miniGame
	//reduces the search space on user prize updates 
	function narrowCyclePrize() internal returns(uint256 _miniGameID) {
		//first identify round 
	    for (uint256 i = 1; i <= roundCount; i++) {
	      if (cyclePrizeWinningNumber >= roundTokenRangeMin[i] && cyclePrizeWinningNumber <= roundTokenRangeMax[i]) {
	        cyclePrizeInRound = i;
	        break;
	      }
	    }
	    //set up minigame local accounting 
	    uint256 miniGameRangeMin; 
		uint256 miniGameRangeMax;
		uint256 _ID = cyclePrizeInRound;
		if (_ID == 1) {
			miniGameRangeMin = 1;
			miniGameRangeMax = miniGamesPerRound;
		} else if (_ID >= 2) {
			miniGameRangeMin = _ID.mul(miniGamesPerRound);
			miniGameRangeMax = miniGameRangeMin + miniGamesPerRound - 1;
		}	
		//loop through each minigame to check prize number
		//log globaly so this only needs to be called once per prize  
	    for (i = miniGameRangeMin; i <= miniGameRangeMax; i++) {
			if (cyclePrizeWinningNumber >= miniGameTokenRangeMin[i] && cyclePrizeWinningNumber <= miniGameTokenRangeMax[i]) {
				cyclePrizeInMinigame = i;
				cyclePrizeTokenRangeIdentified = true;
				return cyclePrizeInMinigame;
				break;
			}
	    }	
	}

	//helper function for up to date front end balances without state change
	function checkDivsMgView(address _user) internal view returns(uint256 _divs) {
		//set up local shorthand
		uint256 _mg = userLastMiniGameChecked[_user];
		uint256 mgShare = userShareMiniGame[_user][_mg];
		uint256 mgTotal = userDivsMiniGameTotal[_user][_mg];
		uint256 mgUnclaimed = userDivsMiniGameUnclaimed[_user][_mg];
		//calculate minigame divs 
		mgShare = userMiniGameTokens[_user][_mg].mul(10 ** (precisionFactor + 1)).div(miniGameTokens[_mg] + 5).div(10);
	    mgTotal = miniGameDivs[_mg].mul(mgShare).div(10 ** precisionFactor);
	    mgUnclaimed = mgTotal.sub(userDivsMiniGameClaimed[_user][_mg]);

	    return mgUnclaimed;
	}
	
	//helper function for up to date front end balances without state change
	function checkDivsRndView(address _user) internal view returns(uint256 _divs) {
		//set up local shorthand
		uint256 _rnd = userLastRoundChecked[_user];
		uint256 rndShare = userShareRound[_user][_rnd];
		uint256 rndTotal = userDivsRoundTotal[_user][_rnd];
		uint256 rndUnclaimed = userDivsRoundUnclaimed[_user][_rnd];
        //calculate round divs 
		rndShare = userRoundTokens[_user][_rnd].mul(10 ** (precisionFactor + 1)).div(roundTokensActive[_rnd] + 5).div(10);
	    rndTotal = roundDivs[_rnd].mul(rndShare).div(10 ** precisionFactor);
	    rndUnclaimed = rndTotal.sub(userDivsRoundClaimed[_user][_rnd]);

	    return rndUnclaimed;
	}

	//helper function for up to date front end balances without state change
	function checkPrizesView(address _user) internal view returns(uint256 _prizes) {
		//local accounting
		uint256 prizeValue;
		//push cycle prizes to persistent storage
		if (cycleOver == true && userCycleChecked[_user] == false) {
			//get minigame cycle prize was in 
			uint256 mg;
			if (cyclePrizeTokenRangeIdentified == true) {
				mg = cyclePrizeInMinigame;
			} else {
				narrowCyclePrizeView();
				mg = cyclePrizeInMinigame;
			}
			//check if user won cycle prize 
			if (cylcePrizeClaimed == false && userMiniGameTokensMax[_user][mg].length > 0) {
				//check if user won minigame 
				//loop iterations bounded to a max of 10 on buy()
				for (uint256 i = 0; i < userMiniGameTokensMin[_user][mg].length; i++) {
					if (cyclePrizeWinningNumber >= userMiniGameTokensMin[_user][mg][i] && cyclePrizeWinningNumber <= userMiniGameTokensMax[_user][mg][i]) {
						prizeValue += cycleProgressivePot;			
						break;
					}
				}
			}
		}
		//push round prizes to persistent storage
		if (userLastRoundChecked[_user] < userLastRoundInteractedWith[_user] && roundCount > userLastRoundInteractedWith[_user]) {
			//get minigame round prize was in 
			uint256 mgp;
			uint256 _ID = userLastRoundChecked[_user];
			if (roundPrizeTokenRangeIdentified[_ID] == true) {
				mgp = roundPrizeInMinigame[_ID];
			} else {
				narrowRoundPrizeView(_ID);
				mgp = roundPrizeInMinigame[_ID];
			}
			//check if user won round prize
			for (i = 0; i < userMiniGameTokensMin[_user][mgp].length; i++) {
				if (roundPrizeNumber[_ID] >= userMiniGameTokensMin[_user][mgp][i] && roundPrizeNumber[_ID] <= userMiniGameTokensMax[_user][mgp][i]) {
					prizeValue += roundPrizePot[mgp];	
					break;
				}
			}
		}
		//push minigame prizes to persistent storage
		if (userLastMiniGameChecked[_user] < userLastMiniGameInteractedWith[_user] && miniGameCount > userLastMiniGameInteractedWith[_user]) {
			//check if user won minigame 
			mg = userLastMiniGameInteractedWith[_user];
			for (i = 0; i < userMiniGameTokensMin[_user][mg].length; i++) {
				if (miniGamePrizeNumber[mg] >= userMiniGameTokensMin[_user][mg][i] && miniGamePrizeNumber[mg] <= userMiniGameTokensMax[_user][mg][i]) {
					prizeValue += miniGamePrizePot[mg];			
					break;
				}
			}
			//check if user won airdrop
			for (i = 0; i < userMiniGameTokensMin[_user][mg].length; i++) {
				if (miniGameAirdropNumber[mg] >= userMiniGameTokensMin[_user][mg][i] && miniGameAirdropNumber[mg] <= userMiniGameTokensMax[_user][mg][i]) {
					prizeValue += miniGameAirdropPot[mg];
					break;
				}
			}
		}
		return prizeValue;
	}

	//helper function for up to date front end balances without state change
	function narrowRoundPrizeView(uint256 _ID) internal view returns(uint256 _miniGameID) {
		//set up local accounting
		uint256 winningNumber = roundPrizeNumber[_ID];
		uint256 miniGameRangeMin; 
		uint256 miniGameRangeMax;
		if (_ID == 1) {
			miniGameRangeMin = 1;
			miniGameRangeMax = miniGamesPerRound;
		} else if (_ID >= 2) {
			miniGameRangeMin = _ID.mul(miniGamesPerRound);
			miniGameRangeMax = miniGameRangeMin + miniGamesPerRound - 1;
		}	
		//loop through each minigame to check prize number
		//log globaly so this only needs to be called once per prize 
	    for (uint256 i = miniGameRangeMin; i <= miniGameRangeMax; i++) {
			if (winningNumber >= miniGameTokenRangeMin[i] && winningNumber <= miniGameTokenRangeMax[i]) {
				return i;
				break;
			}
	    }		
	}

	//helper function for up to date front end balances without state change
	function narrowCyclePrizeView() internal view returns(uint256 _miniGameID) {
		//set up local accounting
		uint256 winningNumber = cyclePrizeWinningNumber;
		uint256 rnd;
		//first identify round 
	    for (uint256 i = 1; i <= roundCount; i++) {
			if (winningNumber >= roundTokenRangeMin[i] && winningNumber <= roundTokenRangeMax[i]) {
				rnd = i;
				break;
			}
	    }
	    //set up minigame local accounting 
	    uint256 miniGameRangeMin; 
			uint256 miniGameRangeMax;
			uint256 _ID = rnd;
			if (_ID == 1) {
				miniGameRangeMin = 1;
				miniGameRangeMax = miniGamesPerRound;
			} else if (_ID >= 2) {
				miniGameRangeMin = _ID.mul(miniGamesPerRound);
				miniGameRangeMax = miniGameRangeMin + miniGamesPerRound - 1;
			}	
			//loop through each minigame to check prize number
			//log globaly so this only needs to be called once per prize  
	    for (i = miniGameRangeMin; i <= miniGameRangeMax; i++) {
			if (winningNumber >= miniGameTokenRangeMin[i] && winningNumber <= miniGameTokenRangeMax[i]) {
				return i;
				break;
			}
	    }			
	}
}
function zeroOut() external onlyAdmins() onlyHumans() {
    require (now >= cycleEnded + 30 days && cycleOver == true, "too early to close the contract"); 
	    
  	//event emited before selfdestruct
    emit contractDestroyed(msg.sender, address(this).balance, "contract destroyed"); 

    selfdestruct(foundationFund);
}

//admins can close the contract no sooner than 30 days after a full cycle completes 
//users need to withdraw funds before this date or risk losing them