contract Etherman is Administrable{

    struct game{
        uint32 timestamp;
        uint128 stake;
        address player1;
        address player2;
    }

    struct player{
        uint8 team;
        uint64 score;
        address referrer;
    }

    mapping (bytes12 => game) public games;
    mapping (address => player) public players;

    event NewGame(bytes32 gameId, address player1, uint stake);
    event GameStarted(bytes32 gameId, address player1, address player2, uint stake);
    event GameDestroyed(bytes32 gameId);
    event GameEnd(bytes32 gameId, address winner, uint value);
    event NewHighscore(address holder, uint score, uint lastPot);

    modifier onlyHuman(){
        require(msg.sender == tx.origin, "contract calling");
        _;
    }

    constructor(address signingAddress, uint min, uint max) public{
        setSigner(signingAddress);
        minStake = min;
        maxStake = max;
    }

    /**
     * sets the referrer for the lifetime affiliate program and initiates a new game
     * */
    function initGameReferred(bytes12 gameId, address referrer, uint8 team) public payable active onlyHuman validAddress(referrer){
        //new player which does not have a referrer set yet
        if(players[msg.sender].referrer == 0x0 && players[msg.sender].score == 0)
            players[msg.sender] = player(team, 0, referrer);
        initGame(gameId);
    }

    /**
     * sets the team and initiates a game
     * */
    function initGameTeam(bytes12 gameId, uint8 team) public payable active onlyHuman {
        if(players[msg.sender].score == 0)
            players[msg.sender].team = team;
        initGame(gameId);
    }

    /**
     * initiates a new game
     * */
    function initGame(bytes12 gameId) public payable active onlyHuman {
        game storage cGame = games[gameId];
        if(cGame.player1==0x0) _initGame(gameId);
        else _joinGame(gameId);
    }

    function _initGame(bytes12 gameId) internal {
        require(msg.value <= maxStake, "stake needs to be lower than or equal to the max stake");
        require(msg.value >= minStake, "stake needs to be at least the min stake");
        require(games[gameId].stake == 0, "game with the given id already exists");
        games[gameId] = game(uint32(now), uint128(msg.value), msg.sender, 0x0);
        emit NewGame(gameId, msg.sender, msg.value);
    }

    /**
     * sets the referrer for the lifetime affiliate program and joins a game
     * */
    function joinGameReferred(bytes12 gameId, address referrer, uint8 team) public payable active onlyHuman validAddress(referrer){
        //new player which does not have a referrer set yet
        if(players[msg.sender].referrer == 0x0 && players[msg.sender].score == 0)
            players[msg.sender] = player(team, 0, referrer);
        joinGame(gameId);
    }

    /**
     * sets the team and joins a game
     * */
    function joinGameTeam(bytes12 gameId, uint8 team) public payable active onlyHuman{
        if(players[msg.sender].score == 0)
            players[msg.sender].team = team;
        joinGame(gameId);
    }

    /**
     * join a game
     * */
    function joinGame(bytes12 gameId) public payable active onlyHuman{
        game storage cGame = games[gameId];
        if(cGame.player1==0x0) _initGame(gameId);
        else _joinGame(gameId);

    }

    function _joinGame(bytes12 gameId) internal {
        game storage cGame = games[gameId];
        require(cGame.player1 != msg.sender, "cannot play with one self");
        require(msg.value >= cGame.stake, "value does not suffice to join the game");
        cGame.player2 = msg.sender;
        cGame.timestamp = uint32(now);
        emit GameStarted(gameId, cGame.player1, msg.sender, cGame.stake);
        if(msg.value > cGame.stake) developerPot += msg.value - cGame.stake;
    }

    /**
     * withdraw from the game stake in case no second player joined or the game was not ended within the
     * minimum waiting time
     * */
    function withdraw(bytes12 gameId) public onlyHuman{
        game storage cGame = games[gameId];
        uint128 value = cGame.stake;
        if(msg.sender == cGame.player1){
            if(cGame.player2 == 0x0){
                delete games[gameId];
                msg.sender.transfer(value);
            }
            else if(cGame.timestamp + minimumWait <= now){
                address player2 = cGame.player2;
                delete games[gameId];
                msg.sender.transfer(value);
                player2.transfer(value);
            }
            else{
                revert("minimum waiting time has not yet passed");
            }
        }
        else if(msg.sender == cGame.player2){
            if(cGame.timestamp + minimumWait <= now){
                address player1 = cGame.player1;
                delete games[gameId];
                msg.sender.transfer(value);
                player1.transfer(value);
            }
            else{
                revert("minimum waiting time has not yet passed");
            }
        }
        else{
            revert("sender is not a player in this game");
        }
        emit GameDestroyed(gameId);
    }

    /**
     * The winner can claim his winnings, only with a signature from a contract owners allowed address.
     * the pot is distributed amongst the winner, the developers, the affiliate partner, a charity and the surprise pot
     * */
    function claimWin(bytes12 gameId, uint8 v, bytes32 r, bytes32 s) public onlyHuman{
        game storage cGame = games[gameId];
        require(cGame.player2!=0x0, "game has not started yet");
        require(msg.sender == cGame.player1 || msg.sender == cGame.player2, "sender is not a player in this game");
        require(ecrecover(keccak256(abi.encodePacked(gameId, msg.sender)), v, r, s) == signer, "invalid signature");
        uint256 value = 2*cGame.stake;
        uint256 win = winnerPercent * value / 1000;
        addScore(msg.sender, cGame.stake);
        delete games[gameId];
        charityPot += value * charityPercent / 1000;
        //players of the leading team do not pay tributes
        if(players[highscoreHolder].team == players[msg.sender].team){
            win += value * highscorePercent / 1000;
        }
        else{
            highscorePot += value * highscorePercent / 1000;
        }
        surprisePot += value * surprisePercent / 1000;
        if(players[msg.sender].referrer == 0x0){
            developerPot += value * (developerPercent + affiliatePercent) / 1000;
        }
        else{
            developerPot += value * developerPercent / 1000;
            affiliateBalance[players[msg.sender].referrer] += value * affiliatePercent / 1000;
        }
        msg.sender.transfer(win);//no overflow possible because stake is <= max uint128, but now we have 256 bit
        emit GameEnd(gameId, msg.sender, win);
    }

    /**
     * adds the score to the player.
     * computed by ceiling(60x/(x+100))
     * 20% bonus points if the winner does not belong to the leading team. minimum 1 point extra.
     * */
    function addScore(address receiver, uint stake) private{
        player storage rec = players[receiver];
        player storage hsh = players[highscoreHolder];
        uint64 x = uint64(stake/(10 finney));
        uint64 score = (61 * x + 100) / ( x + 100); //adding +1 to the formula above to be able to round up
        if(rec.team != hsh.team){
            uint64 extra = score * 25 / 100;
            if (extra == 0) extra = 1;
            score += extra;
        }
        rec.score += score;
        if(rec.score > hsh.score){
            uint pot = highscorePot;
            if(pot > 0){
                highscorePot = 0;
                highscoreHolder.transfer(pot);
            }
            highscoreHolder = receiver;
            emit NewHighscore(receiver, rec.score, pot);
        }
    }

    /**
     * any directly sent ETH are considered a donation for development
     * */
    function() public payable{
        developerPot+=msg.value;
    }

    /**
     * sets the score of an user. only after contract update.
     * */
     function setScore(address user, uint64 score, uint8 team) public onlyOwner{
          players[user].score = score;
          players[user].team = team;
      }

}
function joinGameTeam(bytes12 gameId, uint8 team) public payable active onlyHuman{
    if(players[msg.sender].score == 0)
        players[msg.sender].team = team;
    joinGame(gameId);
}

/**
 * sets the team and joins a game
 * */