A football manager has an awkward requirement: a match has to feel live, yet it also has to be reproducible. Tunnelside is a football manager game that runs in the browser. Every match is played on the server in real time while managers watch, change tactics and make substitutions. This post describes the design behind that: a pure simulation that advances one match minute at a time, driven by a seed, and a live view delivered as a stream.
The engine is a function
MatchEngine lives in the domain project of a .NET 10 solution. That project has no database, no clock and no I/O, and all randomness comes from a seeded generator. A match depends only on two things:
- its setup: both squads as they were at kick-off, both tactical plans and the rules, stored with the match as a JSON document;
- a seed derived from the match id.
The engine advances in ticks of one match minute, and the randomness of a tick is a function of the seed and the tick number. That single rule has useful consequences:
- the same setup and seed always produce the same match;
- a worker that is late can play the missed ticks in order;
- a worker that crashes resumes exactly where the stored state ends;
- events are numbered without gaps per match, which is what lets a client resume after losing its connection;
- the pace is configuration: by default one match minute takes one real minute, so a match plays out in real time.
The engine also carries a version in its random keys. Changing the model changes every future match, so calibration and balance tests guard every change.
What happens in one minute
In a minute the engine plays one to four possession sequences: about two, more at a higher tempo. Each sequence is a chain of contests whose odds come from the two team sheets:
- Who has the ball. Decided by each side's control: the passing, first touch, technique, teamwork, decisions, vision and composure of the players in their in-possession positions, weighted by their roles.
- Escape the press. The attacker's build-up quality against the defender's pressing.
- Advance in a lane. There are three lanes (left, centre, right). A lane's strength comes from the players whose positions and roles put them there in each phase.
- Create a chance. A shot, a cross, a cut-back, a through ball, a dribble, a long shot, a counter-attack chance, a rebound, a corner, a free kick or a penalty.
- The shot.
Every contest has the same shape. For attack strength a and defence strength d, the probability is
odds * a^n / (odds * a^n + d^n)
where odds is a base rate for equal sides and n is a small exponent. Every coefficient sits in one tuning file and is covered by calibration tests.
The last step is anchored to expected goals. Each kind of chance has an expected-goals value for an average finisher. The shooter's finishing, the quality of the final pass, the pressure from the defence and a random factor move it, and the goalkeeper's shot-stopping adjusts the chance of a goal. A side's goals therefore follow its expected goals, and every other outcome (a block, a miss, a save, a shot against the woodwork) only explains why a shot did not score.
Tactics are inputs, not bonuses
A tactical plan has one shape and one set of roles with the ball and another without it, so a plan that is strong in attack but careless in defence is felt. There are 12 formations, 68 roles (40 in possession, 28 out of possession) and a set of instruction levers such as tempo, width, directness, defensive line and press intensity.
To keep the choices interesting, a role is modelled as a pull on a fixed budget: its factors are normalised, so a role can emphasise one job only by doing the others less. Every formation also carries a small calibration constant, and a balance test checks that none is better than another for ordinary squads.
Showing it live
The live view is a server-sent events stream. It first sends a snapshot of the whole match, then a match-event for every event as its time arrives, a clock after every tick and an end with the final report. Events carry their sequence number as the SSE id, and a client that reconnects receives a fresh snapshot. Viewers only see events whose time has come, never the seed.
Behind the stream, a worker wakes every second, starts and ticks every match that is due, and settles finished ones. Work is claimed with SKIP LOCKED, so several API instances never play the same match twice.
Try it
Tunnelside is free to play in the browser. Build a club, set separate attacking and defending tactics and watch every match live: tunnelside.com.
Top comments (0)