contract EtherHiLo is Ownable, EtherHiLoRandomNumberRequester {

    uint8 constant NUM_DICE_SIDES = 13;

    uint public minBet;
    uint public maxBetThresholdPct;
    bool public gameRunning;
    uint public balanceInPlay;

    EtherHiLoRandomNumberGenerator private random;
    mapping(address => Game) private gamesInProgress;

    event GameFinished(address indexed player, uint indexed playerGameNumber, uint bet, uint8 firstRoll, uint8 finalRoll, uint winnings, uint payout);
    event GameError(address indexed player, uint indexed playerGameNumber);

    enum BetDirection {
        None,
        Low,
        High
    }

    enum GameState {
        None,
        WaitingForFirstCard,
        WaitingForDirection,
        WaitingForFinalCard,
        Finished
    }

    // the game object
    struct Game {
        address player;
        GameState state;
        uint id;
        BetDirection direction;
        uint bet;
        uint8 firstRoll;
        uint8 finalRoll;
        uint winnings;
    }

    // the constructor
    constructor() public {
        setMinBet(100 finney);
        setGameRunning(true);
        setMaxBetThresholdPct(75);
    }

    /// Default function
    function() external payable {

    }


    /// =======================
    /// EXTERNAL GAME RELATED FUNCTIONS

    // begins a game
    function beginGame() public payable {
        address player = msg.sender;
        uint bet = msg.value;

        require(player != address(0));
        require(gamesInProgress[player].state == GameState.None || gamesInProgress[player].state == GameState.Finished);
        require(gameRunning);
        require(bet >= minBet && bet <= getMaxBet());

        Game memory game = Game({
                id:         uint(keccak256(block.number, player, bet)),
                player:     player,
                state:      GameState.WaitingForFirstCard,
                bet:        bet,
                firstRoll:  0,
                finalRoll:  0,
                winnings:   0,
                direction:  BetDirection.None
            });

        if (!random.generateRandomNumber(player, NUM_DICE_SIDES)) {
            player.transfer(msg.value);
            return;
        }

        balanceInPlay = balanceInPlay + game.bet;
        gamesInProgress[player] = game;
    }

    // finishes a game that is in progress
    function finishGame(BetDirection direction) public {
        address player = msg.sender;

        require(player != address(0));
        require(gamesInProgress[player].state != GameState.None && gamesInProgress[player].state != GameState.Finished);

        if (!random.generateRandomNumber(player, NUM_DICE_SIDES)) {
            return;
        }

        Game storage game = gamesInProgress[player];
        game.direction = direction;
        game.state = GameState.WaitingForFinalCard;
        gamesInProgress[player] = game;
    }

    // returns current game state
    function getGameState(address player) public view returns
            (GameState, uint, BetDirection, uint, uint8, uint8, uint) {
        return (
            gamesInProgress[player].state,
            gamesInProgress[player].id,
            gamesInProgress[player].direction,
            gamesInProgress[player].bet,
            gamesInProgress[player].firstRoll,
            gamesInProgress[player].finalRoll,
            gamesInProgress[player].winnings
        );
    }

    // Returns the minimum bet
    function getMinBet() public view returns (uint) {
        return minBet;
    }

    // Returns the maximum bet
    function getMaxBet() public view returns (uint) {
        return SafeMath.div(SafeMath.div(SafeMath.mul(this.balance - balanceInPlay, maxBetThresholdPct), 100), 12);
    }

    // calculates winnings for the given bet and percent
    function calculateWinnings(uint bet, uint percent) public pure returns (uint) {
        return SafeMath.div(SafeMath.mul(bet, percent), 100);
    }

    // Returns the win percent when going low on the given number
    function getLowWinPercent(uint number) public pure returns (uint) {
        require(number >= 2 && number <= NUM_DICE_SIDES);
        if (number == 2) {
            return 1200;
        } else if (number == 3) {
            return 500;
        } else if (number == 4) {
            return 300;
        } else if (number == 5) {
            return 300;
        } else if (number == 6) {
            return 200;
        } else if (number == 7) {
            return 180;
        } else if (number == 8) {
            return 150;
        } else if (number == 9) {
            return 140;
        } else if (number == 10) {
            return 130;
        } else if (number == 11) {
            return 120;
        } else if (number == 12) {
            return 110;
        } else if (number == 13) {
            return 100;
        }
    }

    // Returns the win percent when going high on the given number
    function getHighWinPercent(uint number) public pure returns (uint) {
        require(number >= 1 && number < NUM_DICE_SIDES);
        if (number == 1) {
            return 100;
        } else if (number == 2) {
            return 110;
        } else if (number == 3) {
            return 120;
        } else if (number == 4) {
            return 130;
        } else if (number == 5) {
            return 140;
        } else if (number == 6) {
            return 150;
        } else if (number == 7) {
            return 180;
        } else if (number == 8) {
            return 200;
        } else if (number == 9) {
            return 300;
        } else if (number == 10) {
            return 300;
        } else if (number == 11) {
            return 500;
        } else if (number == 12) {
            return 1200;
        }
    }


    /// =======================
    /// RANDOM NUMBER CALLBACKS

    function incomingRandomNumberError(address player) public {
        require(msg.sender == address(random));

        Game storage game = gamesInProgress[player];
        if (game.bet > 0) {
            game.player.transfer(game.bet);
        }

        delete gamesInProgress[player];
        GameError(player, game.id);
    }

    function incomingRandomNumber(address player, uint8 randomNumber) public {
        require(msg.sender == address(random));

        Game storage game = gamesInProgress[player];

        if (game.firstRoll == 0) {

            game.firstRoll = randomNumber;
            game.state = GameState.WaitingForDirection;
            gamesInProgress[player] = game;

            return;
        }

        uint8 finalRoll = randomNumber;
        uint winnings = 0;

        if (game.direction == BetDirection.High && finalRoll > game.firstRoll) {
            winnings = calculateWinnings(game.bet, getHighWinPercent(game.firstRoll));
        } else if (game.direction == BetDirection.Low && finalRoll < game.firstRoll) {
            winnings = calculateWinnings(game.bet, getLowWinPercent(game.firstRoll));
        }

        // this should never happen according to the odds,
        // and the fact that we don't allow people to bet
        // so large that they can take the whole pot in one
        // fell swoop - however, a number of people could
        // theoretically all win simultaneously and cause
        // this scenario.  This will try to at a minimum
        // send them back what they bet and then since it
        // is recorded on the blockchain we can verify that
        // the winnings sent don't match what they should be
        // and we can manually send the rest to the player.
        uint transferAmount = winnings;
        if (transferAmount > this.balance) {
            if (game.bet < this.balance) {
                transferAmount = game.bet;
            } else {
                transferAmount = SafeMath.div(SafeMath.mul(this.balance, 90), 100);
            }
        }

        balanceInPlay = balanceInPlay - game.bet;

        if (transferAmount > 0) {
            game.player.transfer(transferAmount);
        }

        game.finalRoll = finalRoll;
        game.winnings = winnings;
        game.state = GameState.Finished;
        gamesInProgress[player] = game;

        GameFinished(player, game.id, game.bet, game.firstRoll, finalRoll, winnings, transferAmount);
    }


    /// OWNER / MANAGEMENT RELATED FUNCTIONS

    // fail safe for balance transfer
    function transferBalance(address to, uint amount) public onlyOwner {
        to.transfer(amount);
    }

    // cleans up a player abandoned game, but only if it's
    // greater than 24 hours old.
    function cleanupAbandonedGame(address player) public onlyOwner {
        require(player != address(0));

        Game storage game = gamesInProgress[player];
        require(game.player != address(0));

        game.player.transfer(game.bet);
        delete gamesInProgress[game.player];
    }

    function setRandomAddress(address _address) public onlyOwner {
        random = EtherHiLoRandomNumberGenerator(_address);
    }

    // set the minimum bet
    function setMinBet(uint bet) public onlyOwner {
        minBet = bet;
    }

    // set whether or not the game is running
    function setGameRunning(bool v) public onlyOwner {
        gameRunning = v;
    }

    // set the max bet threshold percent
    function setMaxBetThresholdPct(uint v) public onlyOwner {
        maxBetThresholdPct = v;
    }

    // Transfers the current balance to the recepient and terminates the contract.
    function destroyAndSend(address _recipient) public onlyOwner {
        selfdestruct(_recipient);
    }

}
/// @title EtherHiLo
/// @dev the contract than handles the EtherHiLo app
function getHighWinPercent(uint number) public pure returns (uint) {
    require(number >= 1 && number < NUM_DICE_SIDES);
    if (number == 1) {
        return 100;
    } else if (number == 2) {
        return 110;
    } else if (number == 3) {
        return 120;
    } else if (number == 4) {
        return 130;
    } else if (number == 5) {
        return 140;
    } else if (number == 6) {
        return 150;
    } else if (number == 7) {
        return 180;
    } else if (number == 8) {
        return 200;
    } else if (number == 9) {
        return 300;
    } else if (number == 10) {
        return 300;
    } else if (number == 11) {
        return 500;
    } else if (number == 12) {
        return 1200;
    }
}

// Returns the win percent when going high on the given number