I've recently started to look for a football simulation engine (soccer over in the USA) that can be used to begin a football manager game. I realised quickly this is something lots of people try but don't necessarily finish or keep on top of:
-
https://github.com/d-nation/soccer-sim-engine
-
https://github.com/atas76/SimpleFootie
and there's plenty of online forums asking how you might make a simple match simulator, so I thought why not just give it a go and see what I come up with.
My code language of choice is Javascript and I'll be using NodeJS throughout.
Attempt 1 - Use Team Rating to generate prediction
This uses a function that generates a random number
function getRandomNumber(min, max) {
var random = Math.floor(Math.random() * (max - min + 1)) + min;
return random;
}
we can then feed minimum and maximum numbers to get a random number between the two.
This attempt uses the team rating to determine different maximums, so that better rated teams can get a higher score but it isn't impossible for the lower teams to win.
function getHighestLikely(teamRating) {
if (teamRating < 100 && teamRating > 80) {
return 20;
} else if (teamRating < 79 && teamRating > 60) {
return 8;
} else if (teamRating < 59 && teamRating > 30) {
return 2;
} else if (teamRating < 29 && teamRating > 0) {
return 1;
}
}
we can then run the following where the team ratings can be passed in by the user:
var team1rating = 73;
var team2rating = 45;
var homeTeamScore = getRandomNumber(0, getHighestLikely(team1rating));
var awayTeamScore = getRandomNumber(0, getHighestLikely(team2rating));
console.log("Home: " + homeTeamScore + " - " + awayTeamScore + " : Away");
Attempt 2 - Random Event, Random Team Member and Player Rating Comparison
I then looked at how we can utilise some random shooting events alongside a random team member and a comparison of two players ratings.
For this we will need:
- an events variable for goal scoring events
- readFile function (will show below)
- two JSON files with players and player ratings
function readFile(filePath) {
return new Promise(function (resolve, reject) {
fs.readFile(filePath, 'utf8', function (err, data) {
if (err) {
reject(err);
} else {
data = JSON.parse(data);
resolve(data);
}
})
});
}
We use the above function to read in two json files with player ratings, in this example I've put a lot of information that I may or may not use in the future:
{
"name": "Brewers",
"rating": "89",
"players": [{
"name": "Bill Gallagher",
"position": "GK",
"rating": "75",
"skill": {
"passing": "78",
"shooting": "12",
"saving": "75",
"penalty_taking": "43"
}
},
{
"name": "Fred Gallagher",
"position": "LB",
"rating": "90",
"skill": {
"passing": "83",
"shooting": "40",
"tackling": "32",
"penalty_taking": "53"
}
},
{
"name": "George Gallagher",
"position": "CB",
"rating": "84",
"skill": {
"passing": "78",
"shooting": "37",
"tackling": "21",
"penalty_taking": "66"
}
},
{
"name": "Jim Gallagher",
"position": "CB",
"rating": "75",
"skill": {
"passing": "33",
"shooting": "76",
"tackling": "76",
"penalty_taking": "21"
}
},
{
"name": "Sid Gallagher",
"position": "RB",
"rating": "82",
"skill": {
"passing": "66",
"shooting": "65",
"tackling": "81",
"penalty_taking": "88"
}
},
{
"name": "Gregory Gallagher",
"position": "LM",
"rating": "87",
"skill": {
"passing": "51",
"shooting": "88",
"tackling": "81",
"penalty_taking": "94"
}
},
{
"name": "Arthur Gallagher",
"position": "CM",
"rating": "41",
"skill": {
"passing": "33",
"shooting": "66",
"tackling": "55",
"penalty_taking": "1"
}
},
{
"name": "Cameron Gallagher",
"position": "CM",
"rating": "99",
"skill": {
"passing": "88",
"shooting": "95",
"tackling": "91",
"penalty_taking": "62"
}
},
{
"name": "Tanisha Gallagher",
"position": "RM",
"rating": "79",
"skill": {
"passing": "56",
"shooting": "79",
"tackling": "74",
"penalty_taking": "32"
}
},
{
"name": "Aiden Gallagher",
"position": "ST",
"rating": "75",
"skill": {
"passing": "83",
"shooting": "88",
"tackling": "59",
"penalty_taking": "45"
}
},
{
"name": "Louise Peverley",
"position": "ST",
"rating": "88",
"skill": {
"passing": "73",
"shooting": "61",
"tackling": "44",
"penalty_taking": "66"
}
}
],
"manager": "Aiden",
"formation": [4,4,2]
}
the two json files differed only slightly.
I will also require a variable called "events" :
var events = ["shot", "penalty"];
function playMatch(team1Config, team2Config) {
var matchEventsNo = getRandomNumber(0, 10);
console.log("Total Events in the Match: " + matchEventsNo);
readFile(team1Config).then(function (team1) {
readFile(team2Config).then(function (team2) {
while (matchEventsNo != 0) {
console.log("Event: " + matchEventsNo);
var eventTeam = getRandomNumber(1, 2);
var eventPlayerHome = team1.players[getRandomNumber(0, 10)];
var eventPlayerAway = team2.players[getRandomNumber(0, 10)];
if (eventTeam === 1) {
console.log("Event Team: " + team1.name);
} else if (eventTeam === 2) {
console.log("Event Team: " + team2.name);
}
var thisEvent = events[getRandomNumber(0, 1)];
console.log("Event Type: " + thisEvent);
if (thisEvent === "penalty") {
if (eventTeam === 1) {
console.log("Player: " + eventPlayerHome.name + "(" + eventPlayerHome.position + ") rating: " + eventPlayerHome.rating);
console.log("Player: " + team2.players[0].name + "(" + team2.players[0].position + ") rating: " + team2.players[0].rating);
if (eventPlayerHome.rating > team2.players[0].rating) {
homeScore++;
}
} else if (eventTeam === 2) {
console.log("Player: " + eventPlayerAway.name + "(" + eventPlayerAway.position + ") rating: " + eventPlayerAway.rating);
console.log("Player: " + team1.players[0].name + "(" + team1.players[0].position + ") rating: " + team1.players[0].rating);
if (eventPlayerAway.rating > team1.players[0].rating) {
awayScore++;
}
}
} else if (thisEvent === "shot") {
if (eventTeam === 1) {
console.log("Player: " + eventPlayerHome.name + "(" + eventPlayerHome.position + ") rating: " + eventPlayerHome.rating);
console.log("Player: " + team2.players[0].name + "(" + team2.players[0].position + ") rating: " + team2.players[0].rating);
if (eventPlayerHome.rating > team2.players[0].rating) {
homeScore++;
}
} else if (eventTeam === 2) {
console.log("Player: " + eventPlayerAway.name + "(" + eventPlayerAway.position + ") rating: " + eventPlayerAway.rating);
console.log("Player: " + team1.players[0].name + "(" + team1.players[0].position + ") rating: " + team1.players[0].rating);
if (eventPlayerAway.rating > team1.players[0].rating) {
awayScore++;
}
}
}
matchEventsNo--;
console.log(team1.name + " " + homeScore + " - " + awayScore + " " + team2.name);
}
});
});
}
function getRandomNumber(min, max) {
var random = Math.floor(Math.random() * (max - min + 1)) + min;
return random;
}
This created up to ten random events per "Match". For each match it would then decide which of two events had occurred. Either a penalty or a shot. It would then decide which of the two teams (randomly again) has incurred the event. We then channel a series of if statements to determine if there was a goal by comparing the player who took the shot/penalty (randomly chosen from the team) against the opposition goalkeepers ability.
We can run this using:
playMatch("teams/team1.json", "teams/team2.json").then(function(){
});
This gives us a limited number of goals (10 max) but can be increased by saying a minimum of X events and a maximum of (infinite) events to increase goals that can be scored.
Random "Plays" made of up to 10 random events.
Whilst this was good and worked, I thought the best way to boost this method of generating football events and therefore goals was to create "plays" which would consist of 10 random events. These would begin by an event starter such as a throw in, corner, goal kick, free kick or a penalty.
All of these are events that "start" play from a stoppage. After this has been determined a continue of the event is decided as either pass, cross or shoot. A play is only finished being generate once a "shoot" token has been given.
Once the 10 plays have been decided with their potentially infinite events, there is a formula run to see if a goal is scored. First we decide which team is doing each of the 10 events (an array of 10, each number in the array either being 0 or 1 to determine if the home or away team are performing the "play").
We then compare the players skill rating for the given event against a random number between 0 and 100. Thus higher rated players are more likely to be successful in whatever event they are doing i.e. passing. At any stage a pass, cross, shot or penalty can miss thus ending the play and meaning no goal will be scored.
var fs = require("fs");
var async = require("async");
var teamName;
var userName;
var eventStart = ["throwin", "corner", "goalkick", "freekick", "penalty"];
var events = ["pass", "cross", "shoot"];
var homeScore = 0;
var awayScore = 0;
var play = [];
var eventTeams = [];
playMatch("teams/team1.json", "teams/team2.json");
resetVars();
function playMatch(team1Config, team2Config) {
eventTeam().then(function () {
readFile(team1Config).then(function (team1) {
readFile(team2Config).then(function (team2) {
async.eachSeries(eventTeams, function eachTeam(thisTeam, thisTeamCallback) {
console.log("--------------------");
console.log("Starting Play");
if (thisTeam === 0) {
generatePlay().then(function () {
goalScored(team1, team2, team1).then(function (score) {
if (score === 1) {
console.log("Team 1 scored");
homeScore++;
play = [];
thisTeamCallback();
} else {
console.log("Team 1 missed");
play = [];
thisTeamCallback();
}
});
});
} else {
generatePlay().then(function () {
goalScored(team1, team2, team2).then(function (score) {
if (score === 1) {
console.log("Team 2 scored");
awayScore++;
play = [];
thisTeamCallback();
} else {
console.log("Team 2 missed");
play = [];
thisTeamCallback();
}
});
});
}
}, function afterAllTppUserOrgs() {
console.log(team1.name + " " + homeScore + " - " + awayScore + " " + team2.name);
});
});
});
});
}
function eventTeam() {
return new Promise(function (resolve, reject) {
var playTotal = getRandomNumber(1, 10);
console.log("Total plays in the Match: " + playTotal);
while (playTotal !== 0) {
eventTeams.push(getRandomNumber(0, 1));
playTotal--;
if (playTotal === 0) {
resolve();
}
}
});
}
function goalScored(team1, team2, whichTeam) {
return new Promise(function (resolve, reject) {
var score = 0;
async.eachSeries(play, function eachEvent(playEvent, playEventCallback) {
console.log(playEvent);
if (score === -1) {
playEventCallback();
} else {
if (playEvent === "throwin" || playEvent === "corner" || playEvent === "goalkick" || playEvent === "freekick" || playEvent === "pass" || playEvent === "cross") {
if (whichTeam.players[getRandomNumber(1, 10)].skill.passing > getRandomNumber(0, 100)) {
playEventCallback();
} else {
score = -1;
resolve(score);
}
} else if (playEvent === "penalty" || playEvent === "shoot") {
if (whichTeam.players[getRandomNumber(1, 10)].skill.shooting > getRandomNumber(0, 100)) {
console.log("goal scored");
score++;
resolve(score);
} else {
console.log("shot missed");
resolve(score);
}
}
}
}, function afterAllEvents() {
resolve(score);
});
});
}
function getRandomNumber(min, max) {
var random = Math.floor(Math.random() * (max - min + 1)) + min;
return random;
}
function generatePlay() {
return new Promise(function (resolve, reject) {
var startEvent = eventStart[getRandomNumber(0, 4)];
play.push(startEvent);
if (startEvent === "penalty") {
resolve(play);
} else {
newEventInPlay(function () {
resolve(play);
});
}
});
}
function newEventInPlay(callback) {
newEvent = events[getRandomNumber(0, 2)];
play.push(newEvent);
if (newEvent === "shoot") {
callback();
} else {
newEventInPlay(callback);
}
}
function resetVars() {
homeScore = 0;
awayScore = 0;
}
function readFile(filePath) {
return new Promise(function (resolve, reject) {
fs.readFile(filePath, 'utf8', function (err, data) {
if (err) {
reject(err);
} else {
data = JSON.parse(data);
resolve(data);
}
})
});
Added tackling and saving to the formula
My final thought was how I could incorporate the defensive teams statistics to make it more of a two team game. To do this, when a player misses a pass or cross etc. We see if they were tackled or intercepted after the failure.
If yes, the particular play ends with no further actions. If not, the play continues. When a player shoots successfully (the rating is higher than the random number generated between 0 and 100) we see if the keeper was able to save it. To alleviate the bias this gives against scoring, we play many more events. In this example, 30.
var fs = require("fs");
var async = require("async");
var eventStart = ["throwin", "corner", "goalkick", "freekick", "penalty"];
var events = ["pass", "cross", "shoot"];
var homeScore = 0;
var awayScore = 0;
var play = [];
var eventTeams = [];
playMatch("teams/team1.json", "teams/team2.json");
function playMatch(team1Config, team2Config) {
eventTeam().then(function () {
readFile(team1Config).then(function (team1) {
readFile(team2Config).then(function (team2) {
console.log("Todays Match is: " + team1.name + " vs " + team2.name);
async.eachSeries(eventTeams, function eachTeam(thisTeam, thisTeamCallback) {
//console.log("--------------------");
//console.log("Starting Play");
if (thisTeam === 0) {
generatePlay().then(function () {
goalScored(team1, team2).then(function (score) {
if (score === 1) {
//console.log(team1.name + " scored");
homeScore++;
play = [];
thisTeamCallback();
} else {
//console.log(team1.name + " missed");
play = [];
thisTeamCallback();
}
});
});
} else {
generatePlay().then(function () {
goalScored(team2, team1).then(function (score) {
if (score === 1) {
//console.log(team2.name + " scored");
awayScore++;
play = [];
thisTeamCallback();
} else {
//console.log(team2.name + " missed");
play = [];
thisTeamCallback();
}
});
});
}
}, function afterAllTppUserOrgs() {
console.log(team1.name + " " + homeScore + " - " + awayScore + " " + team2.name);
resetVars();
resolve();
});
});
});
});
}
function eventTeam() {
return new Promise(function (resolve, reject) {
var playTotal = getRandomNumber(1, 30);
//console.log("Total plays in the Match: " + playTotal);
while (playTotal !== 0) {
eventTeams.push(getRandomNumber(0, 1));
playTotal--;
if (playTotal === 0) {
resolve();
}
}
});
}
function goalScored(whichTeam, oppTeam) {
return new Promise(function (resolve, reject) {
var score = 0;
async.eachSeries(play, function eachEvent(playEvent, playEventCallback) {
var thisPlayer = whichTeam.players[getRandomNumber(1, 10)];
var oppPlayer = oppTeam.players[getRandomNumber(0, 10)];
//console.log(thisPlayer.name + " (" + thisPlayer.position + ")");
//console.log(playEvent);
if (score === -1) {
playEventCallback();
} else {
if (playEvent === "throwin" || playEvent === "corner" || playEvent === "goalkick" || playEvent === "freekick" || playEvent === "pass" || playEvent === "cross") {
if (thisPlayer.skill.passing > getRandomNumber(0, 100)) {
playEventCallback();
} else {
//console.log("Retain play?");
//console.log("Challenge made by: " + oppPlayer.name + " (" + oppPlayer.position + ")");
if (oppPlayer.skill.tackling > getRandomNumber(0, 100)) {
//console.log("tackled. posession lost");
score = -1;
resolve(score);
} else {
//console.log("retained possession. Continue play");
playEventCallback();
}
}
} else if (playEvent === "penalty" || playEvent === "shoot") {
if (thisPlayer.skill.shooting > getRandomNumber(0, 100)) {
if (oppTeam.players[0].skill.saving > getRandomNumber(0, 100)) {
//console.log("shot saved by " + oppTeam.players[0].name + " (" + oppTeam.players[0].position + ")");
resolve(score);
} else {
//console.log("goal conceeded by " + oppTeam.players[0].name + " (" + oppTeam.players[0].position + ")");
//console.log("goal scored by " + thisPlayer.name + " (" + thisPlayer.position + ")");
score++;
resolve(score);
}
} else {
//console.log("shot missed");
resolve(score);
}
}
}
}, function afterAllEvents() {
resolve(score);
});
});
}
function getRandomNumber(min, max) {
var random = Math.floor(Math.random() * (max - min + 1)) + min;
return random;
}
function generatePlay() {
return new Promise(function (resolve, reject) {
var startEvent = eventStart[getRandomNumber(0, 4)];
play.push(startEvent);
if (startEvent === "penalty") {
resolve(play);
} else {
newEventInPlay(function () {
resolve(play);
});
}
});
}
function newEventInPlay(callback) {
newEvent = events[getRandomNumber(0, 2)];
play.push(newEvent);
if (newEvent === "shoot") {
callback();
} else {
newEventInPlay(callback);
}
}
function resetVars() {
homeScore = 0;
awayScore = 0;
play = [];
eventTeams = [];
}
function readFile(filePath) {
return new Promise(function (resolve, reject) {
fs.readFile(filePath, 'utf8', function (err, data) {
if (err) {
reject(err);
} else {
data = JSON.parse(data);
resolve(data);
}
})
});
}
What next?
The next step for this project would require much more intelligent movement, ratings, and event play for the user. I'm planning on developing this over the next few months and with any luck will have something to open source by the middle of next year.
However, this gives quite a good amount of data and events already. At the bottom I will show the output we see using this bit of code. However, if you have used, or adapted this code. Feel free to get in touch and let me know how it goes by
email.
Good Luck!
Output:
Total plays in the Match: 22
Todays Match is: Brewers vs Arsenal
--------------------
Starting Play
Jim Boden (CB)
penalty
penalty on target by Jim Boden (CB)
goal conceeded by Bill Gallagher (GK)
Arsenal scored
--------------------
Starting Play
Gregory Gallagher (LM)
throwin
Retain play?
Challenge made by: Sid Boden (RB)
tackled. posession lost
Brewers missed
--------------------
Starting Play
Louise Boden (ST)
throwin
Retain play?
Challenge made by: Bill Gallagher (GK)
retained possession. Continue play
Arthur Boden (CM)
cross
Retain play?
Challenge made by: Cameron Gallagher (CM)
tackled. posession lost
Arsenal missed
--------------------
Starting Play
Gregory Boden (LM)
penalty
penalty on target by Gregory Boden (LM)
penalty saved by Bill Gallagher (GK)
Arsenal missed
--------------------
Starting Play
Aiden Boden (ST)
penalty
penalty on target by Aiden Boden (ST)
goal conceeded by Bill Gallagher (GK)
Arsenal scored
--------------------
Starting Play
George Boden (CB)
goalkick
Retain play?
Challenge made by: Louise Peverley (ST)
retained possession. Continue play
Sid Boden (RB)
shoot
goal conceeded by Bill Gallagher (GK)
goal scored by Sid Boden (RB)
Arsenal scored
--------------------
Starting Play
Louise Boden (ST)
corner
Retain play?
Challenge made by: Aiden Gallagher (ST)
retained possession. Continue play
Tanisha Boden (RM)
shoot
shot saved by Bill Gallagher (GK)
Arsenal missed
--------------------
Starting Play
Gregory Boden (LM)
freekick
Gregory Boden (LM)
cross
Cameron Boden (CM)
cross
Gregory Boden (LM)
cross
Sid Boden (RB)
pass
Arthur Boden (CM)
shoot
goal conceeded by Bill Gallagher (GK)
goal scored by Arthur Boden (CM)
Arsenal scored
--------------------
Starting Play
Jim Boden (CB)
goalkick
Retain play?
Challenge made by: Arthur Gallagher (CM)
retained possession. Continue play
George Boden (CB)
pass
Tanisha Boden (RM)
cross
Retain play?
Challenge made by: Cameron Gallagher (CM)
retained possession. Continue play
Aiden Boden (ST)
pass
Gregory Boden (LM)
pass
Fred Boden (LB)
pass
Retain play?
Challenge made by: Cameron Gallagher (CM)
retained possession. Continue play
Sid Boden (RB)
pass
Arthur Boden (CM)
cross
Retain play?
Challenge made by: Bill Gallagher (GK)
retained possession. Continue play
Jim Boden (CB)
cross
Retain play?
Challenge made by: Jim Gallagher (CB)
retained possession. Continue play
Gregory Boden (LM)
shoot
goal conceeded by Bill Gallagher (GK)
goal scored by Gregory Boden (LM)
Arsenal scored
--------------------
Starting Play
Cameron Gallagher (CM)
throwin
Gregory Gallagher (LM)
cross
Retain play?
Challenge made by: Tanisha Boden (RM)
tackled. posession lost
Brewers missed
--------------------
Starting Play
Louise Boden (ST)
goalkick
Cameron Boden (CM)
pass
George Boden (CB)
pass
Retain play?
Challenge made by: Louise Peverley (ST)
retained possession. Continue play
Fred Boden (LB)
shoot
shot missed
Arsenal missed
--------------------
Starting Play
Jim Boden (CB)
freekick
Jim Boden (CB)
cross
Retain play?
Challenge made by: Aiden Gallagher (ST)
retained possession. Continue play
Tanisha Boden (RM)
pass
Retain play?
Challenge made by: Tanisha Gallagher (RM)
tackled. posession lost
Arsenal missed
--------------------
Starting Play
Aiden Boden (ST)
corner
George Boden (CB)
cross
Louise Boden (ST)
cross
Retain play?
Challenge made by: George Gallagher (CB)
retained possession. Continue play
Arthur Boden (CM)
shoot
shot saved by Bill Gallagher (GK)
Arsenal missed
--------------------
Starting Play
Sid Gallagher (RB)
corner
Sid Gallagher (RB)
shoot
goal conceeded by Bill Boden (GK)
goal scored by Sid Gallagher (RB)
Brewers scored
--------------------
Starting Play
George Gallagher (CB)
freekick
Gregory Gallagher (LM)
cross
Retain play?
Challenge made by: Louise Boden (ST)
tackled. posession lost
Brewers missed
--------------------
Starting Play
Sid Gallagher (RB)
penalty
penalty on target by Sid Gallagher (RB)
goal conceeded by Bill Boden (GK)
Brewers scored
--------------------
Starting Play
Tanisha Boden (RM)
corner
Sid Boden (RB)
cross
Arthur Boden (CM)
cross
Retain play?
Challenge made by: Jim Gallagher (CB)
tackled. posession lost
Arsenal missed
--------------------
Starting Play
Arthur Boden (CM)
goalkick
Retain play?
Challenge made by: Tanisha Gallagher (RM)
retained possession. Continue play
Aiden Boden (ST)
shoot
shot saved by Bill Gallagher (GK)
Arsenal missed
--------------------
Starting Play
Arthur Boden (CM)
corner
Retain play?
Challenge made by: Louise Peverley (ST)
tackled. posession lost
Arsenal missed
--------------------
Starting Play
Tanisha Gallagher (RM)
corner
Sid Gallagher (RB)
shoot
shot saved by Bill Boden (GK)
Brewers missed
--------------------
Starting Play
Arthur Boden (CM)
corner
Tanisha Boden (RM)
pass
George Boden (CB)
shoot
shot missed
Arsenal missed
--------------------
Starting Play
Sid Boden (RB)
throwin
Tanisha Boden (RM)
shoot
shot missed
Arsenal missed
Brewers 2 - 5 Arsenal