One morning before work, I decided to write some code to compute a metric that measures the “sharpness” of a particular chess position. Briefly: I use the Stockfish chess engine to determine the current evaluation of the position, and then the five best moves and resulting evaluations from the position. The more variable the resulting evaluations are, the sharper the chess position is.
For example, the position shown below is move 21 from a game between grandmasters Hikaru Nakamura and Veselin Topalov. White (Nakamura) is to move. Black (Topalov) has just played 20…gxf5. The current evaluation of the position is +376 centipawns. A centipawn is 1/100th of a pawn. A positive value means in favor of white, a negative value means in favor of black. So in the position, Nakamura has a significant advantage. My metric computes the sharpness of the position as 584.8 which is high, meaning a very sharp position.
According to Stockfish, the five best moves (in square-to-square notation) and the resulting position evaluations are:
1. 'g3f5', +399 2. 'd4e5', +185 3. 'f3f5', +54 4. 'h3h4', -173 5. 'a2e6', -269
The best move, g3-f5 increases white’s advantage to +399. But the fifth best move, a2-e6, flips the advantage to -269 in black’s favor. This is clearly a very sharp position.
As a counter example, suppose a position has an evaluation of +1.50 and the five best moves lead to positions that have evaluations of +1.52, +1.55, +1.55, +1.58, +1.60. The evaluations don’t change much so the position isn’t very sharp.
I decided that a reasonable sharpness metric is the average of the absolute values of the differences between the evaluation of the current position and the evaluations of the five best moves — but with the adjustment that a change in advantage from one player to the other doubles that term. For the Nakamura-Topalov position above the calculation is:
curr value = 376
1. 399 delta = 23
2. 185 delta = 191
3. 54 delta = 322
4. -173 delta = 2 * 549 = 1098
5. -269 delta = 2 * 645 = 1290
sum = 2924
sharpness = sum / 5 = 584.8
The number of moves to consider, 5, is subjective and based on my experience as a chess player. The doubling of the deltas when the advantage switches from one player to another, is also subjective, and based on my experience.
To implement my idea, first I downloaded the open source Stockfish engine (for Windows) from https://stockfishchess.org/download/. The Stockfish engine is like a server — it accepts commands in text format using a protocol called UCI (Universal Chess Interface) and returns results as text. Using the Stockfish engine directly from a command line is very awkward and so hobbyists have created several libraries on GitHub that allow you to use a standard programming language (Python, Java, C#, JavaScript, etc.) to interact with Stockfish.
I used the very slick, but somewhat confusingly named, Python language “stockfish” library. I installed it directly over the Internet by issuing the command “pip install stockfish” on my command line. See https://github.com/zhelyabuzhsky/stockfish.
To summarize, I wrote a Python language program that calls into functions in the stockfish library, which sends UCI commands to the Stockfish engine on my machine.
The stockfish library accepts a position to analyze in FEN format. The Nakamura-Topalov position is “r2qr3/pp1b1pkp/2ppnn2/4pp2/3PP3/P3PQNP/BPP3P1/R4RK1 w – – 0 21”.
A super interesting exploration. There are quite a few things to investigate. For example, I set the Stockfish engine strength to ELO 2000 because it seems reasonable — not too high but not too low. And the default stockfish library time limit is 1000 milliseconds = 1 second, which also seems reasonable. For possible moves that lead to a forced checkmate in n moves, I assign a value of 400 (for white) or -400 (for black). This is subjective and somewhat arbitrary and I need to give this some more thought.
A possible next step will be to compute the overall sharpness of a complete game. Then maybe I could compute a sharpness metric for individual players by examining a few dozen or so of their games.

Chess is part art and part mental exercise. The history of computer science is closely intertwined with chess. Here are three nice (to my eyes at least) AI-generated images of chess art.
Demo program. Replace “lt” (less than) and “gt” with Boolean operator symbols (my blog editor chokes on symbols).
# position_sharpness.py
# Anaconda3-2023.09-0 Python 3.11.5
# engine: Stockfish 16.1
# API library: stockfish-3.28.0
# Windows 10/11
from stockfish import Stockfish
import numpy as np
print("\nBegin position sharpness analysis ")
loc = "C:\\Python\\Stockfish\\" + \
"stockfish-windows-x86-64\\stockfish\\" + \
"stockfish-windows-x86-64.exe"
stockfish = Stockfish(path=loc)
stockfish.update_engine_parameters({"UCI_Elo": 2000})
p = stockfish.get_parameters()
print("\nStockfish engine parameters: ")
print(p)
# Nakamura-Topalov 2017, white to move 21
pos_fen = "r2qr3/pp1b1pkp/2ppnn2/4pp2/3PP3/" + \
"P3PQNP/BPP3P1/R4RK1 w - - 0 21"
print("\nPosition in FEN: ")
print(pos_fen)
stockfish.set_fen_position(pos_fen)
vis_pos = stockfish.get_board_visual()
print("\nPosition visual: ")
print(vis_pos)
curr_eval = stockfish.get_evaluation()
print("\nPosition evaluation = ", end="")
print(curr_eval)
curr_val = curr_eval["value"]
bms = stockfish.get_top_moves(5)
print("\nBest 5 moves in this position:")
for i in range(len(bms)):
print(bms[i]) # could be less than 5
# this code doesn't take into account a forced mate
# when ["Centipawn"] in None. See below.
print("\nComputing sharpness ")
sum = 0
for i in range(len(bms)):
poss_val = bms[i]["Centipawn"]
delta = np.abs(curr_val - poss_val)
if (curr_val "gt" 0 and poss_val "lt" 0) or \
(curr_val "lt" 0 and poss_val "gt" 0):
delta *= 2
print("delta " + str(i) + " = " + str(delta))
sum += delta
sharpness = sum / len(bms) # could be less than 5
print("\nPosition sharpness = %0.1f " % sharpness)
# compute sharpness using a function with no chit-chat
# deal with foced mate positions
def compute_sharpness(pos_fen):
stockfish.set_fen_position(pos_fen)
curr_eval = stockfish.get_evaluation()
if curr_eval is None: return 0.0
curr_val = curr_eval["value"]
bms = stockfish.get_top_moves(5)
if bms is None or len(bms) == 0:
return 0.0
sum = 0
for i in range(len(bms)): # could be less than 5
if bms[i]["Mate"] is not None: # forced mate
# print("a forced mate exists")
if bms[i]["Mate"] "lt" 0: # black mates white
poss_val = -400
elif bms[i]["Mate"] "gt" 0: # white mates black
poss_val = 400
else:
poss_val = bms[i]["Centipawn"]
delta = np.abs(curr_val - poss_val)
if (curr_val "gt" 0 and poss_val "lt" 0) or \
(curr_val "lt" 0 and poss_val "gt" 0):
delta *= 2
sum += delta
return sum / len(bms)
# pos_fen = "r2qr3/pp1b1pkp/2ppnn2/4pp2/3PP3/" + \
# "P3PQNP/BPP3P1/R4RK1 w - - 0 21"
# pos_sharpness = compute_sharpness(pos_fen)
# print("\nPosition sharpness = %0.1f " % pos_sharpness)
print("\nEnd analysis ")


.NET Test Automation Recipes
Software Testing
SciPy Programming Succinctly
Keras Succinctly
R Programming
Visual Studio Live
Microsoft MLADS Conference
DevIntersection Conference
Machine Learning Week
Ai4 Conference
G2E Conference
iSC West Conference
You must be logged in to post a comment.