
Python simulation of chess game result
The Perpetual Chess podcast recently had an episode in which the winning chances for the Candidates Tournament were discussed, as calculated by "Chess by the Numbers". These were calculated based on Monte Carlo simulations of the full tournament, based on the player live ELO at the time of the tournament start. Chess By the Numbers stated that he ran 5000 simulations for the full tournament, which seemed woefully inadequate to me: surely at least 100 times this many could have been run, reducing the statistical noise. I thought surely it couldn't be hard to write my own code to do a similar calculation.
The ELO formula predicts the expectation number for the total points earned by each player, where the sum of points is typically 1. So either a winner gets 1 point and the loser none, or in the case of a draw each player receives a half-point. Summing the half point for a draw and the full point for a win, multiplying each by the appropriate probabilities, must therefore equal the expectation value from the ELO formula. However, the formula does not specify how this expectation value is partitioned between chances to win or draw, so a separate model is needed for draw probability.
At the highest levels of chess, more than 50% of games are drawn. This Wikipedia article documents some of the statistics. But novices are more likely to play to a win or loss than top-level players. So a model for draw probability needs to consider not just the difference in ELOs, but additionally, the sum of ELOs (or the average ELO).
One approach is described by François Labelle on the web page for his chess probability calculator. There he asserts that draw odds are worth 0.6 pawns, but a pawn is worth a number of ELO points which is an exponential function of the sum of the player ELOs. This works out to 16 ELO if the player ELOs sum to 2080 (if they average 1040).
Of course we're interested in conventional games, without draw odds. Of course with draw odds, the handicapped (typically stronger) player will do everything possible to avoid drawing, since a draw is a loss, but assume anyway that the net win-loss with draw odds would not change substantially even if the player was unaware that the draw odds existed. This is a leap of faith but represents a decent starting point for estimation.
So in this case, we can calculate the chances of a win by assigning, for example, the white player a draw odds penalty, in which case all games are a win or a loss. Then the expectation value of the point represents the win chance. Then the remainder of the expected points without draw odds are the expected points from draws. The draw chances are twice this. Of course the chance for each player to draw needs to be the same, since if one draws, so does the other.
Another factor is whether players play white or black. Arguably the white advantage is also a function of the sum of the player ELOs. But I am going to assume it's a constant number of ELO points. Wikipedia states that white gets approximately 55% of the points (black 45%) in chess due to the first move advantage. From this it is easy to calculate that white gets an effective ELO boost of approximately 35 points, assuming the exponential ELO formula.
The two formulas for ELO-based win probability are a normal formula and an exponential formula. The key difference is in the "statistical tails" -- when players of substantial ELO difference play each other, the exponential approach will give the underdog a better chance. The normal formula (using Gaussians, which yield error functions) is statistically attractive, since Gaussians are very commonly encountered in the study of probability, due to the central limit theorem.
However, normal probability distributions tend to overlook extraordinary events. For example, if a player is playing someone rated 1600 points higher, the exponential formula preduces the weaker player will get approximately 1 point per 10 thousand games. A lot can happen in 10 thousand games: for example, the stronger player may suffer a sudden medical emergency, and forfeit. On the other hand, the Gaussian formula preduces the result that the weaker player will gain only one point out of every 100 million games. Clearly the chances of an extraordinary event occurring are greater than this. So I am tempted to prefer the exponential formula.
The exponential formula is that the ratio of points gained by the stronger player over the weaker player increases by a factor of 10 for every 400 point ELO difference. This is much simpler to deal with than the normal fomula, in particular because each change in ELO has a similar impact on the relative point ratio. Once the point ratio is known, then the point fraction is determined by dividing by 1 plus the point ratio,
So all of the elements are there for a simple game simulator. Here's the python code I wrote (I'm a python newbie, so apologies for my coding style). Also I do not know how to properly format code in chess.com blogposts: it is automatically formatting this in a paragraph style. If anyone knows how to format code other than by posting screenshots, please let me know.
from random import random
from math import exp
#
# simulateGame: result can be -1 (black wins), 0 (draw), or +1 (white wins)
# input: ELO scores of white and black
#
# assume draws odds are worth a certain ELO advantage
# then calculate win % = playing against draw odds
# remaining points are draws
def simulateGame(whiteELO, blackELO):
# white ELO advantage (from chess.com according to Wikipedia)
deltaWhite = 35
# point ratio: white over black
f = 10 ** ((whiteELO + deltaWhite - blackELO) / 400)
# white point fraction (before adjusting for black advantage)
g = f / (1 + f)
# with draw odds
drawELO = 16 * exp((whiteELO + blackELO) / 2040) # https://wismuth.com/elo/calculator.html
ff = f * 10 ** (- drawELO / 400)
w = ff / (1 + ff) # white win odds (same as playing against draw odds)
d = 2 * (g - w)
r = random()
if (r < w):
return 1
elif r < w + d:
return 0
else:
return -1