MadChess 2.1 Released

I have released version 2.1 of my chess engine. I built this version of MadChess using .NET Core, Microsoft’s new cross-OS development platform. I have provided source code and binaries for Windows on the Downloads page.

I do not have access to a Linux or Mac machine.  If you are feeling adventurous and would like to build Linux or Mac binaries, please refer to Getting Started with .NET Core on Windows / Linux / MacOS for instructions on how to build .NET Core applications.

I have configured the MadChess project to build a self-contained application. All binaries required to run MadChess are included in the .zip file. Unlike previous versions of MadChess, no prerequisite .NET framework must be installed prior to running the engine.

MadChess 2.1 is about 60 or 70 ELO stronger than version 2.0.  It includes the following improvements, some impacting playing strength, some more focused on code quality.

  • Converted cached position from a class to a ulong primitive with bit-wise operations.
  • Tuned evaluation using the technique described by Peter Österlund.
  • Include static exchange score when determining move futility.
  • Update history heuristic value of previously played quiet moves that failed to produce a cutoff.
  • Extract principal variation from a triangular array (rather than from hash table).
  • Simplified time management.

MadChess 2.0 Released

I have released version 2 of my chess engine.  I’ve rewritten the engine from scratch in a more procedural style code- as opposed to the sub-optimal (for chess engines at least) object-oriented style I used in MadChess 1.x.

The engine is about 115 – 160 ELO stronger than its predecessor, depending on time control. I estimate it is about 2350 ELO.

Source code and EXE available on the Downloads page.

MadChess 2.0 Beta Build 127 (Limit Strength)

I have added parameters to limit the playing strength of MadChess 2.0.

WriteMessageLine("option name Pawn type spin default " + Evaluation.DefaultPawnMaterialScore +
  " min 1 max 1500");
WriteMessageLine("option name Knight type spin default " + Evaluation.DefaultKnightMaterialScore +
  " min 1 max 1500");
WriteMessageLine("option name Bishop type spin default " + Evaluation.DefaultBishopMaterialScore +
  " min 1 max 1500");
WriteMessageLine("option name Rook type spin default " + Evaluation.DefaultRookMaterialScore +
  " min 1 max 1500");
WriteMessageLine("option name Queen type spin default " + Evaluation.DefaultQueenMaterialScore +
  " min 1 max 1500");
WriteMessageLine("option name PieceLocation type check default true");
WriteMessageLine("option name PassedPawns type check default true");
WriteMessageLine("option name Mobility type check default true");
WriteMessageLine("option name KingSafety type check default true");
WriteMessageLine("option name BishopPair type check default true");
WriteMessageLine("option name Outposts type check default true");
WriteMessageLine("option name Trades type check default true");
WriteMessageLine("option name 7thRank type check default true");
WriteMessageLine("option name NPS type spin default 0 min 0 max 1000000");
WriteMessageLine("option name MoveError type spin default 0 min 0 max 1000");
WriteMessageLine("option name BlunderError type spin default 0 min 0 max 1000");
WriteMessageLine("option name BlunderPercent type spin default 0 min 0 max 100");
WriteMessageLine("option name UCI_LimitStrength type check default false");
WriteMessageLine("option name LimitStrength type check default false");
WriteMessageLine("option name UCI_Elo type spin default " + Search.MinElo + " min " +
  Search.MinElo + " max " + Search.MaxElo);
WriteMessageLine("option name Elo type spin default " + Search.MinElo + " min " +
  Search.MinElo + " max " + Search.MaxElo);

The simplest way to limit the playing strength of MadChess 2.0 is to set the LimitStrength and Elo parameters.  The engine will automatically configure material scores and positional knowledge.

// Evaluation
public
 void ConfigureStrength(int Elo)
{
    // Set default parameters.
    SetDefaultParameters();
    // Limit material and positional understanding.
    if (Elo < 800)
    {
        // Beginner
        // Overvalue queen and undervalue rook.
        RookMaterialScore = 300;
        QueenMaterialScore = 1200;
    }
    if (Elo < 1000)
    {
        // Novice
        // Value knight and bishop equally.
        KnightMaterialScore = 300;
        BishopMaterialScore = 300;
        // Misplace pieces.
        UnderstandsPieceLocation = false;
    }
    if (Elo < 1200)
    {
        // Social
        UnderstandsPassedPawns = false;
    }
    if (Elo < 1400)
    {
        // Strong Social
        UnderstandsMobility = false;
    }
    if (Elo < 1600)
    {
        // Club
        UnderstandsKingSafety = false;
    }
    if (Elo < 1800)
    {
        // Strong Club
        UnderstandsBishopPair = false;
        UnderstandsOutposts = false;
    }
    if (Elo < 2000)
    {
        // Expert
        Understands7thRank = false;
        UnderstandsTrades = false;
    }
    if (_uciStream.Debug)
    {
        _uciStream.WriteMessageLine("info string PawnMaterialScore = " + PawnMaterialScore);
        _uciStream.WriteMessageLine("info string KnightMaterialScore = " + KnightMaterialScore);
        _uciStream.WriteMessageLine("info string BishopMaterialScore = " + BishopMaterialScore);
        _uciStream.WriteMessageLine("info string RookMaterialScore = " + RookMaterialScore);
        _uciStream.WriteMessageLine("info string QueenMaterialScore = " + QueenMaterialScore);
        _uciStream.WriteMessageLine("info string UnderstandsPieceLocation = " + UnderstandsPieceLocation);
        _uciStream.WriteMessageLine("info string UnderstandsPassedPawns = " + UnderstandsPassedPawns);
        _uciStream.WriteMessageLine("info string UnderstandsMobility = " + UnderstandsMobility);
        _uciStream.WriteMessageLine("info string UnderstandsKingSafety = " + UnderstandsKingSafety);
        _uciStream.WriteMessageLine("info string UnderstandsBishopPair = " + UnderstandsBishopPair);
        _uciStream.WriteMessageLine("info string UnderstandsOutposts = " + UnderstandsOutposts);
        _uciStream.WriteMessageLine("info string Understands7thRank = " + Understands7thRank);
        _uciStream.WriteMessageLine("info string UnderstandsTrades = " + UnderstandsTrades);
    }
}

In addition, the engine will configure search speed (NPS- Nodes Per Second), move error, blunder error, and blunder percent based on the Elo value.

// Search
private void ConfigureStrength()
{
    // Set default parameters.
    SetDefaultParameters();

    // Limit search speed.
    // Rating               400  600  800  1000  1200  1400   1600    1800    2000    2200
    // Nodes Per Second     100  100  105   231  1410  7912  33692  115396  335644  861034

    double scale = 0.02d;
    int power = 8;
    int constant = 100;
    double ratingClass = (double)(_elo - MinElo) / 200;
    NodesPerSecond = GetNonLinearBonus(ratingClass, scale, power, constant);

    // Allow errors on every move.
    // Rating      400  600  800 1000 1200 1400 1600 1800 2000 2200
    // Move Error  162  128   98   72   50   32   18    8    2    0

    scale = 2d;
    power = 2;
    constant = 0;
    ratingClass = (double)(MaxElo - _elo) / 200;
    MoveError = GetNonLinearBonus(ratingClass, scale, power, constant);

    // Allow occassional blunders.
    // Rating         400  600  800  1000  1200  1400  1600  1800  2000  2200
    // Blunder Error  511  409  319   241   175   121    79    49    31    25
    // Blunder Pct     25   21   17    14    11     9     7     6     5     5

    scale = 6d;
    power = 2;
    constant = 25;
    BlunderError = GetNonLinearBonus(ratingClass, scale, power, constant);
    scale = 0.25d;
    power = 2;
    constant = 5;
    BlunderPercent = GetNonLinearBonus(ratingClass, scale, power, constant);

    if (_uciStream.Debug)
    {
        _uciStream.WriteMessageLine("info string NPS = " + NodesPerSecond + " MoveError = " + MoveError +
          " BlunderError = " + BlunderError + " BlunderPercent = " + BlunderPercent + ".");
    }
}


private static int GetNonLinearBonus(double RatingClass, double Scale, int Power, int Constant)
{
    return (int) (Scale * Math.Pow(RatingClass, Power)) + Constant;
}

MadChess 2.0 Beta Build 123 (Rook And Queen On 7th Rank)

I added rook and queen on 7th rank evaluation to MadChess 2.0.  The code awards a bonus for a rook or queen on the 7th rank if the enemy king is on the 8th rank (its 1st rank).

Also, I removed late move pruning.  (When I implemented late move pruning in build 44 it added 39 ELO to the playing strength of MadChess 2.0.)  I replaced it with more aggressive null and late move reductions, and more aggressive futility pruning, but found it decreased the playing strength.  Next, I restored late move pruning and left in the aggressive reductions.  I found the strength increased, but not quite back to where it had been.  I discovered a non-linear relationship between these search techniques.  The combined strength of null move reductions, LMR, LMP, and futility pruning is not additive.  I experimented with various parameter configurations before deciding to abandon LMP.  It took 300 hours of testing to confirm the revised search was equivalently strong to the search with LMP.

private const int _mgRookOn7thRank = 20;
private const int _egRookOn7thRank = 40;
private const int _mgQueenOn7thRank = 10;
private const int _egQueenOn7thRank = 20;
private const int _nullMoveReduction = 3;
private const int _pvsMinToHorizon = 3;
private const int _quietSearchRecaptureDepth = 3;
futilityMargins = new[] {50, 100, 175, 275, 400, 550};
_lateMoveReductions = new[] {3, 7, 15};

private void EvaluateRooks(out int MgRooks, out int EgRooks)
{
    MgRooks = 0;
    EgRooks = 0;
    int whiteKingRank = _board.WhiteRanks[_board.CurrentPosition.WhiteKingSquare];
    int blackKingRank = _board.BlackRanks[_board.CurrentPosition.BlackKingSquare];
    if ((whiteKingRank != 1) && (blackKingRank != 1)) return;
    for (int rook = 0; rook < _rooks; rook++)
    {
        int square = _rookSquares[rook];
        byte piece = _board.CurrentPosition.Squares[square];
        if (piece == Piece.WhiteRook)
        {
            // White rook
            if ((_board.WhiteRanks[square] == 7) && (blackKingRank == 1))
            {
                // White rook is on 7th rank.  Black king is on 1st rank.
                MgRooks += _mgRookOn7thRank;
                EgRooks += _egRookOn7thRank;
            }
        }
        else
        {
            // Black rook
            if ((_board.BlackRanks[square] == 7) && (whiteKingRank == 1))
            {
                // Black rook is on 7th rank.  White king is on 1st rank.
                MgRooks -= _mgRookOn7thRank;
                EgRooks -= _egRookOn7thRank;
            }
        }
    }
}

private void EvaluateQueens(out int MgQueens, out int EgQueens)
{
    MgQueens = 0;
    EgQueens = 0;
    int whiteKingRank = _board.WhiteRanks[_board.CurrentPosition.WhiteKingSquare];
    int blackKingRank = _board.BlackRanks[_board.CurrentPosition.BlackKingSquare];
    if ((whiteKingRank != 1) && (blackKingRank != 1)) return;
    for (int queen = 0; queen < _queens; queen++)
    {
        int square = _queenSquares[queen];
        byte piece = _board.CurrentPosition.Squares[square];
        if (piece == Piece.WhiteQueen)
        {
            // White queen
            if ((_board.WhiteRanks[square] == 7) && (blackKingRank == 1))
            {
                // White queen is on 7th rank.  Black king is on 1st rank.
                MgQueens += _mgQueenOn7thRank;
                EgQueens += _egQueenOn7thRank;
            }
        }
        else
        {
            // Black queen
            if ((_board.BlackRanks[square] == 7) && (whiteKingRank == 1))
            {
                // Black queen is on 7th rank.  White king is on 1st rank.
                MgQueens -= _mgQueenOn7thRank;
                EgQueens -= _egQueenOn7thRank;
            }
        }
    }
}

This added 20 ELO to the playing strength of MadChess 2.0.

MadChess 2.0             2306.9 :   1600 (+835,=299,-466),  61.5 %

vs.                             :  games (   +,   =,   -),   (%) :    Diff,    SD, CFS (%)
Glass 1.6                       :    100 (  22,  24,  54),  34.0 :  -140.1,  15.0,    0.0
RomiChess P3L                   :    100 (  20,  20,  60),  30.0 :  -133.7,  16.2,    0.0
OliThink 5.3.2                  :    100 (  23,  20,  57),  33.0 :  -109.7,  15.5,    0.0
Myrddin 0.87                    :    100 (  31,  21,  48),  41.5 :   -75.0,  17.3,    0.0
Sungorus 1.4                    :    100 (  49,  15,  36),  56.5 :    -3.1,   8.2,   35.2
FireFly v2.6.0                  :    100 (  47,  31,  22),  62.5 :   +89.0,  10.0,  100.0
ZCT 0.3.2450                    :    100 (  42,  28,  30),  56.0 :   +94.7,  10.8,  100.0
Beowulf 2.4                     :    100 (  59,  19,  22),  68.5 :   +97.1,  13.0,  100.0
Jazz v444                       :    100 (  60,  17,  23),  68.5 :  +108.9,  10.0,  100.0
Brainless 1.0                   :    100 (  57,  17,  26),  65.5 :  +148.2,  17.8,  100.0
Wing 2.0                        :    100 (  61,  20,  19),  71.0 :  +177.5,  10.1,  100.0
BikJump v2.01                   :    100 (  61,  22,  17),  72.0 :  +198.9,  10.7,  100.0
Matheus-2.3                     :    100 (  66,  19,  15),  75.5 :  +220.5,  10.7,  100.0
Monarch 1.7                     :    100 (  72,  12,  16),  78.0 :  +245.5,  11.0,  100.0
BigLion 2.23w                   :    100 (  82,   7,  11),  85.5 :  +277.1,   9.4,  100.0
Sharper 0.17                    :    100 (  83,   7,  10),  86.5 :  +289.8,  10.8,  100.0

Feature Category Date Build Number WAC1 ELO Rating2 Improvement
Rook And Queen On 7th Rank Evaluation 2015 Aug 20 123 279 2307 +20
Knight Outposts Evaluation 2015 May 27 83 276 2287 +25
Tune Reductions And Pruning Search 2015 Apr 26 74 275 2262 +20
Bishop Pair Evaluation 2015 Apr 15 72 275 2242 +18
Free Passed Pawns Evaluation 2015 Mar 10 59 270 2224 +31
Unstoppable Pawns
Draws, Material Trades
Evaluation 2015 Jan 31 52 270 2193 +39
Late Move Pruning Search 2015 Jan 10 44 273 2154 +39
History Heuristic
Late Move Reductions
Search 2015 Jan 04 43 275 2115 +50
Killer Moves Search 2015 Jan 03 38 275 2065 +61
Futility Pruning Search 2014 Dec 29 37 256 2004 +54

Null Move
Quiescence Recaptures

Search 2014 Dec 28 35 242 1950 +46

King Safety

Evaluation 2014 Dec 24 32 225 1904 +27

Piece Mobility

Evaluation 2014 Dec 16 29 225 1877 +64

Draw By Repetition Bug

Evaluation 2014 Dec 10 27 225 1813 +47

Passed Pawns

Evaluation 2014 Dec 09 26 225 1766 +72

Time Management

Search 2014 Dec 08 25 231 1694 +24

Delay Move Generation
Aspiration Window Bug

Search 2014 Dec 02 23 231 1670 +44

MVV / LVA Move Order
Draw By Insufficient Material
Move List Overflow Bug

Search 2014 Dec 01 22 235 1626 +30

Tapered Evaluation
(MG and EG Piece Location)

Evaluation 2014 Nov 29 21 234 1596 +107
Alpha / Beta Negamax
Aspiration Windows
Quiescence, Hash
Material, Piece Squares
Baseline 2014 Nov 25 20 236 1489

   1  Win At Chess position test, 3 seconds per position
   2  Bullet chess, 2 min / game + 1 sec / move

MadChess 2.0 Beta Build 83 (Knight Outposts)

I added knight outpost evaluation to MadChess 2.0.  Knights on the 5th rank or beyond are awarded a bonus if they cannot be attacked by enemy pawns and are supported by their own pawn.

private const int _knightOutpost = 50;

private int EvaluateKnights()
{
    int knights = 0;
    for (int knight = 0; knight < _knights; knight++)
    {
        int square = _knightSquares[knight];
        byte piece = _board.CurrentPosition.Squares[square];
        int rank;
        int file;
        int enemyPawnLeftFile;
        int enemyPawnRightFile;
        if (piece == Piece.WhiteKnight)
        {
            // White knight
            rank = _board.WhiteRanks[square];
            if (rank < 5) continue;
            // White knight advanced to 5th rank or beyond.
            file = _board.Files[square];
            enemyPawnLeftFile = _blackLeastAdvancedPawns[file - 1];
            enemyPawnRightFile = _blackLeastAdvancedPawns[file + 1];
            if ((enemyPawnLeftFile > (8 - rank)) && (enemyPawnRightFile > (8 - rank)))
            {
                // White knight not attacked by pawns.
                int leftSquare = _board.SquareIndices[square][Direction.SouthWest];
                bool supportedByPawn = (leftSquare != Square.Illegal) &&
                  (_board.CurrentPosition.Squares[leftSquare] == Piece.WhitePawn);
                if (!supportedByPawn)
                {
                    int rightSquare = _board.SquareIndices[square][Direction.SouthEast];
                    supportedByPawn = (rightSquare != Square.Illegal) &&
                      (_board.CurrentPosition.Squares[rightSquare] == Piece.WhitePawn);
                }
                if (supportedByPawn)
                {
                    // White knight supported by own pawn.
                    knights += _knightOutpost;
                }
            }
        }
        else
        {
            // Black knight
            rank = _board.BlackRanks[square];
            if (rank < 5) continue;
            // Black knight advanced to 5th rank or beyond.
            file = _board.Files[square];
            enemyPawnLeftFile = _whiteLeastAdvancedPawns[file - 1];
            enemyPawnRightFile = _whiteLeastAdvancedPawns[file + 1];
            if ((enemyPawnLeftFile > (8 - rank)) && (enemyPawnRightFile > (8 - rank)))
            {
                // Black knight not attacked by pawns.
                int leftSquare = _board.SquareIndices[square][Direction.NorthWest];
                bool supportedByPawn = (leftSquare != Square.Illegal) &&
                  (_board.CurrentPosition.Squares[leftSquare] == Piece.BlackPawn);
                    if (!supportedByPawn)
                {
                    int rightSquare = _board.SquareIndices[square][Direction.NorthEast];
                    supportedByPawn = (rightSquare != Square.Illegal) &&
                      (_board.CurrentPosition.Squares[rightSquare] == Piece.BlackPawn);
                }
                if (supportedByPawn)
                {
                    // Black knight supported by own pawn.
                    knights -= _knightOutpost;
                }
            }
        }
    }
    return knights;
}

This added 25 ELO to the playing strength of MadChess 2.0.

MadChess 2.0                   2287 :   1600 (+785,=327,-488),  59.3 %

vs.                                 :  games (   +,   =,   -),   (%) :   Diff,  SD, CFS (%)
Glass 1.6                           :    100 (  14,  15,  71),  21.5 :   -187,  10,    0.0
RomiChess P3L                       :    100 (  18,  28,  54),  32.0 :   -145,  14,    0.0
OliThink 5.3.2                      :    100 (  21,  17,  62),  29.5 :   -135,  13,    0.0
Myrddin 0.87                        :    100 (  22,  22,  56),  33.0 :   -112,  20,    0.0
Sungorus 1.4                        :    100 (  41,  26,  33),  54.0 :    -23,   6,    0.0
Beowulf 2.4                         :    100 (  47,  26,  27),  60.0 :    +70,   8,  100.0
FireFly v2.6.0                      :    100 (  52,  21,  27),  62.5 :    +74,  11,  100.0
ZCT 0.3.2450                        :    100 (  52,  23,  25),  63.5 :    +84,   9,  100.0
Jazz v444                           :    100 (  52,  26,  22),  65.0 :    +90,   6,  100.0
Wing 2.0                            :    100 (  54,  25,  21),  66.5 :   +157,   7,  100.0
Brainless 1.0                       :    100 (  63,  17,  20),  71.5 :   +159,  25,  100.0
BikJump v2.01                       :    100 (  60,  23,  17),  71.5 :   +182,   7,  100.0
Matheus-2.3                         :    100 (  68,  21,  11),  78.5 :   +208,  13,  100.0
Monarch 1.7                         :    100 (  71,  10,  19),  76.0 :   +230,  11,  100.0
BigLion 2.23w                       :    100 (  70,  17,  13),  78.5 :   +255,   7,  100.0
Sharper 0.17                        :    100 (  80,  10,  10),  85.0 :   +272,   7,  100.0

Feature Category Date Build Number WAC1 ELO Rating2 Improvement
Knight Outposts Evaluation 2015 May 27 83 276 2287 +25
Tune Reductions And Pruning Search 2015 Apr 26 74 275 2262 +20
Bishop Pair Evaluation 2015 Apr 15 72 275 2242 +18
Free Passed Pawns Evaluation 2015 Mar 10 59 270 2224 +31
Unstoppable Pawns
Draws, Material Trades
Evaluation 2015 Jan 31 52 270 2193 +39
Late Move Pruning Search 2015 Jan 10 44 273 2154 +39
History Heuristic
Late Move Reductions
Search 2015 Jan 04 43 275 2115 +50
Killer Moves Search 2015 Jan 03 38 275 2065 +61
Futility Pruning Search 2014 Dec 29 37 256 2004 +54

Null Move
Quiescence Recaptures

Search 2014 Dec 28 35 242 1950 +46

King Safety

Evaluation 2014 Dec 24 32 225 1904 +27

Piece Mobility

Evaluation 2014 Dec 16 29 225 1877 +64

Draw By Repetition Bug

Evaluation 2014 Dec 10 27 225 1813 +47

Passed Pawns

Evaluation 2014 Dec 09 26 225 1766 +72

Time Management

Search 2014 Dec 08 25 231 1694 +24

Delay Move Generation
Aspiration Window Bug

Search 2014 Dec 02 23 231 1670 +44

MVV / LVA Move Order
Draw By Insufficient Material
Move List Overflow Bug

Search 2014 Dec 01 22 235 1626 +30

Tapered Evaluation
(MG and EG Piece Location)

Evaluation 2014 Nov 29 21 234 1596 +107
Alpha / Beta Negamax
Aspiration Windows
Quiescence, Hash
Material, Piece Squares
Baseline 2014 Nov 25 20 236 1489

   1  Win At Chess position test, 3 seconds per position
   2  Bullet chess, 2 min / game + 1 sec / move