Php – Soccer simulation for a game

PHPsimulation

I would like to build a simulation engine which can simulate a soccer (association football) match. It would be cool great if you could help me. What is important to me is to decide which actions happen. The event listeners for each action can be implemented later easily. The function should only simulate the game results and comments to happening actions. There is no 2D/3D graphics needed. We're talking about games like Hattrick.


I would propose that you have an array of minutes with actions at first.

$minutes = array(1, 3, 4, 7, 11, 13, …, 90, 92);

For each of these minutes, you could then simulate an attack.

The attacking team is determined by dice before: $attacking = mt_rand(1, 2);

So the part which is most important to me is the attack function.

Please edit my approach or use it as a sample. Can you help me to improve this? The function should be complex so that the results are as realistic as possible. But you need to find something between high predictability and too random results. I only want to improve this function.

My approach:

<?php
function Chance_Percent($chance, $universe = 100) {
    $chance = abs(intval($chance));
    $universe = abs(intval($universe));
    if (mt_rand(1, $universe) <= $chance) {
        return true;
    }
    return false;
}
function simulate_attack($teamname_att, $teamname_def, $strength_att, $strength_def) {
    global $minute, $goals, $_POST, $matchReport, $fouls, $yellowCards, $redCards, $offsides, $schuesse, $taktiken;
    // input values: attacker's name, defender's name, attacker's strength array, defender's strength array
    // players' strength values vary from 0.1 to 9.9
    // ADJUSTMENT START
    switch ($taktiken[$teamname_att][0]) {
        case 1: $strength_att['defenders'] *= 1.1; $strength_att['forwards'] *= 0.9; break;
        case 3: $strength_att['defenders'] *= 0.9; $strength_att['forwards'] *= 1.1; break;
    }
    switch ($taktiken[$teamname_def][0]) {
        case 1: $strength_def['defenders'] *= 1.1; $strength_def['forwards'] *= 0.9; break;
        case 3: $strength_def['defenders'] *= 0.9; $strength_def['forwards'] *= 1.1; break;
    }
    // ADJUSTMENT END
    $matchReport .= '<p>'.$minute.'\': '.comment($teamname_att, 'attack');
    $offense_strength = $strength_att['forwards']/$strength_def['defenders'];
    $defense_strength = $strength_def['defenders']/$strength_att['forwards'];
    if (Chance_Percent(50*$offense_strength*($taktiken[$teamname_att][2]/2)*($taktiken[$teamname_att][3]/2))) {
        // attacking team passes 1st third of opponent's field side
        $matchReport .= ' '.comment($teamname_def, 'attack_advance');
        if (Chance_Percent(25*($taktiken[$teamname_def][4]/2))) {
            // the defending team fouls the attacking team
            $fouls[$teamname_def]++;
            $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul');
            if (Chance_Percent(43)) {
                // yellow card for the defending team
                // chance is correct for my purpose
                $yellowCards[$teamname_def]++;
                $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_yellow');
            }
            elseif (Chance_Percent(3)) {
                // red card for the defending team
                // chance is correct for my purpose (only 1.43% because it's an alternative way)
                $redCards[$teamname_def]++;
                $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_red');
            }
            // indirect free kick
            // only 58.23% because it's an alternative way
            $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_iFreeKick');
            if (Chance_Percent(25)) {
                // shot at the goal
                $schuesse[$teamname_att]++;
                $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_iFreeKick_shot');
                if (Chance_Percent(25)) {
                    // attacking team scores (6.25% chance)
                    $goals[$teamname_att]++;
                    $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_iFreeKick_shot_score');
                }
                else {
                    // defending goalkeeper saves
                    // only 18.75% because it's an alternative way
                    $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_iFreeKick_shot_save');
                }
            }
            else {
                // defending team cleares the ball
                // only 75% because it's an alternative way
                $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_iFreeKick_clear');
            }
        }
        elseif (Chance_Percent(17)) {
            // attacking team is caught offside
            // only 4.25% because it's an alternative way
            $offsides[$teamname_att]++;
            $matchReport .= ' '.comment($teamname_def, 'attack_advance_offside');
        }
        else {
            if (Chance_Percent(25*($taktiken[$teamname_def][5]/2))) {
                // the defending team fouls the attacking team
                $fouls[$teamname_def]++;
                $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul');
                if (Chance_Percent(43)) {
                    // yellow card for the defending team
                    // chance is correct for my purpose
                    $yellowCards[$teamname_def]++;
                    $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_yellow');
                }
                elseif (Chance_Percent(3)) {
                    // red card for the defending team
                    // chance is correct for my purpose (only 1.43% because it's an alternative way)
                    $redCards[$teamname_def]++;
                    $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_red');
                }
                if (Chance_Percent(19)) {
                    // penalty for the attacking team
                    $schuesse[$teamname_att]++;
                    $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_penalty');
                    if (Chance_Percent(77)) {
                        // attacking team scores (77% chance according to Wikipedia)
                        $goals[$teamname_att]++;
                        $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_penalty_score');
                    }
                    elseif (Chance_Percent(50)) {
                        // shot misses the goal
                        // only 11.5% because it's an alternative way
                        $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_penalty_miss');
                    }
                    else {
                        // defending goalkeeper saves
                        // only 11.5% because it's an alternative way
                        $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_penalty_save');
                    }
                }
                elseif (Chance_Percent(28)) {
                    // direct free kick
                    // only 22.68% because it's an alternative way
                    $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_dFreeKick');
                    if (Chance_Percent(33)) {
                        // shot at the goal
                        $schuesse[$teamname_att]++;
                        $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_dFreeKick_shot');
                        if (Chance_Percent(33)) {
                            // attacking team scores (10.89% chance)
                            $goals[$teamname_att]++;
                            $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_dFreeKick_shot_score');
                        }
                        else {
                            // defending goalkeeper saves
                            $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_dFreeKick_shot_save');
                        }
                    }
                    else {
                        // defending team cleares the ball
                        // only 77% because it's an alternative way
                        $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_dFreeKick_clear');
                    }
                }
                else {
                    // indirect free kick
                    // only 58.23% because it's an alternative way
                    $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_iFreeKick');
                    if (Chance_Percent(25)) {
                        // shot at the goal
                        $schuesse[$teamname_att]++;
                        $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_iFreeKick_shot');
                        if (Chance_Percent(25)) {
                            // attacking team scores (6.25% chance)
                            $goals[$teamname_att]++;
                            $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_iFreeKick_shot_score');
                        }
                        else {
                            // defending goalkeeper saves
                            // only 18.75% because it's an alternative way
                            $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_iFreeKick_shot_save');
                        }
                    }
                    else {
                        // defending team cleares the ball
                        // only 75% because it's an alternative way
                        $matchReport .= ' '.comment($teamname_def, 'attack_advance_foul_iFreeKick_clear');
                    }
                }
            }
            else {
                // attack passes the 2nd third of the opponent's field side - good chance
                $matchReport .= ' '.comment($teamname_def, 'attack_advance_advance');
                if (Chance_Percent(62*($taktiken[$teamname_att][6]/2)*($taktiken[$teamname_att][7]/2)/($taktiken[$teamname_att][8]/2)*($taktiken[$teamname_att][9]/2)/($taktiken[$teamname_def][10]/2))) {
                    // shot at the goal
                    $schuesse[$teamname_att]++;
                    $matchReport .= ' '.comment($teamname_def, 'attack_advance_advance_shot');
                    if (Chance_Percent(30*$strength_def['goalkeeper']/7/($taktiken[$teamname_att][11]/2))) {
                        // the attacking team scores
                        // only 8.78% because it's an alternative way
                        // if goalkeeper has strenth 7 then chance is 8.78% otherwise lower/higher
                        $goals[$teamname_att]++;
                        $matchReport .= ' '.comment($teamname_def, 'attack_advance_advance_shot_score');
                    }
                    else {
                        if (Chance_Percent(50)) {
                            // the defending defenders block the shot
                            $matchReport .= ' '.comment($teamname_def, 'attack_advance_advance_shot_block');
                        }
                        else {
                            // the defending goalkeeper saves
                            $matchReport .= ' '.comment($teamname_def, 'attack_advance_advance_shot_save');
                        }
                    }
                }
            }
        }
    }
    // attacking team doesn't pass 1st third of opponent's field side
    elseif (Chance_Percent(15*$defense_strength*($taktiken[$teamname_att][12]/2)*($taktiken[$teamname_att][13]/2))) {
        // quick counter attack - playing on the break
        // only 7.5% because it's an alternative way
        // if defense has strength 7 then chance is 7.5% otherwise lower/higher
        $strength_att['defenders'] = $strength_att['defenders']*0.8; // weaken the current attacking team's defense
        $matchReport .= ' '.comment($teamname_def, 'attack_quickCounterAttack');
        $matchReport .= ' ['.$goals[$_POST['team1']].':'.$goals[$_POST['team2']].']</p>'; // close comment line
        return simulate_attack($teamname_def, $teamname_att, $strength_def, $strength_att); // new attack - this one is finished
    }
    else {
        // ball goes into touch - out of the field
        $matchReport .= ' '.comment($teamname_def, 'attack_throwIn');
        if (Chance_Percent(33)) {
            // if a new chance is created
            if (Chance_Percent(50)) {
                // throw-in for the attacking team
                $matchReport .= ' '.comment($teamname_def, 'attack_throwIn_att');
                $matchReport .= ' ['.$goals[$_POST['team1']].':'.$goals[$_POST['team2']].']</p>'; // close comment line
                return simulate_attack($teamname_att, $teamname_def, $strength_att, $strength_def); // new attack - this one is finished
            }
            else {
                // throw-in for the defending team
                $matchReport .= ' '.comment($teamname_def, 'attack_throwIn_def');
                $matchReport .= ' ['.$goals[$_POST['team1']].':'.$goals[$_POST['team2']].']</p>'; // close comment line
                return simulate_attack($teamname_def, $teamname_att, $strength_def, $strength_att); // new attack - this one is finished
            }
        }
    }
    $matchReport .= ' ['.$goals[$_POST['team1']].':'.$goals[$_POST['team2']].']</p>'; // close comment line
    return TRUE; // finish the attack
}
?>

Tactical settings which should have an influence on the randomness:

  • adjustment (1=defensive, 2=neutral, 3=offensive): the higher the value is the weaker is the defense and the stronger is the offense
  • speed of play (1=slow, 2=medium, 3=fast): the higher the value is the better are the opportunities but the higher is the risk of getting a quick counter attack
  • distance of passes (1=short, 2=medium, 3=long): the higher the value is the less but better opportunities you get and the more often you are offside
  • creation of changes (1=safe, 2=medium, 3=risky): the higher the value is the better are your opportunities but the higher is the risk of getting a quick counter attack
  • pressure in defense (1=low, 2=medium, 3=high): the higher the value is the more quick counter attacks you will have
  • aggressivity (1=low, 2=medium, 3=high): the higher the value is the more attacks you will stop by fouls

Integration of the tactical settings:

All the tactical settings have a value which can be "1", "2" or "3". "2" is always neutral/medium. So I divide the values by 2. I get a ratio which is 0.5 or 1 or 1.5. I thought that I could then easily multiply the chances by this in order to integrate tactical influence. But one problem has evolved: If I multiply a chance by 2 or more tactical values, it can be higher than 100% (60 x 1.5 x 1.5 for example). So I can't integrate the tactics this way. What else can I do?


Thank you very much!

Update (2014): A few years later, I have now released the full code base of the game as open-source on GitHub. You'll find the specific implementation of this simulation in this file, if anyone is interested.

Best Answer

Build a 'weight'-based-simulation (Yeah, I just now invented that term). Each variable (regardless of its type) has a 'weight'. For example, players have weights. A good player has extra weight. A player with an injury has less weight or even no wait at all (or maybe negative weight?).

You add all the weight together (of both teams, because it is a soccer match). That weight resembles a winning chance percentage. For example;

The weight of Team A = 56, the weight of Team B = 120

The weight already shows that one team is much better (regardless of how the weight was established .. maybe they have very round balls, who cares) than the other.

Based on the weight, you could calculate a winning chance; The winning chance of Team A = 32%, The winning chance of Team B = 68%.

Now you could write an algorithm that simulates a match, influenced by the winning percentage. I wrote an algorithm like this once to draw advertisements. In my case, the number of clicks an advertisement had was the weight. The bigger the weight, the more chance the advertisement was picked by my algorithm.

I wrote the algorithm by taking a large number (like, 1000) and then assigned a range of that number to each advertisement, based on the weight percentage. In this case, Team A gets a range of 32% of 1000, which is 0 - 320, Team B gets a range of 68% which is 321 - 1000. Then my algorithm would draw a number (randomly) between 0 and 1000. The advertisement (or your teams) with the largest range (and thus largest winning chance) has the most chance of being picked by the algorithm, although it could turn out differently.

This kind of algorithm is great (although not perfect) for a balanced outcome (if users could create their own teams, buy better players, etc). You could also make any events within the game drawn by this algorithm, simply by adding a weight to the event as well..

You could add weight to an event (for example the injury of a team mate), per team, based on other weight factors within that team (how many matches played in a row, how good is (or how much weighs) their medic staff, etc). If you do the weight thing right, you could get a very balanced (and easily expandable) simulation algorithm that can both be predictable (just like some matches in real life) or totally surprising (again, just like a real life match).

UPDATE: Tactical Influences You added tactical influences, plus the question 'how would you do it?', so I will elaborate. What you are currently doing (as I understand it) is you take a percentage (the chance something occurs) and multiply that with a ratio, so that it will occur more/less.

However, because you can have multiple ratio's, you end up with a chance more then 100%.

First of all, for every tactical advantage of a team, there is (probably) a counter advantage on the other team. For example, if Team A has a weight in making goals, Team B has a counter weight in stopping goals. This sum is the universe (100%). Now the weight of both tactical advantages makes up a piece of that universe, or total weight (as I explained above).

Say that Team A is 80% certain of scoring a goal, in a certain minute, and Team B is 20% certain of stopping it (based on the weight system). But, because Team B just acquired a very good keeper, there is a tactical influence on Team B's side. This influence should shift the chance of an event, but not the universe itself! In other words, you shouldn't end up with a total chance of more then 100% (although in some cases, this isn't necessarily a bad thing)

So, you should add weight to Team B, based on the tactical influence and then re-calculate the chances based on the new weights.

Assigning Weight

Now, like you commented, assigning weight isn't easy. Certainly not if you have to 'weigh' players on their qualities. Weighing is about more then just saying that a player is 'bad' or 'good', you have to actually grade them (like in high school!). The bigger the highest grade, the more accurate the weighting system is.

Now, assigning weights to tactical influences is a bit more easier. Say that you have the following influences;

  • Stopping goals
  • Scoring goals
  • Defence
  • Attack

Now, create a pool of total weight (say, 1000, I like that number). These are 'tactical points' you could assign. These four influences make up a match, so you could assign 250 points to each influence. This number (250) is the universe of each influence.

The assignment of these points, per team, depends on the team's weight factors (like, do they have a good keeper?)

A keeper, for instance, weighs against the opponents keeper (and maybe also the people that are in between the keeper and the opponent, but let's keep it simple). Say the keeper of Team A weighs 80% of the total, and the keeper of Team B 20%. This rates how good they are, which is directly related to the tactical points they get. So Team A gets 80% of 250 stopping-goals-points and Team B gets 20% of those points.

The rest of the points can be assigned equally. In my example, I took only two keepers as the universe of wether a goal gets stopped or not. In reality, there could be a lot more weight factors (for you to figure out).

Once they are all divided, you can use the tactical points to make out the match. For each minute you could re-calculate the chance of winning. Each minute, you could also re-calculate the tactical influences (say another player enters the field, or a player is injured).

Yes, you will get a LOT of variables. But the more you get, the better a match plays. The more variables (or weights / counter weights) the more it feels like real life.