Play Rather Than Code

Watching The Queen’s Gambit on Netflix nudged me to play chess online. I’ve spent time on chess over the last few years… programming an engine, not actually playing. I’m very rusty. Though my puzzle rating is decent. It’s much higher than my game rating, suggesting I need to work on time management. Thinking fast is not my strength, but everyone online plays blitz or bullet so here goes nothing…

Last night I joined a bullet tournament with a 2+1 clock. Each player has 2 minutes to make all their moves in the game, plus 1 second added to their clock after each move. Many of the games I played were blunderfests. But I managed to play solidly in a few. Here’s a well played game considering I’m making most of my moves in like 2 or 3 seconds. I play white (DamageInkk). Afterwards I had MadChess analyze the game. Converting a “won” game is not inevitable for a patzer like me, especially with the time pressure, so I was glad to do it and glad to see MadChess confirm I played accurately from move 16 on.

I like the checkmate configuration at the end of the game: My opponent chases my pawn, walking right into a discovered check / pawn promotion mate.

Knight Outpost

A knight positioned on an outpost controls opponent territory, restricting movement of the opponent’s pieces on the opponent’s side of the board. An outpost is defined as a square on the fifth or sixth rank, occupied by a minor piece (usually a knight), supported by its own pawn, and positioned such that it cannot ever be attacked by an opposing pawn. In other words, the opponent has no pawns on the neighboring files, or if the opponent has pawns on the neighboring files, they’ve advanced even with or beyond the outpost square.

r4rk1/1qb2ppp/1pN2n2/pP2p3/P3P3/4BP2/3RQ1PP/3R2K1 b - - 0 1

Red highlights in the above position indicate squares in black territory attacked by the white knight on c6. Yellow highlights indicate squares in white territory attacked by the same knight.

Controlling Space

Controlling board space- especially central squares- provides room to maneuver pieces. It cramps the opponent’s position, who will have difficulty bringing pieces out from behind their pawns. And it enables a quick redeployment of pieces from one zone of action (queenside, for example) to another zone of action (kingside) to initiate an attack or muster a defense around the king.

Be careful not to overextend when grabbing space. In the following position, white appears to control more space than black. Black’s counterplay demonstrates, however, that white merely occupies space but does not control it.

r1bq1rk1/ppp1ppbp/n2p1np1/4P3/2PP1P2/2N2N2/PP4PP/R1BQKB1R b KQ - 0 1

Black moves his knight to safety (and attacks the e5 central square) then takes advantage of white’s greediness.

Fortunes have changed. Now black controls more territory.

MadChess 3.0 Beta 4b7963b (Remove Aspiration Windows)

When analyzing a Carlsen versus Vachier-Lagrave game, I noticed MadChess 3.0 Beta struggling to find Magnus Carlsen’s crushing 23rd move, d6, in the following position.

2r2rk1/1ppq2b1/1n6/1N1Ppp2/p7/Pn2BP2/1PQ1BP2/3RK2R w K - 0 23

In the engine output displayed by the Hiarcs Chess GUI, I noticed MadChess kept restarting its search of ply 18. It indicated it was searching the first move (of 47 legal moves), second move, third move, etc… then would restart searching the first move again, still on ply 18. It restarted searching the first move of ply 18 numerous times. I suspected this was due to search instability caused by aspiration windows: searching a window centered around the previous ply’s score (ply 17 for this position) causes a fail high, the window is expanded in the direction of the fail high, sometimes (bizarrely) this causes a fail low, the window is expanded in the direction of the fail low, causing a fail high, low, high, etc… until the window is wide enough in both directions to return a stable score.

For this position, with aspiration windows enabled (by default they are), MadChess took four minutes to find Carlsen’s move. Even though MadChess is single-threaded, it has a non-deterministic search because it generates random Zobrist position keys (for the initial key, piece squares, side-to-move, castling rights, and en passant squares). Due to this non-determinism, in some cases restarting MadChess and searching the position again caused MadChess to recommend sub-optimal moves, unable to find Carlsen’s move even after 18 minutes, when I gave up.

With Aspiration Windows
=======================
0.00: +1.10/3 23.Bc4 Nxc4 24.Qxc4
0.00: +1.24/4 23.Na7 Nxd5 24.Nxc8 Rxc8
0.00: +1.06/5 23.Na7 Ra8 24.d6 Rxa7 25.Bc4+
0.00: +0.97/6 23.Na7 Rcd8 24.d6 Rf7 25.Bxb6 cxb6
0.00: +1.71/7 23.Na7 Nd4 24.Bxd4 exd4 25.Nxc8 Rxc8 26.Bc4
0.00: +1.17/8 23.Na7 Nd4 24.Bxd4 exd4 25.Rh4 Nxd5 26.Nxc8 Rxc8
0.00: +1.11/9 23.Kf1 Qf7 24.Rh4 Nxd5 25.Na7 Qf6 26.Rxa4 Nxe3+ 27.fxe3
0.00: +1.53/10 23.Na7 Ra8 24.Bxb6 cxb6 25.Bb5 Qd6 26.Bxa4 Rxa7 27.Bxb3 Ra5
0.00: +1.44/11 23.Bc4 Na5 24.Bf1 Nxd5 25.Qxa4 b6 26.Qh4 c6 27.Qh7+ Kf7 28.Bxb6
0.00: +1.31/12 23.Bc4 Nxc4 24.Qxc4 Rf7 25.Bh6 Nd4 26.Bxg7 Rxg7 27.Nxd4 exd4 28.d6+ Kf8
0.01: +1.33/13 23.Bc4 Nxc4 24.Qxc4 Rf7 25.Bh6 Bxh6 26.Rxh6 Kg7 27.Rh1 f4 28.Rg1+ Kf8 29.Qxa4
0.03: +1.71/14 23.Rh4 Bf6 24.Rb4 Kh8 25.Na7 Nd4 26.Bxd4 exd4 27.Nxc8 Rxc8 28.b3 Nxd5 29.Rxa4 Kg8
0.08: +1.34/15 23.Rh4 Rf7 24.Bxb6 cxb6 25.Rc4 Rcf8 26.Rxa4 Nd4 27.Nxd4 exd4 28.Qc4 Rc8 29.Qb5 Kh7 30.Qxd7
0.10: +1.36/16 23.Rh4 Rf7 24.Bxb6 cxb6 25.Rc4 Rcf8 26.Rxa4 Nd4 27.Nxd4 exd4 28.Qc4 Rc8 29.Qb5 Be5 30.Ra7 Bb8
0.15: +1.35/17 23.Rh4 Rf7 24.Bxb6 cxb6 25.Rc4 Rcf8 26.Rxa4 Nd4 27.Nxd4 exd4 28.Qc4 Rd8 29.Qb4 Qxd5 30.Qxb6 Kh7 31.Kf1
0.42: +1.84/18 23.Rg1 Rf7 24.Rg5 Kh8 25.Qd3 Kg8 26.Bf1 Nd4 27.Nxd4 exd4 28.Qxd4 Rd8 29.Qe5 Nxd5 30.Bh3 Kh8 31.Rh5+ Kg8
1.03: +1.62/19 23.Rg1 Rf7 24.Rg5 Kh8 25.Bd3 e4 26.fxe4 f4 27.Bxb6 cxb6 28.Qe2 f3 29.Qf1 Rc5 30.e5 Rxd5 31.Qh1+ Kg8 32.Qh7+
1.36: +1.62/20 23.Rg1 Rf7 24.Rg5 Kh8 25.Bd3 e4 26.fxe4 f4 27.Bxb6 cxb6 28.Qe2 f3 29.Qf1 Rc5 30.e5 Rxd5 31.Qh1+ Kg8 32.Qh7+ Kf8
3.31: +2.07/21 23.Rg1 Rf7 24.Rg5 Kh8 25.Bd3 e4 26.fxe4 f4 27.e5 fxe3 28.e6 exf2+ 29.Kf1 Qe7 30.Rh5+ Kg8 31.Bh7+ Kf8 32.exf7 Qxf7 33.Qxf2
6.11: +2.39/22 23.Rg1 Rf7 24.Rg5 Kf8 25.Na7 Ra8 26.d6 Nd4 27.Bxd4 exd4 28.dxc7 Qxc7 29.Qxc7 Rxc7 30.Rxf5+ Kg8 31.Nb5 Ra5 32.Nxc7 Rxf5 33.Ne6 Rd5
10.23: +1.92/23 23.Rg1 Rf7 24.Bh6 Nd4 25.Nxd4 exd4 26.Rxd4 Re8 27.Qd3 Kf8 28.Qd2 Kg8 29.d6 cxd6 30.Rxd6 Qc7 31.Rxb6 Qxb6 32.Bxg7 Rxg7 33.Rxg7+ Kxg7 34.Qd7+
18.10: +2.15/24 23.Na7 Ra8 24.Nb5 Rac8 25.Rg1 Rf7 26.Rg5 Kf8 27.Na7 Rb8 28.Bb5 Qe7 29.Rxf5 Nd4 30.Bxd4 exd4+ 31.Kf1 Rxf5 32.Qxf5+ Qf7 33.Qe4 Qxd5 34.Qf4+ Qf7

I’ve long suspected aspiration windows are worthless, so I disabled them, produced a new build of MadChess, restarted the Hiracs Chess GUI, and searched the position again. MadChess found Carlsen’s move in 45 seconds. Restarting MadChess and searching the position again reliably found Carlsen’s move at ply 18 within 40 – 50 seconds.

Without Aspiration Windows
==========================
0.00: +1.10/3 23.Bc4 Nxc4 24.Qxc4
0.00: +1.24/4 23.Na7 Nxd5 24.Nxc8 Rxc8
0.00: +1.06/5 23.Na7 Ra8 24.d6 Rxa7 25.Bc4+
0.00: +0.97/6 23.Na7 Rcd8 24.d6 Rf7 25.Bxb6 cxb6
0.00: +2.04/7 23.Na7 Rcd8 24.Bxb6 cxb6 25.Bc4 b5 26.Bxb5
0.00: +1.71/8 23.Na7 Nd4 24.Bxd4 exd4 25.Rh5 Qe8 26.Nxc8 Qxh5
0.00: +1.43/9 23.Na7 Ra8 24.Bxb6 cxb6 25.Bb5 Qe7 26.Bxa4 Qg5 27.Bxb3
0.00: +1.40/10 23.Na7 Ra8 24.Bxb6 cxb6 25.Bb5 Qd6 26.Bxa4 Rxa7 27.Qxb3 Qc5
0.00: +1.57/11 23.Rg1 f4 24.Bxb6 cxb6 25.Qe4 Nc1 26.Na7 Rc5 27.Qxe5 Rc2 28.Bb5
0.00: +1.90/12 23.Rg1 f4 24.Bxb6 cxb6 25.Qe4 Rc1 26.Qxe5 Rc5 27.Nd6 Rc2 28.Qe6+ Qxe6
0.02: +1.40/13 23.Rh4 Qd8 24.Rxa4 Nd4 25.Nxd4 exd4 26.Bxd4 Nxa4 27.Bxg7 Kxg7 28.Qxa4 Rh8 29.Bb5
0.03: +1.46/14 23.Rh4 Qe7 24.Rxa4 Nd4 25.Nxd4 exd4 26.d6 Qe6 27.d7 Nxa4 28.dxc8=Q Rxc8 29.Bxd4 Bxd4
0.05: +1.60/15 23.Rh4 Qe7 24.Rb4 Nd4 25.Nxd4 exd4 26.Bxd4 Bxd4 27.Rbxd4 c5 28.d6 cxd4 29.Qxc8 Qxd6 30.Qxb7
0.12: +1.31/16 23.Rh4 Rf7 24.Rb4 Bf8 25.Rh4 Qd8 26.Rh5 Nxd5 27.Rg5+ Bg7 28.Rxf5 Rd7 29.Rh5 Nxe3 30.fxe3 Rxd1+
0.19: +1.30/17 23.Rh4 Rf7 24.Rb4 Bf8 25.Rh4 Rg7 26.Bf1 f4 27.Na7 Rd8 28.Bxb6 cxb6 29.Bb5 Rg1+ 30.Ke2 Qg7 31.Bxa4
0.45: +1.55/18 23.d6 cxd6 24.Qd3 Nd4 25.Nxd4 exd4 26.Bxd4 Rc6 27.Bxb6 Rxb6 28.Qe3 Qc7 29.Qe6+ Qf7 30.Bc4 Rc6 31.Qxf7+ Rxf7
0.53: +1.87/19 23.d6 cxd6 24.Qd3 Nd4 25.Nxd4 exd4 26.Bxd4 Rc6 27.Bxb6 Rxb6 28.Qe3 Rxb2 29.Bc4+ Rf7 30.Bxf7+ Kxf7 31.Rh4 Kg8 32.Kf1

Knowing the performance of a chess engine on one particular position is not indicative of how it performs in games against a variety of opponents, I decided to remove the aspiration window code (not merely disable it), then run a gauntlet tournament against ten opponents. I removed the following code.

private int[] _singlePvAspirationWindows = new[] {100, 500};
private int[] _multiPvAspirationWindows = new[] {100, 200, 300, 400, 500, 600, 700,
800, 900, 1000, 2000, 5000, StaticScore.Max};
// ...
private int GetScoreWithinAspirationWindow(Board Board, int PrincipalVariations)
{
var bestScore = _bestMoves[0].Score;
if ((_originalHorizon < _aspirationMinToHorizon) || (Math.Abs(bestScore) >= StaticScore.Checkmate))
{
// Reset move scores, then search moves with infinite aspiration window.
for (var moveIndex = 0; moveIndex < Board.CurrentPosition.MoveIndex; moveIndex++)
{
_rootMoves[moveIndex].Score = -StaticScore.Max;
}
return GetDynamicScore(Board, 0, _originalHorizon, false, -StaticScore.Max, StaticScore.Max);
}
int[] aspirationWindows;
// ReSharper disable once ConvertSwitchStatementToSwitchExpression
switch (_scoreError)
{
case 0 when PrincipalVariations == 1:
// Use single PV aspiration windows.
aspirationWindows = _singlePvAspirationWindows;
break;
default:
// Use multi PV aspiration windows.
aspirationWindows = _multiPvAspirationWindows;
break;
}
var alpha = 0;
var beta = 0;
var scorePrecision = ScorePrecision.Exact;
for (var aspirationIndex = 0; aspirationIndex < aspirationWindows.Length; aspirationIndex++)
{
var aspirationWindow = aspirationWindows[aspirationIndex];
// Reset move scores.
for (var moveIndex = 0; moveIndex < Board.CurrentPosition.MoveIndex; moveIndex++)
{
_rootMoves[moveIndex].Score = -StaticScore.Max;
}
// Adjust alpha / beta window.
// ReSharper disable once SwitchStatementMissingSomeCases
switch (scorePrecision)
{
case ScorePrecision.LowerBound:
// Fail High
alpha = beta - 1;
beta = Math.Min(beta + aspirationWindow, StaticScore.Max);
break;
case ScorePrecision.UpperBound:
// Fail Low
beta = alpha + 1;
alpha = Math.Max(alpha - aspirationWindow, -StaticScore.Max);
break;
case ScorePrecision.Exact:
// Initial Aspiration Window
// Center aspiration window around best score from prior ply.
alpha = PrincipalVariations > 1
? _lastAlpha
: bestScore - _scoreError - aspirationWindow;
beta = bestScore + aspirationWindow;
break;
default:
throw new Exception(scorePrecision + " score precision not supported.");
}
// Search moves with aspiration window.
if (_debug()) _writeMessageLine(
$"info string LowAspirationWindow = {aspirationWindow} Alpha = {alpha} Beta = {beta}");
var score = GetDynamicScore(Board, 0, _originalHorizon, false, alpha, beta);
if (Math.Abs(score) == StaticScore.Interrupted) return score; // Stop searching.
if (score >= beta)
{
// Search failed high.
scorePrecision = ScorePrecision.LowerBound;
if (PvInfoUpdate) UpdateInfoScoreOutsideAspirationWindow(Board.Nodes, score, true);
continue;
}
// Find lowest score.
var lowestScore = PrincipalVariations == 1
? score
: GetBestScore(Board.CurrentPosition, PrincipalVariations);
if (lowestScore <= alpha)
{
// Search failed low.
scorePrecision = ScorePrecision.UpperBound;
if (PvInfoUpdate) UpdateInfoScoreOutsideAspirationWindow(Board.Nodes, score, false);
continue;
}
// Score within aspiration window.
_lastAlpha = alpha;
return score;
}
// Search moves with infinite aspiration window.
return GetDynamicScore(Board, 0, _originalHorizon, false, -StaticScore.Max, StaticScore.Max);
}
private int GetBestScore(Position Position, int Rank)
{
Debug.Assert(Rank > 0);
if (Rank == 1)
{
var bestScore = -StaticScore.Max;
for (var moveIndex = 0; moveIndex < Position.MoveIndex; moveIndex++)
{
var score = _rootMoves[moveIndex].Score;
if (score > bestScore) bestScore = score;
}
return bestScore;
}
// Sort moves and return Rank best move (1 based index).
Array.Sort(_rootMoves, 0, Position.MoveIndex, _moveScoreComparer);
return _rootMoves[Rank - 1].Score;
}

The tournament confirmed my suspicions. MadChess performed 9 Elo stronger without aspiration windows.

See my Are Aspiration Windows Worthless? post on the TalkChess forum for a discussion of this blog post.

 

Feature Category Date Commit1 WAC2 Elo Rating3 Improvement
Remove Aspiration Windows Search 2020 Dec 20 4b7963b 290 2530 +9
Time Management Search 2020 Dec 19 d143bb5 286 2521 +8
Crash Bug Search 2020 Aug 29 2d855ec 288 2513 +0
King Safety Evaluation 2020 Aug 16 6794c89 288 2513 +63
Eval Param Tuning Evaluation 2020 Jul 23 bef88d5 283 2450 +30
Late Move Pruning Search 2020 Feb 08 6f3d17a 288 2420 +29
Piece Mobility Evaluation 2020 Feb 01 5c5d4fc 282 2391 +62
Passed Pawns Evaluation 2018 Dec 27 103 279 2329 +119
Staged Move Generation Search 2018 Dec 15 93 275 2210 +39
History Heuristics Search 2018 Dec 03 84 275 2171 +28
Eval Param Tuning Evaluation 2018 Nov 24 75 272 2143 +47
Sophisticated Search
Material and Piece Location
Baseline 2018 Nov 08 58 269 2096 0
  1. GitHub commit (hash) or Subversion source code revision (integer)
  2. Win At Chess position test, 3 seconds per position
  3. Bullet chess, 2 min / game + 1 sec / move

MadChess 3.0 Beta d143bb5 (Time Management)

I improved MadChess 3.0 Beta’s time management. I added code that increases MoveTimeSoftLimit, a TimeSpan variable that controls how long the engine examines a position (in a timed game) before responding with its move. The code increases MoveTimeSoftLimit 25% each ply (depth >= 9) if the score decreases at least one third of a pawn from the prior ply. In some engines this is known as “panic time.” The engine notices the score dropping and- to anthropomorphize it- panics like a human chess player would, spending more time than usual searching for a move that prevents its position from crumbling.

private const int _minMovesRemaining = 8;
private const int _piecesMovesPer128 = 160;
private const int _materialAdvantageMovesPer1024 = 25;
private const int _moveTimeHardLimitPer128 = 512;
private const int _adjustMoveTimeMinDepth = 9;
private const int _adjustMoveTimeMinScoreDecrease = 33;
private const int _adjustMoveTimePer128 = 32;
private const int _haveTimeSearchNextPlyPer128 = 70;
private ScoredMove[] _bestMoves; // ScoredMove is a struct the binds together ulong Move and int Score.
// _originalHorizon is the search depth, also known as ply.
// ...
public ulong FindBestMove(Board Board)
{
// ...
// Iteratively deepen search.
_originalHorizon = 0;
var bestMove = new ScoredMove(Move.Null, -StaticScore.Max);
do
{
_originalHorizon++;
_selectiveHorizon = 0;
// Clear principal variations and age move history.
// The Dictionary enumerator allocates memory which is not desirable when searching positions.
// However, this occurs only once per ply.
using (var pvEnumerator = _principalVariations.GetEnumerator())
{
while (pvEnumerator.MoveNext()) pvEnumerator.Current.Value[0] = Move.Null;
}
_moveHistory.Age(Board.CurrentPosition.WhiteMove);
// Get score within aspiration window.
var score = GetScoreWithinAspirationWindow(Board, principalVariations);
if (Math.Abs(score) == StaticScore.Interrupted) break; // Stop searching.
// Find best moves.
SortMovesByScore(_rootMoves, Board.CurrentPosition.MoveIndex - 1);
var bestMoves = _scoreError == 0 ? principalVariations : legalMoveIndex;
for (var moveIndex = 0; moveIndex < bestMoves; moveIndex++)
{
_bestMoves[moveIndex] = _rootMoves[moveIndex];
}
if (PvInfoUpdate) UpdateInfo(Board, principalVariations, true);
bestMove = _bestMoves[0];
_bestMovePlies[_originalHorizon] = bestMove;
if (MateInMoves.HasValue && (Math.Abs(bestMove.Score) >= StaticScore.Checkmate) &&
(Evaluation.GetMateDistance(bestMove.Score) <= MateInMoves.Value))
{
// Found checkmate in correct number of moves.
break;
}
AdjustMoveTime();
if (!HaveTimeForNextHorizon()) break; // Do not have time to search next ply.
} while (Continue && (_originalHorizon < HorizonLimit));
_stopwatch.Stop();
if (_debug()) _writeMessageLine(
$"info string Stopping search at {_stopwatch.Elapsed.TotalMilliseconds:0} milliseconds.");
return _scoreError == 0 ? bestMove.Move : GetInferiorMove(Board.CurrentPosition);
}
private void AdjustMoveTime()
{
if (!CanAdjustMoveTime || (_originalHorizon < _adjustMoveTimeMinDepth) ||
(MoveTimeSoftLimit == MoveTimeHardLimit)) return;
if (_bestMovePlies[_originalHorizon].Score >= (_bestMovePlies[_originalHorizon - 1].Score -
_adjustMoveTimeMinScoreDecrease)) return;
// Score has decreased significantly from last ply.
if (_debug()) _writeMessageLine(
"Adjusting move time because score has decreased significantly from previous ply.");
MoveTimeSoftLimit += TimeSpan.FromMilliseconds((MoveTimeSoftLimit.TotalMilliseconds *
_adjustMoveTimePer128) / 128);
if (MoveTimeSoftLimit > MoveTimeHardLimit) MoveTimeSoftLimit = MoveTimeHardLimit;
}
private bool HaveTimeForNextHorizon()
{
if (MoveTimeSoftLimit == TimeSpan.MaxValue) return true;
var moveTimePer128 = (int)((128 * _stopwatch.Elapsed.TotalMilliseconds) /
MoveTimeSoftLimit.TotalMilliseconds);
return moveTimePer128 <= _haveTimeSearchNextPlyPer128;
}

In MadChess 3.0 Beta, when it’s the engine’s turn to move (in a timed game), it allots time to search for a best move. It calculates MoveTimeSoftLimit by examining the game clock and the current position. It then calculates MoveTimeHardLimit simply by multiplying MoveTimeSoftLimit by four. The soft time limit is examined each ply. If the engine already has used 70 / 128ths of the allotted time (55%) or more, it replies with the best move found so far rather than begin searching the next ply (because it anticipates not having enough time to complete the search). Once MadChess begins searching a ply, it will not interrupt the search unless it’s reached or exceeded the MoveTimeHardLimit.

In addition, I improved code that calculates move time limits when playing a game with a traditional clock (non Fischer clock). Previously, MadChess 3.0 Beta mismanaged time in non Fischer clock games and occasionally lost on time. Now its playing strength in non Fischer clock games is on par with its strength in Fischer clock games. Usually I test MadChess in tournaments that use a Fischer clock (my computer opponent rating lists all use a Fischer clock), so playing games at 20 moves / 1 min (repeating) was special testing I did for this feature- but don’t intend to continue- to eliminate an engine deficiency. Considering “taste and comfort are personal,” as my father says, I recognize other people may prefer games with a traditional clock. So I’ve ensured MadChess plays well in those time controls.

I did not test sudden death time controls. Why? Because time scrambles don’t produce quality chess by humans or computer chess engines. In my opinion, MadChess’ performance in sudden death time control is not worth testing.

This “panic time” code increased the playing strength of MadChess 3.0 Beta by 8 Elo.

 

Feature Category Date Commit1 WAC2 Elo Rating3 Improvement
Time Management Search 2020 Dec 19 d143bb5 286 2521 +8
Crash Bug Search 2020 Aug 29 2d855ec 288 2513 +0
King Safety Evaluation 2020 Aug 16 6794c89 288 2513 +63
Eval Param Tuning Evaluation 2020 Jul 23 bef88d5 283 2450 +30
Late Move Pruning Search 2020 Feb 08 6f3d17a 288 2420 +29
Piece Mobility Evaluation 2020 Feb 01 5c5d4fc 282 2391 +62
Passed Pawns Evaluation 2018 Dec 27 103 279 2329 +119
Staged Move Generation Search 2018 Dec 15 93 275 2210 +39
History Heuristics Search 2018 Dec 03 84 275 2171 +28
Eval Param Tuning Evaluation 2018 Nov 24 75 272 2143 +47
Sophisticated Search
Material and Piece Location
Baseline 2018 Nov 08 58 269 2096 0
  1. GitHub commit (hash) or Subversion source code revision (integer)
  2. Win At Chess position test, 3 seconds per position
  3. Bullet chess, 2 min / game + 1 sec / move