StockChess






4.91/5 (32 votes)
A WPF chess application that uses the Stockfish chess engine
Introduction
StockChess combines the awesome features of WPF with the exceptional chess analysis of Stockfish – the strongest open-source chess engine in the world. The app is quite basic, with the user matching his/her wits against the chess engine as either black or white.
Background
I had two goals in mind when developing this application: the first was to develop an app where the user can play against a chess engine; the second was to create a WPF chess application that followed the MVVM pattern. For the chess engine, I decided to use Stockfish, which is why the app is named StockChess. Specifically the app makes use of Stockfish 8, the most recent release of the engine as at the time of writing.
Requirements
To run the attached project, you require VS2015 or higher and to better understand this article, knowledge of chess notation will be useful.
Interacting with the Chess Engine
To enable the user to play against Stockfish, the application has to communicate with the chess engine. This is done using UCI commands. What are UCI commands you ask? These are text commands that are used to interact with an engine using the UCI (Universal Chess Interface) protocol. The following table contains examples of commands a GUI can send to the engine,
Command | Purpose |
uci | This is the first command sent to the engine telling it to switch to UCI mode. |
isready | Synchronizes the engine with the GUI. |
ucinewgame | Tells the engine that the search command that follows this command will be from a new game. |
position startpos moves e2e4 e7e5 | Tells the engine to set up its internal chess board and play the moves e4 e5. |
go movetime 5000 | Tells the engine to start analysing the position, set up with the position command, in 5 sec. |
StockChess launches and communicates with Stockfish by making use of the Process
class which is in the Systems.Diagnostics
namespace. This is done by the StockfishService
class:
using System;
using System.IO;
using System.Reactive.Linq;
using System.Diagnostics;
using StockChessCS.Helpers;
using StockChessCS.Interfaces;
namespace StockChessCS.Services
{
public class StockfishService : IEngineService
{
private StreamReader strmReader;
private StreamWriter strmWriter;
private Process engineProcess;
private IDisposable engineListener;
public event Action<string> EngineMessage;
public void SendCommand(string command)
{
if (strmWriter != null && command != UciCommands.uci)
{
strmWriter.WriteLine(command);
}
}
public void StopEngine()
{
if (engineProcess != null & !engineProcess.HasExited)
{
engineListener.Dispose();
strmReader.Close();
strmWriter.Close();
}
}
public void StartEngine()
{
FileInfo engine = new FileInfo(Path.Combine
(Environment.CurrentDirectory, "stockfish_8_x64.exe"));
if (engine.Exists && engine.Extension == ".exe")
{
engineProcess = new Process();
engineProcess.StartInfo.FileName = engine.FullName;
engineProcess.StartInfo.UseShellExecute = false;
engineProcess.StartInfo.RedirectStandardInput = true;
engineProcess.StartInfo.RedirectStandardOutput = true;
engineProcess.StartInfo.RedirectStandardError = true;
engineProcess.StartInfo.CreateNoWindow = true;
engineProcess.Start();
strmWriter = engineProcess.StandardInput;
strmReader = engineProcess.StandardOutput;
engineListener = Observable.Timer(TimeSpan.Zero,
TimeSpan.FromMilliseconds(1)).Subscribe(s => ReadEngineMessages());
strmWriter.WriteLine(UciCommands.uci);
strmWriter.WriteLine(UciCommands.isready);
strmWriter.WriteLine(UciCommands.ucinewgame);
}
else
{
throw new FileNotFoundException();
}
}
private void ReadEngineMessages()
{
var message = strmReader.ReadLine();
if (message != string.Empty)
{
EngineMessage?.Invoke(message);
}
}
}
}
Imports System.IO
Imports System.Reactive.Linq
Public Class StockfishService
Implements IEngineService
Private strmReader As StreamReader
Private strmWriter As StreamWriter
Private engineProcess As Process
Private engineListener As IDisposable
Public Event EngineMessage(message As String) Implements IEngineService.EngineMessage
Public Sub SendCommand(command As String) Implements IEngineService.SendCommand
If strmWriter IsNot Nothing AndAlso command <> UciCommands.uci Then
strmWriter.WriteLine(command)
End If
End Sub
Public Sub StopEngine() Implements IEngineService.StopEngine
If engineProcess IsNot Nothing And Not engineProcess.HasExited Then
engineListener.Dispose()
strmReader.Close()
strmWriter.Close()
End If
End Sub
Public Sub StartEngine() Implements IEngineService.StartEngine
Dim engine As New FileInfo(Path.Combine_
(Environment.CurrentDirectory, "stockfish_8_x64.exe"))
If engine.Exists AndAlso engine.Extension = ".exe" Then
engineProcess = New Process
engineProcess.StartInfo.FileName = engine.FullName
engineProcess.StartInfo.UseShellExecute = False
engineProcess.StartInfo.RedirectStandardInput = True
engineProcess.StartInfo.RedirectStandardOutput = True
engineProcess.StartInfo.RedirectStandardError = True
engineProcess.StartInfo.CreateNoWindow = True
engineProcess.Start()
strmWriter = engineProcess.StandardInput
strmReader = engineProcess.StandardOutput
engineListener = Observable.Timer(TimeSpan.Zero, _
TimeSpan.FromMilliseconds(1)).Subscribe(Sub() ReadEngineMessages())
strmWriter.WriteLine(UciCommands.uci)
strmWriter.WriteLine(UciCommands.isready)
strmWriter.WriteLine(UciCommands.ucinewgame)
Else
Throw New FileNotFoundException
End If
End Sub
Private Sub ReadEngineMessages()
Dim message = strmReader.ReadLine()
If message <> String.Empty Then
RaiseEvent EngineMessage(message)
End If
End Sub
End Class
Notice I'm starting the chess engine in the StartEngine
method and making use of Rxs Observable.Timer
to check, in one millisecond intervals, for any messages it sends. When the engine sends a message, the EngineMessage
event is raised. The chess engine is included in the project.
Pieces & Squares
A chess board has 64 squares and in its initial setup, has 32 pieces. These are represented in the project by objects of type IBoardItem
.
public interface IBoardItem
{
int Rank { get; set; }
char File { get; set; }
ChessBoardItem ItemType { get; set; }
}
Public Interface IBoardItem
Property Rank As Integer
Property File As Char
Property ItemType As ChessBoardItem
End Interface
ChessBoardItem
is an enumeration with two values; Piece
and Square
.
public enum ChessBoardItem
{
Piece,
Square
}
Public Enum ChessBoardItem
Piece
Square
End Enum
The two classes that implement the IBoardItem
interface are the BoardSquare
class:
public class BoardSquare : IBoardItem
{
public int Rank { get; set; }
public char File { get; set; }
public ChessBoardItem ItemType { get; set; }
}
Public Class BoardSquare
Implements IBoardItem
Public Property Rank As Integer Implements IBoardItem.Rank
Public Property File As Char Implements IBoardItem.File
Public Property ItemType As ChessBoardItem Implements IBoardItem.ItemType
End Class
and the ChessPiece
class:
public class ChessPiece : ViewModelBase, IBoardItem
{
private PieceColor _color;
public PieceColor Color
{
get { return _color; }
set
{
_color = value;
OnPropertyChanged();
}
}
private PieceType _piece;
public PieceType Piece
{
get { return _piece; }
set
{
_piece = value;
OnPropertyChanged();
}
}
private int _rank;
public int Rank
{
get { return _rank; }
set
{
_rank = value;
OnPropertyChanged();
}
}
private char _file;
public char File
{
get { return _file; }
set
{
_file = value;
OnPropertyChanged();
}
}
public ChessBoardItem ItemType { get; set; }
}
Public Class ChessPiece
Inherits ViewModelBase
Implements IBoardItem
Private _color As PieceColor
Public Property Color As PieceColor
Get
Return _color
End Get
Set(value As PieceColor)
_color = value
OnPropertyChanged()
End Set
End Property
Private _piece As PieceType
Public Property Piece As PieceType
Get
Return _piece
End Get
Set(value As PieceType)
_piece = value
OnPropertyChanged()
End Set
End Property
Private _rank As Integer
Public Property Rank As Integer Implements IBoardItem.Rank
Get
Return _rank
End Get
Set(value As Integer)
_rank = value
OnPropertyChanged()
End Set
End Property
Private _file As Char
Public Property File As Char Implements IBoardItem.File
Get
Return _file
End Get
Set(value As Char)
_file = value
OnPropertyChanged()
End Set
End Property
Public Property ItemType As ChessBoardItem Implements IBoardItem.ItemType
End Class
These two classes serve as the models for the project.
The static
Chess
class defines a function that returns an collection of IBoardItem
, which represents the initial setup of a chess
board, and several methods used to move chess pieces.
using System.Collections.ObjectModel;
using System.Linq;
using StockChessCS.Enums;
using StockChessCS.Interfaces;
using StockChessCS.Models;
namespace StockChessCS.Helpers
{
public static class Chess
{
public static ObservableCollection<IBoardItem> BoardSetup()
{
ObservableCollection<IBoardItem> items = new ObservableCollection<IBoardItem>();
var files = ("abcdefgh").ToArray();
// Board squares
foreach (var fl in files)
{
for (int rank = 1; rank <= 8; rank++)
{
items.Add(new BoardSquare { Rank = rank, File = fl,
ItemType = ChessBoardItem.Square });
}
}
// Pawns
foreach (var fl in files)
{
items.Add(new ChessPiece { ItemType = ChessBoardItem.Piece,
Color = PieceColor.Black,
Piece = PieceType.Pawn, Rank = 7, File = fl });
items.Add(new ChessPiece { ItemType = ChessBoardItem.Piece,
Color = PieceColor.White,
Piece = PieceType.Pawn, Rank = 2, File = fl });
}
// Black pieces
items.Add(new ChessPiece { ItemType = ChessBoardItem.Piece,
Color = PieceColor.Black,
Piece = PieceType.Rook, Rank = 8, File = 'a' });
items.Add(new ChessPiece { ItemType = ChessBoardItem.Piece,
Color = PieceColor.Black,
Piece = PieceType.Knight, Rank = 8, File = 'b' });
items.Add(new ChessPiece { ItemType = ChessBoardItem.Piece,
Color = PieceColor.Black,
Piece = PieceType.Bishop, Rank = 8, File = 'c' });
items.Add(new ChessPiece { ItemType = ChessBoardItem.Piece,
Color = PieceColor.Black,
Piece = PieceType.Queen, Rank = 8, File = 'd' });
items.Add(new ChessPiece { ItemType = ChessBoardItem.Piece,
Color = PieceColor.Black,
Piece = PieceType.King, Rank = 8, File = 'e' });
items.Add(new ChessPiece { ItemType = ChessBoardItem.Piece,
Color = PieceColor.Black,
Piece = PieceType.Bishop, Rank = 8, File = 'f' });
items.Add(new ChessPiece { ItemType = ChessBoardItem.Piece,
Color = PieceColor.Black,
Piece = PieceType.Knight, Rank = 8, File = 'g' });
items.Add(new ChessPiece { ItemType = ChessBoardItem.Piece,
Color = PieceColor.Black,
Piece = PieceType.Rook, Rank = 8, File = 'h' });
// White pieces
items.Add(new ChessPiece { ItemType = ChessBoardItem.Piece,
Color = PieceColor.White,
Piece = PieceType.Rook, Rank = 1, File = 'a' });
items.Add(new ChessPiece { ItemType = ChessBoardItem.Piece,
Color = PieceColor.White,
Piece = PieceType.Knight, Rank = 1, File = 'b' });
items.Add(new ChessPiece { ItemType = ChessBoardItem.Piece,
Color = PieceColor.White,
Piece = PieceType.Bishop, Rank = 1, File = 'c' });
items.Add(new ChessPiece { ItemType = ChessBoardItem.Piece,
Color = PieceColor.White,
Piece = PieceType.Queen, Rank = 1, File = 'd' });
items.Add(new ChessPiece { ItemType = ChessBoardItem.Piece,
Color = PieceColor.White,
Piece = PieceType.King, Rank = 1, File = 'e' });
items.Add(new ChessPiece { ItemType = ChessBoardItem.Piece,
Color = PieceColor.White,
Piece = PieceType.Bishop, Rank = 1, File = 'f' });
items.Add(new ChessPiece { ItemType = ChessBoardItem.Piece,
Color = PieceColor.White,
Piece = PieceType.Knight, Rank = 1, File = 'g' });
items.Add(new ChessPiece { ItemType = ChessBoardItem.Piece,
Color = PieceColor.White,
Piece = PieceType.Rook, Rank = 1, File = 'h' });
return items;
}
public static void MovePiece(ChessPiece selectedPiece, BoardSquare selectedSquare,
ObservableCollection<IBoardItem> items)
{
switch (selectedPiece.Piece)
{
case PieceType.King:
KingMove(selectedPiece, selectedSquare, items);
break;
case PieceType.Pawn:
PawnMove(selectedPiece, selectedSquare, items);
break;
default:
Move(selectedPiece, selectedSquare);
break;
}
}
private static void Move(ChessPiece piece, BoardSquare square)
{
piece.Rank = square.Rank;
piece.File = square.File;
}
private static void KingMove(ChessPiece piece,
BoardSquare targetSquare, ObservableCollection<IBoardItem> items)
{
if (piece.File == 'e' && targetSquare.File == 'g') // Short castle
{
var rook = items.OfType<ChessPiece>().Where(p => p.Color == piece.Color &&
p.Piece == PieceType.Rook && p.File == 'h').FirstOrDefault();
piece.File = 'g';
rook.File = 'f';
}
else if (piece.File == 'e' && targetSquare.File == 'c') // Long castle
{
var rook = items.OfType<ChessPiece>().Where(p => p.Color == piece.Color &&
p.Piece == PieceType.Rook && p.File == 'a').FirstOrDefault();
piece.File = 'c';
rook.File = 'd';
}
else { Move(piece, targetSquare); }
}
private static void PawnMove(ChessPiece piece,
BoardSquare targetSquare, ObservableCollection<IBoardItem> items)
{
// Promotion
switch (piece.Color)
{
case PieceColor.Black:
if (piece.Rank == 1) piece.Piece = PieceType.Queen;
break;
case PieceColor.White:
if (piece.Rank == 8) piece.Piece = PieceType.Queen;
break;
}
// En passant
if (piece.File != targetSquare.File)
{
var opponentPawn = items.OfType<ChessPiece>().Where
(p => p.Color != piece.Color &&
p.Piece == PieceType.Pawn && p.Rank == piece.Rank &&
p.File == targetSquare.File).FirstOrDefault();
items.Remove(opponentPawn);
}
Move(piece, targetSquare);
}
public static void CapturePiece(ChessPiece selectedPiece, ChessPiece otherPiece,
ObservableCollection<IBoardItem> items)
{
selectedPiece.Rank = otherPiece.Rank;
selectedPiece.File = otherPiece.File;
items.Remove(otherPiece);
}
}
}
Imports System.Collections.ObjectModel
Public NotInheritable Class Chess
Public Shared Function BoardSetup() As ObservableCollection(Of IBoardItem)
Dim items As New ObservableCollection(Of IBoardItem)
Dim files = ("abcdefgh").ToArray()
' Board squares
For Each fl In files
For rank As Integer = 1 To 8
items.Add(New BoardSquare With {.Rank = rank, .File = fl, _
.ItemType = ChessBoardItem.Square})
Next
Next
' Pawns
For Each fl In files
items.Add(New ChessPiece With {.ItemType = ChessBoardItem.Piece, _
.Color = PieceColor.Black,
.Piece = PieceType.Pawn, .Rank = 7, .File = fl})
items.Add(New ChessPiece With {.ItemType = ChessBoardItem.Piece, _
.Color = PieceColor.White,
.Piece = PieceType.Pawn, .Rank = 2, .File = fl})
' Black pieces
items.Add(New ChessPiece With {.ItemType = ChessBoardItem.Piece, _
.Color = PieceColor.Black,
.Piece = PieceType.Rook, .Rank = 8, .File = "a"})
items.Add(New ChessPiece With {.ItemType = ChessBoardItem.Piece, _
.Color = PieceColor.Black,
.Piece = PieceType.Knight, .Rank = 8, .File = "b"})
items.Add(New ChessPiece With {.ItemType = ChessBoardItem.Piece, _
.Color = PieceColor.Black,
.Piece = PieceType.Bishop, .Rank = 8, .File = "c"})
items.Add(New ChessPiece With {.ItemType = ChessBoardItem.Piece, _
.Color = PieceColor.Black,
.Piece = PieceType.Queen, .Rank = 8, .File = "d"})
items.Add(New ChessPiece With {.ItemType = ChessBoardItem.Piece, _
.Color = PieceColor.Black,
.Piece = PieceType.King, .Rank = 8, .File = "e"})
items.Add(New ChessPiece With {.ItemType = ChessBoardItem.Piece, _
.Color = PieceColor.Black,
.Piece = PieceType.Bishop, .Rank = 8, .File = "f"})
items.Add(New ChessPiece With {.ItemType = ChessBoardItem.Piece, _
.Color = PieceColor.Black,
.Piece = PieceType.Knight, .Rank = 8, .File = "g"})
items.Add(New ChessPiece With {.ItemType = ChessBoardItem.Piece, _
.Color = PieceColor.Black,
.Piece = PieceType.Rook, .Rank = 8, .File = "h"})
' White pieces
items.Add(New ChessPiece With {.ItemType = ChessBoardItem.Piece, _
.Color = PieceColor.White,
.Piece = PieceType.Rook, .Rank = 1, .File = "a"})
items.Add(New ChessPiece With {.ItemType = ChessBoardItem.Piece, _
.Color = PieceColor.White,
.Piece = PieceType.Knight, .Rank = 1, .File = "b"})
items.Add(New ChessPiece With {.ItemType = ChessBoardItem.Piece, _
.Color = PieceColor.White,
.Piece = PieceType.Bishop, .Rank = 1, .File = "c"})
items.Add(New ChessPiece With {.ItemType = ChessBoardItem.Piece, _
.Color = PieceColor.White,
.Piece = PieceType.Queen, .Rank = 1, .File = "d"})
items.Add(New ChessPiece With {.ItemType = ChessBoardItem.Piece, _
.Color = PieceColor.White,
.Piece = PieceType.King, .Rank = 1, .File = "e"})
items.Add(New ChessPiece With {.ItemType = ChessBoardItem.Piece, _
.Color = PieceColor.White,
.Piece = PieceType.Bishop, .Rank = 1, .File = "f"})
items.Add(New ChessPiece With {.ItemType = ChessBoardItem.Piece, _
.Color = PieceColor.White,
.Piece = PieceType.Knight, .Rank = 1, .File = "g"})
items.Add(New ChessPiece With {.ItemType = ChessBoardItem.Piece, _
.Color = PieceColor.White,
.Piece = PieceType.Rook, .Rank = 1, .File = "h"})
Return items
End Function
Public Shared Sub MovePiece(selectedPiece As ChessPiece, selectedSquare As BoardSquare,
items As ObservableCollection(Of IBoardItem))
Select Case selectedPiece.Piece
Case PieceType.King
KingMove(selectedPiece, selectedSquare, items)
Exit Select
Case PieceType.Pawn
PawnMove(selectedPiece, selectedSquare, items)
Exit Select
Case Else
Move(selectedPiece, selectedSquare)
Exit Select
End Select
End Sub
Private Shared Sub Move(piece As ChessPiece, square As BoardSquare)
piece.Rank = square.Rank
piece.File = square.File
End Sub
Private Shared Sub KingMove(piece As ChessPiece, targetSquare As BoardSquare, _
items As ObservableCollection(Of IBoardItem))
If piece.File = "e" And targetSquare.File = "g" Then ' Short castle
Dim rook = items.OfType(Of ChessPiece).Where(Function(p) _
p.Color = piece.Color AndAlso
p.Piece = PieceType.Rook AndAlso
p.File = "h").FirstOrDefault
piece.File = "g"
rook.File = "f"
ElseIf piece.File = "e" And targetSquare.File = "c" Then ' Long castle
Dim rook = items.OfType(Of ChessPiece).Where(Function(p) _
p.Color = piece.Color AndAlso
p.Piece = PieceType.Rook AndAlso
p.File = "a").FirstOrDefault
piece.File = "c"
rook.File = "d"
Else
Move(piece, targetSquare)
End If
End Sub
Private Shared Sub PawnMove(piece As ChessPiece, _
targetSquare As BoardSquare, items As ObservableCollection(Of IBoardItem))
' Promotion
Select Case piece.Color
Case PieceColor.Black
If targetSquare.Rank = 1 Then piece.Piece = PieceType.Queen
Exit Select
Case PieceColor.White
If targetSquare.Rank = 8 Then piece.Piece = PieceType.Queen
Exit Select
End Select
' En passant
If piece.File <> targetSquare.File Then
Dim opponentPawn = items.OfType(Of ChessPiece).Where(Function(p) _
p.Color <> piece.Color AndAlso
p.Piece = PieceType.Pawn AndAlso
p.Rank = piece.Rank AndAlso
p.File = targetSquare.File).FirstOrDefault
items.Remove(opponentPawn)
End If
Move(piece, targetSquare)
End Sub
Public Shared Sub CapturePiece(selectedPiece As ChessPiece, otherPiece As ChessPiece,
items As ObservableCollection(Of IBoardItem))
selectedPiece.Rank = otherPiece.Rank
selectedPiece.File = otherPiece.File
items.Remove(otherPiece)
End Sub
End Class
View Models
The project has only one view model, ChessViewModel
, containing the properties the project's view will bind to.
public class ChessViewModel : ViewModelBase
{
private IEngineService engine;
private StringBuilder moves = new StringBuilder();
private short deepAnalysisTime = 5000;
private short moveValidationTime = 1;
private TaskFactory ctxTaskFactory;
public ChessViewModel(IEngineService es)
{
engine = es;
BoardItems = Chess.BoardSetup();
ctxTaskFactory = new TaskFactory(TaskScheduler.FromCurrentSynchronizationContext());
engine.EngineMessage += EngineMessage;
engine.StartEngine();
}
private ObservableCollection<IBoardItem> _boardItems;
public ObservableCollection<IBoardItem> BoardItems
{
get { return _boardItems; }
set
{
_boardItems = value;
OnPropertyChanged();
}
}
private bool _isEngineThinking;
public bool IsEngineThinking
{
get { return _isEngineThinking; }
set
{
_isEngineThinking = value;
OnPropertyChanged();
}
}
private bool _checkMate;
public bool CheckMate
{
get { return _checkMate; }
set
{
_checkMate = value;
OnPropertyChanged();
}
}
private PieceColor _playerColor;
public PieceColor PlayerColor
{
get { return _playerColor; }
set
{
_playerColor = value;
OnPropertyChanged();
}
}
private bool playerWantsToMovePiece;
private bool playerWantsToCapturePiece;
private ChessPiece selectedPiece;
private BoardSquare targetSquare;
private ChessPiece targetPiece;
public IBoardItem SelectedBoardItem
{
set
{
if (value is ChessPiece)
{
var piece = (ChessPiece)value;
if (piece.Color == PlayerColor)
{
selectedPiece = piece;
}
else if (piece.Color != PlayerColor && selectedPiece != null)
{
playerWantsToCapturePiece = true;
targetPiece = piece;
ValidateMove(targetPiece);
}
}
else if (value is BoardSquare && selectedPiece != null)
{
playerWantsToMovePiece = true;
targetSquare = (BoardSquare)value;
ValidateMove(targetSquare);
}
}
}
private ICommand _newGameCommand;
public ICommand NewGameCommand
{
get
{
if (_newGameCommand == null) _newGameCommand = new RelayCommand(o => NewGame());
return _newGameCommand;
}
}
private void NewGame()
{
BoardItems = Chess.BoardSetup();
if (moves.Length > 0) moves.Clear();
if (CheckMate) CheckMate = false;
if (IsEngineThinking) IsEngineThinking = false;
ResetSomeMembers();
engine.SendCommand(UciCommands.ucinewgame);
if (PlayerColor == PieceColor.Black)
{
engine.SendCommand(UciCommands.position);
engine.SendCommand(UciCommands.go_movetime + " " + deepAnalysisTime.ToString());
IsEngineThinking = true;
}
}
private ICommand _stopEngineCommand;
public ICommand StopEngineCommand
{
get
{
if (_stopEngineCommand == null) _stopEngineCommand =
new RelayCommand(o => StopEngine());
return _stopEngineCommand;
}
}
private void StopEngine()
{
engine.EngineMessage -= EngineMessage;
engine.StopEngine();
}
private ICommand _changePlayerColorCommand;
public ICommand ChangePlayerColorCommand
{
get
{
if (_changePlayerColorCommand == null)
{
_changePlayerColorCommand = new RelayCommand<PieceColor>(ChangePlayerColor);
}
return _changePlayerColorCommand;
}
}
private void ChangePlayerColor(PieceColor color)
{
PlayerColor = color;
NewGame();
}
private void EngineMessage(string message)
{
if (message.Contains(UciCommands.bestmove)) // Message is in the form:
// bestmove <move> ponder <move>
{
if (!message.Contains("ponder")) CheckMate = true;
var move = message.Split(' ').ElementAt(1);
var position1 = move.Take(2);
var position2 = move.Skip(2);
var enginePiece = BoardItems.OfType<ChessPiece>().
Where(p => p.Rank == int.Parse(position1.ElementAt(1).ToString()) &
p.File == position1.ElementAt(0)).Single();
if (enginePiece.Color == PlayerColor) // Player made illegal move
{
RemoveLastMove();
ResetSomeMembers();
}
else
{
if (playerWantsToMovePiece)
{
ctxTaskFactory.StartNew(() =>
Chess.MovePiece(selectedPiece, targetSquare, BoardItems)).Wait();
DeeperMoveAnalysis();
}
else if (playerWantsToCapturePiece)
{
ctxTaskFactory.StartNew(() =>
Chess.CapturePiece(selectedPiece, targetPiece, BoardItems)).Wait();
DeeperMoveAnalysis();
}
else // Engine move
{
moves.Append(" " + move);
var pieceToCapture = BoardItems.OfType<ChessPiece>().
Where(p => p.Rank == int.Parse(position2.ElementAt(1).ToString()) &
p.File == position2.ElementAt(0)).SingleOrDefault();
if (pieceToCapture != null)
{
ctxTaskFactory.StartNew(() =>
Chess.CapturePiece(enginePiece, pieceToCapture, BoardItems)).Wait();
}
else
{
targetSquare = BoardItems.OfType<BoardSquare>().
Where(s => s.Rank == int.Parse(position2.ElementAt(1).ToString()) &
s.File == position2.ElementAt(0)).SingleOrDefault();
ctxTaskFactory.StartNew(() =>
Chess.MovePiece(enginePiece, targetSquare, BoardItems)).Wait();
}
IsEngineThinking = false;
}
}
}
}
private void ResetSomeMembers()
{
if (selectedPiece != null) selectedPiece = null;
if (playerWantsToCapturePiece) playerWantsToCapturePiece = false;
if (playerWantsToMovePiece) playerWantsToMovePiece = false;
}
private void DeeperMoveAnalysis()
{
SendMovesToEngine(deepAnalysisTime);
IsEngineThinking = true;
ResetSomeMembers();
}
private void RemoveLastMove()
{
if (moves.Length > 0)
{
var length = moves.Length;
var start = moves.Length - 5;
moves.Remove(start, 5);
}
}
private void ValidateMove(IBoardItem item)
{
var position1 = selectedPiece.File.ToString() + selectedPiece.Rank.ToString();
var position2 = item.File.ToString() + item.Rank.ToString();
var move = position1 + position2;
moves.Append(" " + move);
SendMovesToEngine(moveValidationTime);
}
private void SendMovesToEngine(short time)
{
var command = UciCommands.position + moves.ToString();
engine.SendCommand(command);
command = UciCommands.go_movetime + " " + time.ToString();
engine.SendCommand(command);
}
}
Public Class ChessViewModel
Inherits ViewModelBase
Private engine As IEngineService
Private moves As New StringBuilder
Private deepAnalysisTime As Short = 5000
Private moveValidationTime As Short = 1
Private ctxTaskFactory As TaskFactory
Public Sub New(es As IEngineService)
engine = es
BoardItems = Chess.BoardSetup()
ctxTaskFactory = New TaskFactory(TaskScheduler.FromCurrentSynchronizationContext)
AddHandler engine.EngineMessage, AddressOf EngineMessage
engine.StartEngine()
End Sub
Private _boardItems As ObservableCollection(Of IBoardItem)
Public Property BoardItems As ObservableCollection(Of IBoardItem)
Get
Return _boardItems
End Get
Set(value As ObservableCollection(Of IBoardItem))
_boardItems = value
OnPropertyChanged()
End Set
End Property
Private _isEngineThinking As Boolean
Public Property IsEngineThinking As Boolean
Get
Return _isEngineThinking
End Get
Set(value As Boolean)
_isEngineThinking = value
OnPropertyChanged()
End Set
End Property
Private _checkMate As Boolean
Public Property CheckMate As Boolean
Get
Return _checkMate
End Get
Set(value As Boolean)
_checkMate = value
OnPropertyChanged()
End Set
End Property
Private _playerColor As PieceColor
Public Property PlayerColor As PieceColor
Get
Return _playerColor
End Get
Set(value As PieceColor)
_playerColor = value
OnPropertyChanged()
End Set
End Property
Private playerWantsToMovePiece As Boolean
Private playerWantsToCapturePiece As Boolean
Private selectedPiece As ChessPiece
Private targetSquare As BoardSquare
Private targetPiece As ChessPiece
Public WriteOnly Property SelectedBoardItem As IBoardItem
Set(value As IBoardItem)
If TypeOf value Is ChessPiece Then
Dim piece = CType(value, ChessPiece)
If piece.Color = PlayerColor Then
selectedPiece = piece
ElseIf piece.Color <> PlayerColor AndAlso selectedPiece IsNot Nothing Then
playerWantsToCapturePiece = True
targetPiece = piece
ValidateMove(targetPiece)
End If
ElseIf TypeOf value Is BoardSquare AndAlso selectedPiece IsNot Nothing Then
playerWantsToMovePiece = True
targetSquare = CType(value, BoardSquare)
ValidateMove(targetSquare)
End If
End Set
End Property
Private _newGameCommand As ICommand
Public ReadOnly Property NewGameCommand As ICommand
Get
If _newGameCommand Is Nothing Then
_newGameCommand = New RelayCommand(AddressOf NewGame)
End If
Return _newGameCommand
End Get
End Property
Private Sub NewGame()
BoardItems = Chess.BoardSetup()
If moves.Length > 0 Then moves.Clear()
If CheckMate Then CheckMate = False
If IsEngineThinking Then IsEngineThinking = False
ResetSomeMembers()
engine.SendCommand(UciCommands.ucinewgame)
If PlayerColor = PieceColor.Black Then
engine.SendCommand(UciCommands.position)
engine.SendCommand(UciCommands.go_movetime & " " & deepAnalysisTime.ToString)
IsEngineThinking = True
End If
End Sub
Private _stopEngineCommand As ICommand
Public ReadOnly Property StopEngineCommand As ICommand
Get
If _stopEngineCommand Is Nothing Then
_stopEngineCommand = New RelayCommand(AddressOf StopEngine)
End If
Return _stopEngineCommand
End Get
End Property
Private Sub StopEngine()
RemoveHandler engine.EngineMessage, AddressOf EngineMessage
engine.StopEngine()
End Sub
Private _changePlayerColorCommand As ICommand
Public ReadOnly Property ChangePlayerColorCommand As ICommand
Get
If _changePlayerColorCommand Is Nothing Then
_changePlayerColorCommand = New RelayCommand(Of PieceColor)_
(AddressOf ChangePlayerColor)
End If
Return _changePlayerColorCommand
End Get
End Property
Private Sub ChangePlayerColor(color As PieceColor)
PlayerColor = color
NewGame()
End Sub
Private Sub EngineMessage(message As String)
If message.Contains(UciCommands.bestmove) Then ' Message is in the form:
' bestmove <move> ponder <move>
If Not message.Contains("ponder") Then CheckMate = True
Dim move = message.Split(" ").ElementAt(1)
Dim position1 = move.Take(2)
Dim position2 = move.Skip(2)
Dim enginePiece = BoardItems.OfType(Of ChessPiece).Where(Function(p) _
p.Rank = CInt(position1(1).ToString()) And
p.File = position1(0)).Single
If enginePiece.Color = PlayerColor Then ' Player made illegal move
RemoveLastMove()
ResetSomeMembers()
Else
If playerWantsToMovePiece Then
ctxTaskFactory.StartNew(Sub() Chess.MovePiece_
(selectedPiece, targetSquare, BoardItems)).Wait()
DeeperMoveAnalysis()
ElseIf playerWantsToCapturePiece Then
ctxTaskFactory.StartNew(Sub() Chess.CapturePiece_
(selectedPiece, targetPiece, BoardItems)).Wait()
DeeperMoveAnalysis()
Else ' Engine move
moves.Append(" " & move)
Dim pieceToCapture = BoardItems.OfType(Of ChessPiece).Where_
(Function(p) p.Rank = CInt(position2(1).ToString()) And
p.File = position2(0)).SingleOrDefault
If pieceToCapture IsNot Nothing Then
ctxTaskFactory.StartNew(Sub() Chess.CapturePiece_
(enginePiece, pieceToCapture, BoardItems)).Wait()
Else
targetSquare = BoardItems.OfType(Of BoardSquare).Where_
(Function(s) s.Rank = CInt(position2(1).ToString()) And
s.File = position2(0)).SingleOrDefault
ctxTaskFactory.StartNew(Sub() Chess.MovePiece_
(enginePiece, targetSquare, BoardItems)).Wait()
End If
IsEngineThinking = False
End If
End If
End If
End Sub
Private Sub ResetSomeMembers()
If selectedPiece IsNot Nothing Then selectedPiece = Nothing
If playerWantsToMovePiece Then playerWantsToMovePiece = False
If playerWantsToCapturePiece Then playerWantsToCapturePiece = False
End Sub
Private Sub DeeperMoveAnalysis()
SendMovesToEngine(deepAnalysisTime)
IsEngineThinking = True
ResetSomeMembers()
End Sub
Private Sub RemoveLastMove()
If moves.Length > 0 Then
Dim length = moves.Length
Dim start = moves.Length - 5
moves.Remove(start, 5)
End If
End Sub
Private Sub ValidateMove(item As IBoardItem)
Dim position1 = selectedPiece.File.ToString() & selectedPiece.Rank.ToString()
Dim position2 = item.File.ToString() & item.Rank.ToString()
Dim move = position1 & position2
moves.Append(" " & move)
SendMovesToEngine(moveValidationTime)
End Sub
Private Sub SendMovesToEngine(time As Short)
Dim command = UciCommands.position & moves.ToString
engine.SendCommand(command)
command = UciCommands.go_movetime & " " & time.ToString
engine.SendCommand(command)
End Sub
End Class
If you look at the SelectedBoardItem
property and the EngineMessage
method, you'll notice that I'm using the chess engine to check whether a valid move has been made. This is done by sending a position
and go
command to the engine with a movetime
of one millisecond. If the move is valid, I send another command to the engine to carry out a deeper analysis of the player's move.
Please note that when a go
command is sent to the engine, it responds with the bestmove
command which is in the format bestmove <move> ponder <move>
. E.g., bestmove e7e5 ponder g1f3
. This could be in response to the command position startpos moves e2e4
. Basically, the engine is saying the best move for black after white has played e4
is e5
, to which white should best respond by playing Nf3
.
The Chess Board
The chess board in the project is a ListBox
.
<ListBox x:Name="BoardListBox"
HorizontalAlignment="Center" VerticalAlignment="Center"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
ItemsSource="{Binding BoardItems}"
SelectedItem="{Binding SelectedBoardItem, Mode=OneWayToSource}"
ItemContainerStyle="{StaticResource BoardItemContainerStyle}"
ItemTemplate="{StaticResource BoardTemplate}"
ItemsPanel="{StaticResource BoardPanelTemplate}"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
RenderTransformOrigin="0.5,0.5" Grid.Row="1"/>
The ItemsPanelTemplate
of the ListBox
uses a Grid
with eight rows and eight columns.
<ItemsPanelTemplate x:Key="BoardPanelTemplate">
<!-- Width is 400 since each square is 50 in the DrawingBrush – eight squares
in each row and column -->
<Grid Width="400" Height="{Binding RelativeSource={RelativeSource Self}, Path=Width}"
Background="{StaticResource LightWoodBoardBrush}">
<Grid.RowDefinitions>
<RowDefinition/> <RowDefinition/> <RowDefinition/> <RowDefinition/>
<RowDefinition/> <RowDefinition/> <RowDefinition/> <RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/>
<ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/>
</Grid.ColumnDefinitions>
</Grid>
</ItemsPanelTemplate>
The checkerboard pattern is created using a DrawingBrush
that is set as the background of the Grid
.
<DrawingBrush x:Key="LightWoodBoardBrush" Viewport="0,0,100,100"
ViewportUnits="Absolute" TileMode="Tile" Stretch="None">
<DrawingBrush.Drawing>
<DrawingGroup RenderOptions.EdgeMode="Aliased"
RenderOptions.BitmapScalingMode="HighQuality">
<GeometryDrawing>
<GeometryDrawing.Brush>
<ImageBrush ImageSource="Images/Textures/Texture_2.jpg" Stretch="Fill"/>
</GeometryDrawing.Brush>
<GeometryDrawing.Geometry>
<GeometryGroup>
<RectangleGeometry Rect="0,0,100,100"/>
</GeometryGroup>
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing>
<GeometryDrawing.Brush>
<ImageBrush ImageSource="Images/Textures/Texture_1.jpg" Stretch="Fill"/>
</GeometryDrawing.Brush>
<GeometryDrawing.Geometry>
<GeometryGroup>
<RectangleGeometry Rect="50,0,50,50"/>
<RectangleGeometry Rect="0,50,50,50"/>
</GeometryGroup>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
The DataTemplate
of the ListBox
uses an Image
control. There are actually two templates, which I switch between when flipping the board.
<DataTemplate x:Key="BoardTemplate">
<Image Name="PieceImg" Style="{StaticResource PieceStyle}"
RenderTransformOrigin="0.5,0.5">
<Image.RenderTransform>
<RotateTransform Angle="0"/>
</Image.RenderTransform>
</Image>
</DataTemplate>
<DataTemplate x:Key="FlippedBoardTemplate">
<Image Style="{StaticResource PieceStyle}" RenderTransformOrigin="0.5,0.5">
<Image.RenderTransform>
<RotateTransform Angle="180"/>
</Image.RenderTransform>
</Image>
</DataTemplate>
The Image
control uses MultiDataTrigger
s to display the desired image for a particular piece.
<Style x:Key="PieceStyle" TargetType="Image">
<Style.Triggers>
<!-- White Pieces -->
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ItemType}"
Value="{x:Static Member=enums:ChessBoardItem.Piece}"/>
<Condition Binding="{Binding Piece}"
Value="{x:Static Member=enums:PieceType.King}"/>
<Condition Binding="{Binding Color}"
Value="{x:Static Member=enums:PieceColor.White}"/>
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter Property="Source" Value="Images/Pieces/WhiteKing.png"/>
</MultiDataTrigger.Setters>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ItemType}"
Value="{x:Static Member=enums:ChessBoardItem.Piece}"/>
<Condition Binding="{Binding Piece}"
Value="{x:Static Member=enums:PieceType.Queen}"/>
<Condition Binding="{Binding Color}"
Value="{x:Static Member=enums:PieceColor.White}"/>
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter Property="Source" Value="Images/Pieces/WhiteQueen.png"/>
</MultiDataTrigger.Setters>
</MultiDataTrigger>
...
</Style.Triggers>
</Style>
The location of a square or piece on the board is determined by its Rank
and File
.
<Style x:Key="BoardItemContainerStyle" TargetType="ListBoxItem">
<Setter Property="Grid.Row" Value="{Binding Rank,
Converter={StaticResource RankRowConverter}, Mode=OneWay}"/>
<Setter Property="Grid.Column" Value="{Binding File,
Converter={StaticResource FileColumnConverter}, Mode=OneWay}"/>
...
</Style>
Changing the Player Color
The player color can be changed by selecting one of two options in the Color
menu. Doing so results in a new game being set up. In the screenshot above, the Color
menu is open showing I was playing using the black pieces. The following is the XAML for the Color
menu:
<MenuItem Name="ColorMenuItem" Header="Color" Margin="12,0,0,0">
<MenuItem.Icon>
<Border Style="{StaticResource IconBorderStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Rectangle Fill="White"/>
<Rectangle Grid.Row="1" Fill="Black"/>
</Grid>
</Border>
</MenuItem.Icon>
<MenuItem Name="WhiteMenuItem" Header="White" IsCheckable="True" IsChecked="True"
Checked="ItemChecked" Command="{Binding ChangePlayerColorCommand}"
CommandParameter="{x:Static Member=enums:PieceColor.White}"/>
<MenuItem Name="BlackMenuItem" Header="Black" IsCheckable="True"
Checked="ItemChecked" Command="{Binding ChangePlayerColorCommand}"
CommandParameter="{x:Static Member=enums:PieceColor.Black}"/>
</MenuItem>
Flipping the Chess Board
Flipping the chess board is done by selecting the Flip Board option in the Game menu. The flipping is done in the Window's code behind by rotating the ListBox
and switching its DataTemplate
, as I mentioned earlier.
namespace StockChessCS
{
public partial class MainWindow
{
private bool isBoardFlipped;
private RotateTransform boardRotateTx;
private DataTemplate boardTemplate;
public MainWindow()
{
InitializeComponent();
boardRotateTx = new RotateTransform();
}
private void FlipBoard(object sender, RoutedEventArgs e)
{
if (!isBoardFlipped)
{
boardRotateTx.Angle = 180;
boardTemplate = (DataTemplate)FindResource("FlippedBoardTemplate");
isBoardFlipped = true;
}
else
{
boardRotateTx.Angle = 0;
boardTemplate = (DataTemplate)FindResource("BoardTemplate");
isBoardFlipped = false;
}
BoardListBox.RenderTransform = boardRotateTx;
BoardListBox.ItemTemplate = boardTemplate;
}
...
}
}
Class MainWindow
Private isBoardFlipped As Boolean
Private boardRotateTx As New RotateTransform
Private boardTemplate As DataTemplate
Private Sub FlipBoard(ByVal sender As Object, ByVal e As RoutedEventArgs)
If Not isBoardFlipped Then
boardRotateTx.Angle = 180
boardTemplate = CType(FindResource("FlippedBoardTemplate"), DataTemplate)
isBoardFlipped = True
Else
boardRotateTx.Angle = 0
boardTemplate = CType(FindResource("BoardTemplate"), DataTemplate)
isBoardFlipped = False
End If
BoardListBox.RenderTransform = boardRotateTx
BoardListBox.ItemTemplate = boardTemplate
End Sub
...
End Class
Conclusion
Though it is virtually impossible for a human being to beat Stockfish, it does offer a good challenge so I hope you'll enjoy using the app. You can download the solution from the link at the top of the article and look through the rest of the code.
History
- 7th December, 2016: Initial post
- 1st January, 2017: Updated code