Introduction
After getting initial feedback on my first working version of this Chess WinForms game here, I have used the suggestions and comments from there to come up with this new version, trying to follow the MVC design pattern.
Here is the Github repo (as of writing, is under the Separating_Concerns branch): https://github.com/Shinglington/prjChessForms
Code
I've split up this section into parts based on the MVC design pattern.
Model
Compared to my previous post, a Chess object now does not handle the UI.
Chess
using prjChessForms.Controller;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
namespace prjChessForms.MyChessLibrary
{
enum PromotionOption
{
Queen,
Rook,
Knight,
Bishop,
}
class Chess
{
public event EventHandler<PieceSelectionChangedEventArgs> PieceSelectionChanged;
public event EventHandler<PlayerTimerTickEventArgs> PlayerTimerTick;
public event EventHandler<PlayerCapturedPiecesChangedEventArgs> PlayerCapturedPiecesChanged;
public event EventHandler<PromotionEventArgs> PlayerPromotion;
public event EventHandler<GameOverEventArgs> GameOver;
private Board _board;
private System.Timers.Timer _timer;
private CancellationTokenSource cts = new CancellationTokenSource();
private GameResult _result;
private Player[] _players;
private int _turnCount;
private SemaphoreSlim _semaphoreReceiveClick = new SemaphoreSlim(0, 1);
private Coords _clickedCoords;
private PromotionOption _selectedPromotion;
private bool _waitingForClick, _waitingForPromotion;
public Chess()
{
CreatePlayers(new TimeSpan(0, 10, 0));
_board = new Board(_players);
_timer = new System.Timers.Timer(1000);
_waitingForClick = false;
}
public Player CurrentPlayer { get { return _players[_turnCount % 2]; } }
public Player WhitePlayer { get { return _players[0]; } }
public Player BlackPlayer { get { return _players[1]; } }
public Square[,] BoardSquares { get { return _board.GetSquares(); } }
public Piece GetPieceAt(Coords coords) => _board.GetPieceAt(coords);
public Coords GetCoordsOf(Piece piece) => _board.GetCoordsOfPiece(piece);
public async Task StartGame()
{
await Play(cts.Token);
OnGameOver();
}
public void SendCoords(Coords coords)
{
if (_waitingForClick)
{
Debug.WriteLine("Model received coords input of {0}", coords);
_clickedCoords = coords;
_semaphoreReceiveClick.Release();
}
}
public void SendPromotion(PromotionOption option)
{
if (_waitingForPromotion)
{
Debug.WriteLine("Promotion received to {0}", option.ToString());
_selectedPromotion = option;
_semaphoreReceiveClick.Release();
}
}
public void AttachModelObserver(IModelObserver observer)
{
_board.PieceInSquareChanged += new EventHandler<PieceChangedEventArgs>(observer.OnPieceInSquareChanged);
PieceSelectionChanged += new EventHandler<PieceSelectionChangedEventArgs>(observer.OnPieceSelectionChanged);
PlayerTimerTick += new EventHandler<PlayerTimerTickEventArgs>(observer.OnPlayerTimerTick);
PlayerCapturedPiecesChanged += new EventHandler<PlayerCapturedPiecesChangedEventArgs>(observer.OnPlayerCapturedPiecesChanged);
PlayerPromotion += new EventHandler<PromotionEventArgs>(observer.OnPromotion);
GameOver += new EventHandler<GameOverEventArgs>(observer.OnGameOver);
foreach(Square s in _board.GetSquares())
{
observer.OnPieceInSquareChanged(this, new PieceChangedEventArgs(s, s.Piece));
}
observer.OnPlayerTimerTick(this, new PlayerTimerTickEventArgs(WhitePlayer));
observer.OnPlayerTimerTick(this, new PlayerTimerTickEventArgs(BlackPlayer));
}
private async Task Play(CancellationToken cToken)
{
_turnCount = 0;
_result = GameResult.Unfinished;
_timer.Elapsed += OnPlayerTimerTick;
while (_result == GameResult.Unfinished)
{
try
{
ChessMove move = await GetChessMove(cToken);
CapturePiece(Rulebook.MakeMove(_board, CurrentPlayer, move));
ChangeSelection(null);
if (Rulebook.RequiresPromotion(_board, move.EndCoords))
{
await Promotion(move.EndCoords, cToken);
}
_turnCount++;
_result = Rulebook.GetGameResult(_board, CurrentPlayer);
}
catch when (cToken.IsCancellationRequested)
{
Debug.WriteLine("Cancelled Play");
_waitingForClick = false;
_result = GameResult.Time;
}
}
_timer.Elapsed -= OnPlayerTimerTick;
cts.Cancel();
}
private async Task<ChessMove> GetChessMove(CancellationToken cToken)
{
Coords fromCoords = Coords.Null;
Coords toCoords = Coords.Null;
ChessMove move = new ChessMove();
bool completeInput = false;
_waitingForClick = true;
_timer.Start();
while (!completeInput)
{
Debug.WriteLine("Waiting for click");
await _semaphoreReceiveClick.WaitAsync(cToken);
Debug.WriteLine("Received click at {0}", _clickedCoords);
if (GetPieceAt(_clickedCoords) != null && GetPieceAt(_clickedCoords).Owner.Equals(CurrentPlayer))
{
ChangeSelection(GetPieceAt(_clickedCoords));
fromCoords = _clickedCoords;
toCoords = Coords.Null;
}
else if (!fromCoords.Equals(Coords.Null))
{
toCoords = _clickedCoords;
}
// Check if move is valid now
if (!toCoords.IsNull && !fromCoords.IsNull && Rulebook.CheckLegalMove(_board, CurrentPlayer, new ChessMove(fromCoords, toCoords)))
{
move = new ChessMove(fromCoords, toCoords);
completeInput = true;
}
else
{
toCoords = Coords.Null;
}
}
_waitingForClick = false;
_timer.Stop();
return move;
}
private void CreatePlayers(TimeSpan time)
{
_players = new Player[2];
_players[0] = new HumanPlayer(PieceColour.White, time);
_players[1] = new HumanPlayer(PieceColour.Black, time);
}
private void OnPlayerTimerTick(object sender, ElapsedEventArgs e)
{
TimeSpan interval = new TimeSpan(0, 0, 1);
if (CurrentPlayer.RemainingTime.Subtract(interval) < TimeSpan.Zero)
{
_timer.Elapsed -= OnPlayerTimerTick;
cts.Cancel();
}
else
{
CurrentPlayer.TickTime(interval);
if (PlayerTimerTick != null)
{
PlayerTimerTick.Invoke(this, new PlayerTimerTickEventArgs(CurrentPlayer));
}
}
}
private void OnGameOver()
{
cts.Cancel();
Player winner = null;
if (_result == GameResult.Checkmate || _result == GameResult.Time)
{
winner = CurrentPlayer == _players[0] ? _players[1] : _players[0];
}
if (GameOver != null)
{
GameOver.Invoke(this, new GameOverEventArgs(_result, winner));
}
}
private void CapturePiece(Piece p)
{
if (p != null)
{
CurrentPlayer.AddCapturedPiece(p);
if (PlayerCapturedPiecesChanged != null)
{
PlayerCapturedPiecesChanged.Invoke(this, new PlayerCapturedPiecesChangedEventArgs(CurrentPlayer, p));
}
}
}
private void ChangeSelection(Piece selectedPiece)
{
if (PieceSelectionChanged != null)
{
List<Coords> endCoords = new List<Coords>();
Coords selectedCoords = new Coords();
if (selectedPiece != null)
{
selectedCoords = GetCoordsOf(selectedPiece);
foreach (ChessMove m in Rulebook.GetPossibleMoves(_board, selectedPiece))
{
endCoords.Add(m.EndCoords);
}
}
PieceSelectionChanged.Invoke(this, new PieceSelectionChangedEventArgs(selectedPiece, selectedCoords, endCoords));
}
}
private async Task Promotion(Coords promotionCoords, CancellationToken cToken)
{
Player owner = GetPieceAt(promotionCoords).Owner;
Piece promotedPiece = new Queen(owner);
if (PlayerPromotion != null)
{
PlayerPromotion.Invoke(this, new PromotionEventArgs(owner.Colour, promotionCoords));
_waitingForPromotion = true;
await _semaphoreReceiveClick.WaitAsync(cToken);
_waitingForPromotion = false;
switch (_selectedPromotion)
{
case PromotionOption.Knight:
promotedPiece = new Knight(owner);
break;
case PromotionOption.Bishop:
promotedPiece = new Bishop(owner);
break;
case PromotionOption.Rook:
promotedPiece = new Rook(owner);
break;
}
}
_board.GetSquareAt(promotionCoords).Piece = promotedPiece;
}
}
}
Board
using System;
using System.Collections.Generic;
namespace prjChessForms.MyChessLibrary
{
class Board
{
public event EventHandler<PieceChangedEventArgs> PieceInSquareChanged;
private const int ROW_COUNT = 8;
private const int COL_COUNT = 8;
private Player[] _players;
private Square[,] _squares;
public Board(Player[] players)
{
_players = players;
SetupBoard();
}
public int RowCount { get { return ROW_COUNT; } }
public int ColumnCount { get { return COL_COUNT; } }
public void MakeMove(ChessMove Move)
{
Coords StartCoords = Move.StartCoords;
Coords EndCoords = Move.EndCoords;
Piece p = GetPieceAt(StartCoords);
if (p != null)
{
GetSquareAt(EndCoords).Piece = p;
GetSquareAt(StartCoords).Piece = null;
p.HasMoved = true;
}
}
public King GetKing(PieceColour colour)
{
King king = null;
foreach (Piece p in GetPieces(colour))
{
if (p.GetType() == typeof(King))
{
king = (King)p;
break;
}
}
return king;
}
public List<Piece> GetPieces(PieceColour colour)
{
List<Piece> pieces = new List<Piece>();
Piece p;
for (int y = 0; y < ROW_COUNT; y++)
{
for (int x = 0; x < COL_COUNT; x++)
{
p = GetPieceAt(new Coords(x, y));
if (p != null && p.Colour == colour)
{
pieces.Add(p);
}
}
}
return pieces;
}
public Piece GetPieceAt(Coords coords)
{
return (GetSquareAt(coords).Piece);
}
public Coords GetCoordsOfPiece(Piece piece)
{
if (piece == null)
{
throw new ArgumentNullException();
}
foreach (Square s in GetSquares())
{
if (s.Piece == piece)
{
return s.Coords;
}
}
throw new Exception("Piece could not be located");
}
public Square[,] GetSquares()
{
return _squares;
}
public Square GetSquareAt(Coords coords)
{
return _squares[coords.X, coords.Y];
}
public void RemoveGhostPawns()
{
foreach (Square s in GetSquares())
{
if (s.GetGhostPawn() != null)
{
s.Piece = null;
}
}
}
public bool CheckMoveInCheck(Player player, ChessMove move)
{
Coords start = move.StartCoords;
Coords end = move.EndCoords;
bool startPieceHasMoved = GetPieceAt(start).HasMoved;
Piece originalEndPiece = GetPieceAt(end);
MakeMove(move);
bool SelfCheck = Rulebook.IsInCheck(this, player);
MakeMove(new ChessMove(end, start));
GetSquareAt(start).Piece.HasMoved = startPieceHasMoved;
GetSquareAt(end).Piece = originalEndPiece;
return SelfCheck;
}
private void SetupBoard()
{
_squares = new Square[COL_COUNT, ROW_COUNT];
Square s;
for (int y = 0; y < ROW_COUNT; y++)
{
for (int x = 0; x < COL_COUNT; x++)
{
s = new Square(x, y);
s.PieceChanged += OnPieceInSquareChanged;
_squares[x, y] = s;
}
}
AddDefaultPieces();
}
private void AddDefaultPieces()
{
char[,] defaultPieces =
{
{ 'P','P','P','P','P','P','P','P'},
{ 'R','N','B','Q','K','B','N','R'}
};
Player player;
Square square;
for (int i = 0; i < 2; i++)
{
player = _players[i];
for (int y = 0; y < 2; y++)
{
for (int x = 0; x < COL_COUNT; x++)
{
if (player.Colour == PieceColour.White)
{
square = GetSquareAt(new Coords(x, 1 - y));
}
else
{
square = GetSquareAt(new Coords(x, ROW_COUNT - 2 + y));
}
AddPiece(defaultPieces[y, x], player, square);
}
}
}
}
private void AddPiece(char pieceType, Player player, Square square)
{
Piece p = null;
switch (pieceType)
{
case 'P':
p = new Pawn(player);
break;
case 'N':
p = new Knight(player);
break;
case 'B':
p = new Bishop(player);
break;
case 'R':
p = new Rook(player);
break;
case 'Q':
p = new Queen(player);
break;
case 'K':
p = new King(player);
break;
default:
throw new ArgumentException("Unrecognised pieceType");
}
square.Piece = p;
}
private void OnPieceInSquareChanged(object sender, PieceChangedEventArgs e)
{
if (PieceInSquareChanged != null)
{
PieceInSquareChanged.Invoke(this, e);
}
}
}
}
Square
using System;
namespace prjChessForms.MyChessLibrary
{
class Square
{
public EventHandler<PieceChangedEventArgs> PieceChanged;
private Piece _piece;
public Square(int x, int y)
{
Coords = new Coords(x, y);
_piece = null;
}
public Piece Piece
{
get
{
if (_piece != null && _piece.GetType() == typeof(GhostPawn))
{
return null;
}
return _piece;
}
set
{
_piece = value;
if (PieceChanged != null)
{
PieceChanged.Invoke(this, new PieceChangedEventArgs(this, Piece));
}
}
}
public Coords Coords { get; }
public GhostPawn GetGhostPawn()
{
return (_piece != null && _piece.GetType() == typeof(GhostPawn)) ? (GhostPawn)_piece : null;
}
}
}
Piece
using System;
using System.Drawing;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace prjChessForms.MyChessLibrary
{
public enum PieceColour
{
White,
Black
}
abstract class Piece
{
public Piece(Player player)
{
Owner = player;
string imageName = Colour.ToString() + "_" + this.GetType().Name;
try
{
Image = (Image)Properties.Resources.ResourceManager.GetObject(imageName);
}
catch
{
Image = null;
}
}
public bool HasMoved { get; set; }
public Player Owner { get; }
public Image Image { get; }
public PieceColour Colour { get { return Owner.Colour; } }
public string Name { get { return GetType().Name; } }
public string Fullname { get { return $"{Colour} {Name}"; } }
public override string ToString()
{
return Fullname;
}
public abstract bool CanMove(Board board, Coords startCoords, Coords endCoords);
}
class Pawn : Piece
{
public Pawn(Player player) : base(player) { }
public override bool CanMove(Board board, Coords startCoords, Coords endCoords)
{
bool allowed = false;
int xChange = endCoords.X - startCoords.X;
int yChange = endCoords.Y - startCoords.Y;
int direction = yChange > 0 ? 1 : -1;
if (direction != (Owner.Colour == PieceColour.White ? 1 : -1))
{
return false;
}
if (xChange == 0 && board.GetPieceAt(endCoords) == null)
{
if (Math.Abs(yChange) == 1)
{
allowed = true;
}
else if (Math.Abs(yChange) == 2 && !HasMoved
&& board.GetPieceAt(new Coords(startCoords.X, startCoords.Y + direction)) == null)
{
allowed = true;
}
}
if (Math.Abs(xChange) == 1 && Math.Abs(yChange) == 1)
{
if (board.GetPieceAt(endCoords) != null)
{
allowed = true;
}
}
return allowed;
}
}
class GhostPawn : Piece
{
public GhostPawn(Player player, Pawn referencedPawn) : base(player)
{
LinkedPawn = referencedPawn;
}
public Pawn LinkedPawn { get; }
public override bool CanMove(Board board, Coords startCoords, Coords endCoords)
{
return false;
}
}
class Knight : Piece
{
public Knight(Player player) : base(player) { }
public override bool CanMove(Board board, Coords startCoords, Coords endCoords)
{
bool allowed = false;
int xChange = Math.Abs(endCoords.X - startCoords.X);
int yChange = Math.Abs(endCoords.Y - startCoords.Y);
if ((xChange == 2 && yChange == 1) || (xChange == 1 && yChange == 2))
{
allowed = true;
}
return allowed;
}
}
class Bishop : Piece
{
public Bishop(Player player) : base(player) { }
public override bool CanMove(Board board, Coords startCoords, Coords endCoords)
{
bool allowed = false;
int xChange = endCoords.X - startCoords.X;
int yChange = endCoords.Y - startCoords.Y;
if (Math.Abs(xChange) == Math.Abs(yChange))
{
allowed = true;
int xDirection = xChange > 0 ? 1 : -1;
int yDirection = yChange > 0 ? 1 : -1;
for (int delta = 1; delta < Math.Abs(xChange); delta += 1)
{
Coords checkCoords = new Coords(startCoords.X + delta * xDirection, startCoords.Y + delta * yDirection);
if (board.GetPieceAt(checkCoords) != null)
{
allowed = false;
break;
}
}
}
return allowed;
}
}
class Rook : Piece
{
public Rook(Player player) : base(player) { }
public override bool CanMove(Board board, Coords startCoords, Coords endCoords)
{
bool allowed = false;
int xChange = endCoords.X - startCoords.X;
int yChange = endCoords.Y - startCoords.Y;
if (xChange == 0 && yChange != 0)
{
allowed = true;
int direction = yChange > 0 ? 1 : -1;
for (int deltaY = 1; deltaY < Math.Abs(yChange); deltaY += 1)
{
Coords checkCoords = new Coords(startCoords.X, startCoords.Y + deltaY * direction);
if (board.GetPieceAt(checkCoords) != null)
{
allowed = false;
break;
}
}
}
else if (yChange == 0 && xChange != 0)
{
allowed = true;
int direction = xChange > 0 ? 1 : -1;
for (int deltaX = 1; deltaX < Math.Abs(xChange); deltaX += 1)
{
Coords checkCoords = new Coords(startCoords.X + deltaX * direction, startCoords.Y);
if (board.GetPieceAt(checkCoords) != null)
{
allowed = false;
break;
}
}
}
return allowed;
}
}
class Queen : Piece
{
public Queen(Player player) : base(player) { }
public override bool CanMove(Board board, Coords startCoords, Coords endCoords)
{
return BishopMove(board, startCoords, endCoords) || RookMove(board, startCoords, endCoords);
}
private bool BishopMove(Board board, Coords startCoords, Coords endCoords)
{
bool allowed = false;
int xChange = endCoords.X - startCoords.X;
int yChange = endCoords.Y - startCoords.Y;
if (Math.Abs(xChange) == Math.Abs(yChange))
{
allowed = true;
int xDirection = xChange > 0 ? 1 : -1;
int yDirection = yChange > 0 ? 1 : -1;
for (int delta = 1; delta < Math.Abs(xChange); delta += 1)
{
Coords checkCoords = new Coords(startCoords.X + delta * xDirection, startCoords.Y + delta * yDirection);
if (board.GetPieceAt(checkCoords) != null)
{
allowed = false;
break;
}
}
}
return allowed;
}
private bool RookMove(Board board, Coords startCoords, Coords endCoords)
{
bool allowed = false;
int xChange = endCoords.X - startCoords.X;
int yChange = endCoords.Y - startCoords.Y;
if (xChange == 0 && yChange != 0)
{
allowed = true;
int direction = yChange > 0 ? 1 : -1;
for (int deltaY = 1; deltaY < Math.Abs(yChange); deltaY += 1)
{
Coords checkCoords = new Coords(startCoords.X, startCoords.Y + deltaY * direction);
if (board.GetPieceAt(checkCoords) != null)
{
allowed = false;
break;
}
}
}
else if (yChange == 0 && xChange != 0)
{
allowed = true;
int direction = xChange > 0 ? 1 : -1;
for (int deltaX = 1; deltaX < Math.Abs(xChange); deltaX += 1)
{
Coords checkCoords = new Coords(startCoords.X + deltaX * direction, startCoords.Y);
if (board.GetPieceAt(checkCoords) != null)
{
allowed = false;
break;
}
}
}
return allowed;
}
}
class King : Piece
{
public King(Player player) : base(player) { }
public override bool CanMove(Board board, Coords startCoords, Coords endCoords)
{
bool allowed = false;
int xChange = endCoords.X - startCoords.X;
int yChange = endCoords.Y - startCoords.Y;
if (Math.Abs(xChange) <= 1 && Math.Abs(yChange) <= 1)
{
allowed = true;
}
return allowed;
}
}
}
Player
using System;
using System.Collections.Generic;
namespace prjChessForms.MyChessLibrary
{
abstract class Player
{
public Player(PieceColour colour, TimeSpan initialTime)
{
Colour = colour;
RemainingTime = initialTime;
CapturedPieces = new List<Piece>();
}
public TimeSpan RemainingTime { get; private set; }
public PieceColour Colour { get; }
public List<Piece> CapturedPieces { get; private set; }
public void TickTime(TimeSpan time)
{
RemainingTime = RemainingTime.Subtract(time);
}
public void AddCapturedPiece(Piece piece)
{
CapturedPieces.Add(piece);
}
}
class HumanPlayer : Player
{
public HumanPlayer(PieceColour colour, TimeSpan initialTime) : base(colour, initialTime) { }
}
class ComputerPlayer : Player
{
public ComputerPlayer(PieceColour colour, TimeSpan initialTime) : base(colour, initialTime) { }
}
}
Rulebook
using System;
using System.Collections.Generic;
namespace prjChessForms.MyChessLibrary
{
public enum GameResult
{
Unfinished,
Checkmate,
Stalemate,
Time
}
public struct ChessMove
{
public ChessMove(Coords startCoords, Coords endCoords)
{
StartCoords = startCoords;
EndCoords = endCoords;
}
public Coords StartCoords { get; }
public Coords EndCoords { get; }
public override string ToString()
{
return StartCoords.ToString() + " -> " + EndCoords.ToString();
}
}
class Rulebook
{
public static Piece MakeMove(Board board, Player player, ChessMove move)
{
if (!CheckLegalMove(board, player, move))
{
throw new ArgumentException(string.Format("Move {0} is not a valid move", move));
}
Piece capturedPiece = board.GetPieceAt(move.EndCoords);
if (IsEnPassant(board, move))
{
GhostPawn ghostPawn = board.GetSquareAt(move.EndCoords).GetGhostPawn();
Coords linkedPawnCoords = board.GetCoordsOfPiece(ghostPawn.LinkedPawn);
capturedPiece = ghostPawn.LinkedPawn;
board.GetSquareAt(linkedPawnCoords).Piece = null;
}
// Remove ghost pawns
board.RemoveGhostPawns();
if (IsDoublePawnMove(board, move))
{
Coords ghostPawnCoords = new Coords(move.StartCoords.X, move.StartCoords.Y + (move.EndCoords.Y - move.StartCoords.Y) / 2);
board.GetSquareAt(ghostPawnCoords).Piece = new GhostPawn(player, (Pawn)board.GetPieceAt(move.StartCoords));
}
else if (IsCastle(board, move))
{
int direction = move.EndCoords.X - move.StartCoords.X > 0 ? 1 : -1;
Coords rookCoords = direction > 0 ? new Coords(board.ColumnCount - 1, move.StartCoords.Y) : new Coords(0, move.StartCoords.Y);
board.MakeMove(new ChessMove(rookCoords, new Coords(move.EndCoords.X + direction * -1, move.EndCoords.Y)));
}
board.MakeMove(move);
return capturedPiece;
}
public static bool CheckLegalMove(Board board, Player player, ChessMove move)
{
Coords start = move.StartCoords;
Coords end = move.EndCoords;
bool legal = false;
Piece movedPiece = board.GetPieceAt(start);
Piece capturedPiece = board.GetPieceAt(end);
if (movedPiece != null && movedPiece.Colour == player.Colour && !start.Equals(end))
{
if (IsEnPassant(board, move) || IsCastle(board, move))
{
legal = true;
}
else if (movedPiece.CanMove(board, start, end))
{
if (capturedPiece == null || (capturedPiece.Colour != player.Colour))
{
if (!board.CheckMoveInCheck(player, move))
{
legal = true;
}
}
}
}
return legal;
}
public static List<ChessMove> GetPossibleMoves(Board board, Piece p)
{
List<ChessMove> possibleMoves = new List<ChessMove>();
Coords pieceCoords = board.GetCoordsOfPiece(p);
if (board.GetPieceAt(pieceCoords) != null)
{
Piece piece = board.GetPieceAt(pieceCoords);
ChessMove move;
for (int y = 0; y < board.RowCount; y++)
{
for (int x = 0; x < board.ColumnCount; x++)
{
move = new ChessMove(pieceCoords, new Coords(x, y));
if (CheckLegalMove(board, piece.Owner, move))
{
possibleMoves.Add(move);
}
}
}
}
return possibleMoves;
}
public static GameResult GetGameResult(Board board, Player current)
{
if (IsInStalemate(board, current))
{
return GameResult.Stalemate;
}
else if (IsInCheckmate(board, current))
{
return GameResult.Checkmate;
}
else
{
return GameResult.Unfinished;
}
}
public static bool IsInCheck(Board board, Player currentPlayer)
{
bool check = false;
King king = board.GetKing(currentPlayer.Colour);
if (king == null)
{
return true;
}
Coords kingCoords = board.GetCoordsOfPiece(king);
foreach (Square square in board.GetSquares())
{
if (square.Piece != null && square.Piece.Owner != currentPlayer)
{
if (CheckLegalMove(board, square.Piece.Owner, new ChessMove(square.Coords, kingCoords)))
{
check = true;
break;
}
}
}
return check;
}
public static bool RequiresPromotion(Board board, Coords pieceCoords)
{
bool requiresPromotion = false;
Piece p = board.GetPieceAt(pieceCoords);
if (p.GetType() == typeof(Pawn))
{
if (pieceCoords.Y == 0 || pieceCoords.Y == board.RowCount - 1)
{
requiresPromotion = true;
}
}
return requiresPromotion;
}
private static bool IsInCheckmate(Board board, Player currentPlayer)
{
if (!IsInCheck(board, currentPlayer))
{
return false;
}
return !CheckIfThereAreRemainingLegalMoves(board, currentPlayer);
}
private static bool IsInStalemate(Board board, Player currentPlayer)
{
if (IsInCheck(board, currentPlayer))
{
return false;
}
return !CheckIfThereAreRemainingLegalMoves(board, currentPlayer);
}
private static bool CheckIfThereAreRemainingLegalMoves(Board board, Player currentPlayer)
{
bool anyLegalMoves = false;
foreach (Piece p in board.GetPieces(currentPlayer.Colour))
{
List<ChessMove> moves = GetPossibleMoves(board, p);
if (moves.Count > 0)
{
anyLegalMoves = true;
break;
}
}
return anyLegalMoves;
}
private static bool IsDoublePawnMove(Board board, ChessMove move)
{
if (board.GetPieceAt(move.StartCoords).GetType() == typeof(Pawn))
{
if (Math.Abs(move.EndCoords.Y - move.StartCoords.Y) == 2)
{
return true;
}
}
return false;
}
private static bool IsCastle(Board board, ChessMove move)
{
bool isCastleMove = false;
if (board.GetPieceAt(move.StartCoords).GetType() == typeof(King) && !board.GetPieceAt(move.StartCoords).HasMoved)
{
if (Math.Abs(move.EndCoords.Y - move.StartCoords.Y) == 0 && Math.Abs(move.EndCoords.X - move.StartCoords.X) == 2)
{
int direction = move.EndCoords.X - move.StartCoords.X > 0 ? 1 : -1;
Coords rookCoords = direction > 0 ? new Coords(board.ColumnCount - 1, move.StartCoords.Y) : new Coords(0, move.StartCoords.Y);
Piece p = board.GetPieceAt(rookCoords);
if (p != null && p.GetType() == typeof(Rook) && !p.HasMoved)
{
isCastleMove = true;
Coords currCoords = new Coords(move.StartCoords.X + direction, move.StartCoords.Y);
while (!currCoords.Equals(rookCoords))
{
if (board.GetPieceAt(currCoords) != null || board.CheckMoveInCheck(p.Owner, new ChessMove(move.StartCoords, currCoords)))
{
isCastleMove = false;
break;
}
currCoords = new Coords(currCoords.X + direction, currCoords.Y);
}
}
}
}
return isCastleMove;
}
private static bool IsEnPassant(Board board, ChessMove move)
{
if (board.GetPieceAt(move.StartCoords).GetType() == typeof(Pawn))
{
Pawn piece = (Pawn)board.GetPieceAt(move.StartCoords);
int legalDirection = (piece.Colour == PieceColour.White ? 1 : -1);
if (Math.Abs(move.EndCoords.X - move.StartCoords.X) == 1 && move.EndCoords.Y - move.StartCoords.Y == legalDirection)
{
GhostPawn ghostPawn = board.GetSquareAt(move.EndCoords).GetGhostPawn();
if (ghostPawn != null && ghostPawn.Colour != piece.Colour)
{
return true;
}
}
}
return false;
}
}
}
Coords
namespace prjChessForms.MyChessLibrary
{
public struct Coords
{
public static readonly Coords Null = new Coords(-1, -1);
public Coords(int x, int y)
{
X = x;
Y = y;
}
public int X { get; }
public int Y { get; }
public bool IsNull => this.Equals(Null);
public override string ToString() => $"{(char)('a' + X)}{Y + 1}";
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != typeof(Coords))
{
return false;
}
else
{
Coords other = (Coords)obj;
return other.X == X && other.Y == Y;
}
}
}
}
ChessEventArgs
using System;
using System.Collections.Generic;
namespace prjChessForms.MyChessLibrary
{
class PieceChangedEventArgs : EventArgs
{
public PieceChangedEventArgs(Square square, Piece newPiece)
{
NewPiece = newPiece;
Square = square;
}
public Square Square { get; set; }
public Piece NewPiece { get; set; }
}
class PieceSelectionChangedEventArgs : EventArgs
{
public PieceSelectionChangedEventArgs(Piece piece, Coords selectedPieceCoords, List<Coords> validMoves)
{
SelectedPiece = piece;
SelectedPieceCoords = selectedPieceCoords;
PossibleEndCoords = validMoves;
}
public Piece SelectedPiece { get; set; }
public Coords SelectedPieceCoords { get; set; }
public List<Coords> PossibleEndCoords { get; set; }
}
class PlayerTimerTickEventArgs : EventArgs
{
public PlayerTimerTickEventArgs(Player player)
{
CurrentPlayer = player;
PlayerRemainingTime = player.RemainingTime;
}
public Player CurrentPlayer { get; set; }
public TimeSpan PlayerRemainingTime { get; set; }
}
class PlayerCapturedPiecesChangedEventArgs : EventArgs
{
public PlayerCapturedPiecesChangedEventArgs(Player player, Piece capturedPiece)
{
Player = player;
CapturedPiece = capturedPiece;
}
public Player Player { get; set; }
public Piece CapturedPiece { get; set; }
}
class PromotionEventArgs : EventArgs
{
public PromotionEventArgs(PieceColour colour, Coords coords)
{
PromotingCoords = coords;
PromotingColour = colour;
}
public Coords PromotingCoords { get; set; }
public PieceColour PromotingColour { get; set; }
}
class GameOverEventArgs : EventArgs
{
public GameOverEventArgs(GameResult result, Player winner)
{
Result = result;
Winner = winner;
}
public GameResult Result { get; set; }
public Player Winner { get; set; }
}
}
View
The view mainly invokes events which are received by the controller, but also realises an interface IModelObserver, which allows the view to be updated when the model changes.
IModelObserver
using prjChessForms.MyChessLibrary;
namespace prjChessForms.Controller
{
interface IModelObserver
{
void OnPieceInSquareChanged(object sender, PieceChangedEventArgs e);
void OnPieceSelectionChanged(object sender, PieceSelectionChangedEventArgs e);
void OnPlayerCapturedPiecesChanged(object sender, PlayerCapturedPiecesChangedEventArgs e);
void OnPlayerTimerTick(object sender, PlayerTimerTickEventArgs e);
void OnGameOver(object sender, GameOverEventArgs e);
void OnPromotion(object sender, PromotionEventArgs e);
}
}
ChessForm
using prjChessForms.Controller;
using prjChessForms.MyChessLibrary;
using System;
using System.Windows.Forms;
namespace prjChessForms.PresentationUI
{
partial class ChessForm : Form, IModelObserver
{
public EventHandler<SquareClickedEventArgs> SquareClicked;
public EventHandler<PromotionSelectedEventArgs> PromotionSelected;
private TableLayoutPanel _layoutPanel;
private BoardTableLayoutPanel _boardPanel;
private PromotionSelectionPanel _promotionPanel;
private PlayerInformationPanel _whiteInfo;
private PlayerInformationPanel _blackInfo;
public ChessForm()
{
InitializeComponent();
SetupControls();
}
public ChessController Controller { get; set; }
public void OnPieceInSquareChanged(object sender, PieceChangedEventArgs e)
{
_boardPanel.UpdateSquare(e.Square.Coords, e.NewPiece);
}
public void OnPieceSelectionChanged(object sender, PieceSelectionChangedEventArgs e)
{
_boardPanel.UpdateHighlights(e.SelectedPiece, e.SelectedPieceCoords, e.PossibleEndCoords);
}
public void OnPlayerCapturedPiecesChanged(object sender, PlayerCapturedPiecesChangedEventArgs e)
{
switch (e.Player.Colour)
{
case PieceColour.White:
_whiteInfo.UpdateCapturedPieces(e.CapturedPiece);
break;
case PieceColour.Black:
_blackInfo.UpdateCapturedPieces(e.CapturedPiece);
break;
}
}
public void OnPromotion(object sender, PromotionEventArgs e)
{
_promotionPanel = new PromotionSelectionPanel(e.PromotingColour);
_promotionPanel.PromotionSelected += OnPromotionSelected;
_promotionPanel.Show();
}
public void OnGameOver(object sender, GameOverEventArgs e)
{
if (e.Winner == null)
{
MessageBox.Show("Draw", e.Result.ToString());
}
else
{
MessageBox.Show(e.Winner.Colour.ToString() + " Wins!", e.Result.ToString());
}
}
public void OnPlayerTimerTick(object sender, PlayerTimerTickEventArgs e)
{
switch (e.CurrentPlayer.Colour)
{
case PieceColour.White:
_whiteInfo.UpdateTime(e.PlayerRemainingTime);
break;
case PieceColour.Black:
_blackInfo.UpdateTime(e.PlayerRemainingTime);
break;
}
}
private void SetupControls()
{
SetupLayoutPanel();
SetupBoard();
SetupPlayerInfo();
}
private void SetupLayoutPanel()
{
_layoutPanel = new TableLayoutPanel()
{
Parent = this,
Dock = DockStyle.Fill,
Padding = new Padding(0)
};
_layoutPanel.ColumnStyles.Clear();
_layoutPanel.RowStyles.Clear();
_layoutPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 90));
_layoutPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 10));
_layoutPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 10));
_layoutPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 80));
_layoutPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 10));
}
private void SetupBoard()
{
_boardPanel = new BoardTableLayoutPanel(this)
{
Parent = _layoutPanel,
Dock = DockStyle.Fill,
};
_boardPanel.SquareClicked += OnBoardClicked;
_layoutPanel.SetCellPosition(_boardPanel, new TableLayoutPanelCellPosition(0, 1));
}
private void SetupPlayerInfo()
{
_whiteInfo = new PlayerInformationPanel(PieceColour.White)
{
Parent = _layoutPanel,
Dock = DockStyle.Fill,
};
_layoutPanel.SetCellPosition(_whiteInfo, new TableLayoutPanelCellPosition(0, 2));
_blackInfo = new PlayerInformationPanel(PieceColour.Black)
{
Parent = _layoutPanel,
Dock = DockStyle.Fill,
};
_layoutPanel.SetCellPosition(_blackInfo, new TableLayoutPanelCellPosition(0, 0));
}
private void OnBoardClicked(object sender, SquareClickedEventArgs e)
{
if (SquareClicked != null)
{
SquareClicked.Invoke(this, e);
}
}
private void OnPromotionSelected(object sender, PromotionSelectedEventArgs e)
{
if (PromotionSelected != null)
{
_promotionPanel.Hide();
_promotionPanel = null;
PromotionSelected.Invoke(this, e);
}
}
}
}
BoardTableLayoutPanel
using prjChessForms.MyChessLibrary;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
namespace prjChessForms.PresentationUI
{
class BoardTableLayoutPanel : TableLayoutPanel
{
public EventHandler<SquareClickedEventArgs> SquareClicked;
private const int ROWCOUNT = 8;
private const int COLUMNCOUNT = 8;
private Button[,] _buttons;
public BoardTableLayoutPanel(ChessForm parentForm)
{
ParentForm = parentForm;
ColumnCount = COLUMNCOUNT;
RowCount = ROWCOUNT;
SetupRowsAndColumns();
Display();
}
public ChessForm ParentForm { get; }
public void Display(bool flippedPerspective = false)
{
for (int x = 0; x < ColumnCount; x++)
{
for (int y = 0; y < RowCount; y++)
{
if (flippedPerspective)
{
SetCellPosition(_buttons[x, y], new TableLayoutPanelCellPosition(x, y));
}
else
{
SetCellPosition(_buttons[x, y], new TableLayoutPanelCellPosition(x, RowCount - 1 - y));
}
}
}
}
public Coords GetCoordsOf(Button button)
{
for (int y = 0; y < _buttons.GetLength(1); y++)
{
for (int x = 0; x < _buttons.GetLength(0); x++)
{
if (_buttons[x, y] == button)
{
return new Coords(x, y);
}
}
}
throw new ArgumentException("Button could not be found");
}
public void UpdateSquares(Square[,] squares, Piece selectedPiece, List<Coords> possibleMoves)
{
for (int y = 0; y < squares.GetLength(1); y++)
{
for (int x = 0; x < squares.GetLength(0); x++)
{
Piece piece = squares[x, y].Piece;
Button button = _buttons[x, y];
if (piece != null)
{
button.Image = piece.Image;
if (piece.Equals(selectedPiece))
{
button.BackColor = Color.Blue;
}
else if (possibleMoves.Contains(new Coords(x, y)))
{
button.BackColor = Color.Green;
}
else
{
button.BackColor = DefaultBackColor;
}
}
else
{
button.Image = null;
}
}
}
}
public void UpdateHighlights(Piece selectedPiece, Coords selectedCoords, List<Coords> possibleMoves)
{
for (int y = 0; y < _buttons.GetLength(1); y++)
{
for (int x = 0; x < _buttons.GetLength(0); x++)
{
Button button = _buttons[x, y];
if (selectedPiece != null && selectedCoords.Equals(new Coords(x, y)))
{
button.BackColor = Color.Blue;
}
else if (possibleMoves.Contains(new Coords(x, y)))
{
button.BackColor = Color.Green;
}
else
{
button.BackColor = DefaultBackColor;
}
}
}
}
public void UpdateSquare(Coords coords, Piece piece)
{
_buttons[coords.X, coords.Y].Image = piece != null ? piece.Image : null;
}
private void SetupRowsAndColumns()
{
RowStyles.Clear();
ColumnStyles.Clear();
_buttons = new Button[ColumnCount, RowCount];
for (int x = 0; x < ColumnCount; x++)
{
ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100 / ColumnCount));
for (int y = 0; y < RowCount; y++)
{
RowStyles.Add(new RowStyle(SizeType.Percent, 100 / RowCount));
Button button = new Button()
{
Parent = this,
Dock = DockStyle.Fill,
};
button.Click += OnSquareClicked;
_buttons[x, y] = button;
}
}
}
private void OnSquareClicked(object sender, EventArgs e)
{
Button button = (Button)sender;
SquareClicked.Invoke(this, new SquareClickedEventArgs(GetCoordsOf(button)));
}
}
}
PlayerInformationPanel
using prjChessForms.MyChessLibrary;
using System;
using System.Windows.Forms;
namespace prjChessForms.PresentationUI
{
class PlayerInformationPanel : TableLayoutPanel
{
private Label _colourLabel;
private Label _timeLabel;
private CapturedPiecesPanel _capturedPieces;
public PlayerInformationPanel(PieceColour colour)
{
PieceColour = colour;
SetupPanel();
}
public PieceColour PieceColour { get; }
public void UpdateTime(TimeSpan remainingTime)
{
string text = remainingTime.ToString();
if (_timeLabel.InvokeRequired)
{
Action safeWrite = delegate { UpdateTime(remainingTime); };
_timeLabel.Invoke(safeWrite);
}
else
{
_timeLabel.Text = text;
}
}
public void UpdateCapturedPieces(Piece capturedPiece)
{
_capturedPieces.AddCapturedPiece(capturedPiece);
}
private void SetupPanel()
{
RowStyles.Clear();
ColumnStyles.Clear();
RowStyles.Add(new RowStyle(SizeType.Percent, 100));
ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20));
ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 60));
ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20));
SetupCapturedPieces();
SetupLabel();
SetupTimer();
}
private void SetupLabel()
{
_colourLabel = new Label
{
Text = PieceColour.ToString(),
Parent = this,
Dock = DockStyle.Fill,
};
SetCellPosition(_colourLabel, new TableLayoutPanelCellPosition(0, 0));
}
private void SetupCapturedPieces()
{
_capturedPieces = new CapturedPiecesPanel(PieceColour)
{
Parent = this,
Dock = DockStyle.Fill,
Padding = new Padding(0),
};
SetCellPosition(_capturedPieces, new TableLayoutPanelCellPosition(1, 0));
}
private void SetupTimer()
{
_timeLabel = new Label()
{
Text = TimeSpan.Zero.ToString(),
Parent = this,
Dock = DockStyle.Fill,
};
SetCellPosition(_timeLabel, new TableLayoutPanelCellPosition(2, 0));
}
}
}
PromotionSelectionPanel
using prjChessForms.MyChessLibrary;
using System;
using System.Drawing;
using System.Windows.Forms;
namespace prjChessForms.PresentationUI
{
class PromotionSelectionPanel : Form
{
public EventHandler<PromotionSelectedEventArgs> PromotionSelected;
private TableLayoutPanel _panel;
private PromotionOption _selectedPromotion;
public PromotionSelectionPanel(PieceColour colour)
{
PieceColour = colour;
SetupPanel();
SetupButtons();
}
public PieceColour PieceColour { get; }
public PromotionOption SelectedPromotionOption
{
get { return _selectedPromotion; }
set
{
_selectedPromotion = value;
if (PromotionSelected != null)
{
PromotionSelected.Invoke(this, new PromotionSelectedEventArgs(_selectedPromotion));
}
}
}
private void OnSelectionButtonClicked(PromotionOption option) => SelectedPromotionOption = option;
private void SetupPanel()
{
_panel = new TableLayoutPanel()
{
Parent = this,
Dock = DockStyle.Fill,
};
_panel.RowStyles.Clear();
_panel.ColumnStyles.Clear();
_panel.RowStyles.Add(new RowStyle(SizeType.Percent, 50));
_panel.RowStyles.Add(new RowStyle(SizeType.Percent, 50));
_panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
_panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
}
private void SetupButtons()
{
Button button;
int i = 0;
foreach (PromotionOption option in Enum.GetValues(typeof(PromotionOption)))
{
button = new Button()
{
Parent = _panel,
Image = (Image)Properties.Resources.ResourceManager.GetObject(PieceColour.ToString() + "_" + option.ToString()),
Dock = DockStyle.Fill
};
_panel.SetCellPosition(button, new TableLayoutPanelCellPosition(i / 2, i % 2));
button.Click += (sender, e) => OnSelectionButtonClicked(option);
i++;
}
}
}
}
CapturedPiecesPanel
using prjChessForms.MyChessLibrary;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
namespace prjChessForms.PresentationUI
{
internal class CapturedPiecesPanel : TableLayoutPanel
{
private Dictionary<string, int> _capturedPieceCounts;
public CapturedPiecesPanel(PieceColour colour)
{
CapturedPieceColour = colour == PieceColour.White ? PieceColour.Black : PieceColour.White;
_capturedPieceCounts = new Dictionary<string, int>()
{
{"Pawn", 0},
{"Bishop", 0},
{"Knight", 0 },
{"Rook", 0},
{"Queen", 0}
};
RowStyles.Clear();
ColumnStyles.Clear();
RowStyles.Add(new RowStyle(SizeType.Percent, 100));
for (int i = 0; i < 5; i++)
{
ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20));
}
}
public PieceColour CapturedPieceColour { get; }
public void AddCapturedPiece(Piece piece)
{
_capturedPieceCounts[piece.Name]++;
UpdateDisplayPanel();
}
private void UpdateDisplayPanel()
{
Controls.Clear();
int tableIndex = 0;
for (int i = 0; i < 5; i++)
{
KeyValuePair<string, int> keyValuePair = _capturedPieceCounts.ElementAt(i);
if (keyValuePair.Value > 0)
{
Control p = CreatePieceCaptureCounter(keyValuePair.Key);
SetCellPosition(p, new TableLayoutPanelCellPosition(tableIndex, 0));
tableIndex++;
}
}
}
private Control CreatePieceCaptureCounter(string pieceName)
{
int count = _capturedPieceCounts[pieceName];
Label p = new Label()
{
Parent = this,
Dock = DockStyle.Fill,
Image = (Image)Properties.Resources.ResourceManager.GetObject(CapturedPieceColour.ToString() + "_" + pieceName),
Text = "x" + count.ToString()
};
return p;
}
}
}
PresentationEventArgs
using prjChessForms.MyChessLibrary;
using System;
namespace prjChessForms.PresentationUI
{
class SquareClickedEventArgs : EventArgs
{
public SquareClickedEventArgs(Coords coords)
{
ClickedCoords = coords;
}
public Coords ClickedCoords { get; set; }
}
class PromotionSelectedEventArgs : EventArgs
{
public PromotionSelectedEventArgs(PromotionOption option)
{
SelectedOption = option;
}
public PromotionOption SelectedOption { get; set; }
}
}
Controller
The controller sends inputs from the view to the model, and attaches the view as an IModelObserver to the model.
ChessController
using prjChessForms.MyChessLibrary;
using prjChessForms.PresentationUI;
namespace prjChessForms.Controller
{
class ChessController
{
private Chess _chessModel;
private ChessForm _chessView;
public ChessController(Chess chessModel, ChessForm chessView)
{
_chessModel = chessModel;
_chessView = chessView;
_chessView.Controller = this;
_chessModel.AttachModelObserver(_chessView);
_chessView.SquareClicked += OnBoardClickReceived;
_chessView.PromotionSelected += OnPromotionReceived;
_chessModel.StartGame();
}
private void OnBoardClickReceived(object sender, SquareClickedEventArgs e)
{
_chessModel.SendCoords(e.ClickedCoords);
}
private void OnPromotionReceived(object sender, PromotionSelectedEventArgs e)
{
_chessModel.SendPromotion(e.SelectedOption);
}
}
}
Program
using prjChessForms.Controller;
using prjChessForms.MyChessLibrary;
using prjChessForms.PresentationUI;
using System;
using System.Windows.Forms;
namespace prjChessForms
{
internal static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Chess game = new Chess();
ChessForm form = new ChessForm();
ChessController controller = new ChessController(game, form);
Application.Run(form);
}
}
}
Thoughts
This is my first time learning about and following the MVC design pattern, so I am looking forward to getting feedback about it. One other flaw that I am aware of is some inconsistent spacing between properties and one liner methods. While I have (hopefully) consistently made it so that there is one empty line between methods, I am not sure what is the best practice for properties.
Sorry that I haven't put many comments on each class, and hopefully it isn't too difficult to understand what each part of my code does.