6
\$\begingroup\$

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.

\$\endgroup\$
0

4 Answers 4

2
\$\begingroup\$

I will update this answer when I have more time, maybe during the weekend, but you deserve feedback for such effort!

  1. While your naming of classes is not bad, you method names leave a little something to be desired. Remember clean code, is code other people can easily understand, and "AddDefaultPieces" doesn't really explain what is going to happen.

    "PopulatePiecesNewGame" is likely a better choice as it better conveys what the method actually does. The other name is too generic, for such a specific task, especially because chess has many modes, default setting for king of the hill is very different.

  2. You don't follow SOLID. This makes your code very hard to test. Especially automated tests. I would recommend watching some uncle bob lectures, his stories are great, but focus on the meat and potatoes, and get his definition of the five letters of SOLID.

  3. Looking at AddDefaultPieces First you loop through players. Then you loop through y that has to be less than 2? When you use i as an index, it is usual to choose the next letter to be j. Y would indicate a y coordinate, but the max values is 2 so that makes no sense.

    The you loop through the column count. Where you confirm the player color is the same as pieceColor.White. (Poor naming, the player.Color is actually a pieceColor, so why not name it player.PieceColor?) That makes no sense.

    Then you get a square, Through some pretty obscure parameter sets.,depending on player color. Especially how the black pieces are placed is a bit strange to figure out. There has to be a more easily readable way to place these pieces in just as efficient a manner.

    Say:

     void PlaceWhitePieces() {
    
        SquareA2 = new Square(1,2);
        SquareA2.Piece = new Pawn(playerWhite);
    
        SquareB2 = new Square(2,2);
        SquareB2.Piece = new Pawn(playerWhite);
    
        etc.  
     }
    
     void PlaceBlackPiecces() {
    
     } 
    

This is also a second point: the notation in chess is x axes = letters, A - H and y axis Numbers 1-8. Why create custom coordinates that then operate on digits only? At least put an abstract layer above it, to hide from the code-reviewer that there is an actual math based coordinate set under the normal notation. This will make it easier to read the code.

Generally switch cases are considered a bad idea, because if you need to add a new type, then you have to revisit all places where that switch case is implemented to correct a change. This might not be relevant for a chess game? But that doesn't mean you shouldn't think about how you would normally avoid a switch case statement. Especially when it isn't needed.

I will try to rewrite this so it becomes more apparent what I mean during the weekend.

I think I would change the amount of maths you are required to do, into a more readable format also. I would suggest you instead make "ValidMoveToCoordinates" a set of coord that are calculated every time a piece is moved, so those validMoves are calculated when a piece is moved, not as part of the Move action. (This would also allow you to change the color of the squares that are possible destinations, much easier)

I am not sure I copied your code correctly, because just copy/pasting the code into my own project causes the capture mechanism to fail; also you cannot "deselect" after touching a piece (even in real chess, you are allowed to touch a piece - it is only after you let the piece go, that the move is required).

But this maybe be sloppy copying on my part. However this is something to consider, until the weekend, where I hope I will have time to do, if not a complete, then at least a partial rewrite of what I think it should look like to be cleaner.

\$\endgroup\$
6
  • \$\begingroup\$ Thanks so much for this btw, I had almost lost hope of getting any feedback! I will have to look at my code again too since it's been a while. \$\endgroup\$ Jun 28, 2023 at 9:32
  • \$\begingroup\$ Capturing does seem to work for me using my Github code, maybe I missed a bit when copying the code over? \$\endgroup\$ Jun 28, 2023 at 10:21
  • 1
    \$\begingroup\$ I am not totally sure :) I apologize for the somewhat vague response. I tried to be as specific as I could. But of course, nothing speaks better than showing the code you would write instead. So This weekend I have negotiated with the wife, I will spend some hours on rewriting the code where I experience the most "wtf's" to something I would subjectively consider cleaner. Regarding Capture: I think perhaps I simply didn't copy paste correctly, I also don't have the piece Image files, so I just leave them blank, I didn't check the code well enough to know if that causes an issue \$\endgroup\$ Jun 28, 2023 at 12:55
  • \$\begingroup\$ Oh wow, thank you so so much. I am in the middle of watching this uncle bob lecture on SOLID as you recommended, and am already realising how much I have misunderstood the purpose of OO! \$\endgroup\$ Jun 28, 2023 at 16:03
  • \$\begingroup\$ I tried to make some changes myself to try to save you a bit of time and have posted it in an answer, is this the sort of thing you were looking at regarding the AddDefaultPieces method? \$\endgroup\$ Jun 29, 2023 at 19:06
2
\$\begingroup\$

You could further improve the flexibility of your code by doing this:

 public class Board : IBoard
    {
        private readonly IBoardCreator _boardCreator;

        private const int ROW_COUNT = 8;
        private const int COL_COUNT = 8;
        private Player[] _players;
        private Square[,] _squares;

        public Board(IBoardCreator boardCreator, Player[] players)
        {
            _boardCreator = boardCreator;
            _players = players;
        }

//etc......

And then rewriting some of the code in the "Board" code like this:

public class BoardCreator : IBoardCreator
{
    private readonly IWhitePieceStartingPosition _whitePieceStartingPosition;
    private readonly IBlackPiecesStartingPosition _blackPiecesStartingPosition;
    private readonly IBoard _board;
    private Square selectedSquare;
    public event EventHandler<PieceChangedEventArgs> _PieceInSquareChanged;

    public BoardCreator(IWhitePieceStartingPosition whitePieceStartingPosition, IBlackPiecesStartingPosition blackPiecesStartingPosition, IBoard board, EventHandler<PieceChangedEventArgs> pieceInSquareChanged)
    {
        _whitePieceStartingPosition = whitePieceStartingPosition;
        _blackPiecesStartingPosition = blackPiecesStartingPosition;
        _board = board;
        _PieceInSquareChanged = pieceInSquareChanged;
    }

    public void SetupBoard()
    {
        _board.SetAvailableSquare(new Square[COL_COUNT, ROW_COUNT]);

        for (int y = 0; y < ROW_COUNT; y++)
        {
            for (int x = 0; x < COL_COUNT; x++)
            {
                s = new Square(x, y);
                s.PieceChanged += OnPieceInSquareChanged;
                _board.GetAvailableSquares()[x, y] = s;
            }
        }
        _whitePieceStartingPosition.PlaceWhiteStartingPieces();
        _blackPiecesStartingPosition.PlaceBlackStartingPieces();
    }

    private void OnPieceInSquareChanged(object sender, PieceChangedEventArgs e)
    {
        if (_PieceInSquareChanged != null)
        {
            _PieceInSquareChanged.Invoke(this, e);
        }
    }
}

public interface IBoardCreator {
    void SetupBoard();
}

public interface IBoard
{
    Square[,] GetAvailableSquares();
    int GetColumnCount();

    void SetAvailableSquare(Square[,] squares);
    //This likely has a few more interfaces....
}

public interface IWhitePieceStartingPosition {
        void PlaceWhiteStartingPieces();
}

public interface IBlackPiecesStartingPosition
{
    void PlaceBlackStartingPieces();
}

public interface IPiecePlacer
{
    void PlacePieceAt(Piece piece, Coords coords);
}

public interface ISquareFromBoardProvider
{
    Square GetSquareAt(Coords coords);
}

public class BlackPiecesStartingPosition : IBlackPiecesStartingPosition
{
    private readonly IPiecePlacer _pieceOnBoardPlacer;
    private readonly IBoard _board;

    public BlackPiecesStartingPosition(IPiecePlacer pieceOnBoardPlacer)
    {
        _pieceOnBoardPlacer = pieceOnBoardPlacer;
    }

    public void PlaceBlackStartingPieces()
    {
        PieceColour col = PieceColour.Black;
        for (int x = 0; x < _board.GetColumnCount(); x++)
        {
            _pieceOnBoardPlacer.PlacePieceAt(new Pawn(col), new Coords(x, 6));
        }
        _pieceOnBoardPlacer.PlacePieceAt(new Rook(col), new Coords(0, 7));
        _pieceOnBoardPlacer.PlacePieceAt(new Knight(col), new Coords(1, 7));
        _pieceOnBoardPlacer.PlacePieceAt(new Bishop(col), new Coords(2, 7));
        _pieceOnBoardPlacer.PlacePieceAt(new Queen(col), new Coords(3, 7));
        _pieceOnBoardPlacer.PlacePieceAt(new King(col), new Coords(4, 7));
        _pieceOnBoardPlacer.PlacePieceAt(new Bishop(col), new Coords(5, 7));
        _pieceOnBoardPlacer.PlacePieceAt(new Knight(col), new Coords(6, 7));
        _pieceOnBoardPlacer.PlacePieceAt(new Rook(col), new Coords(7, 7));
    }
}

public class BlackWhiteStartingPosition : IWhitePieceStartingPosition
{
    private readonly IPiecePlacer _pieceOnBoardPlacer;
    private readonly IBoard _board;

    public BlackWhiteStartingPosition(IPiecePlacer pieceOnBoardPlacer)
    {
        _pieceOnBoardPlacer = pieceOnBoardPlacer;
    }

    public void PlaceWhiteStartingPieces()
    {
        PieceColour col = PieceColour.White;
        for (int x = 0; x < _board.GetColumnCount(); x++)
        {
            _pieceOnBoardPlacer.PlacePieceAt(new Pawn(col), new Coords(x, 1));
        }
        _pieceOnBoardPlacer.PlacePieceAt(new Rook(col), new Coords(0, 0));
        _pieceOnBoardPlacer.PlacePieceAt(new Knight(col), new Coords(1, 0));
        _pieceOnBoardPlacer.PlacePieceAt(new Bishop(col), new Coords(2, 0));
        _pieceOnBoardPlacer.PlacePieceAt(new Queen(col), new Coords(3, 0));
        _pieceOnBoardPlacer.PlacePieceAt(new King(col), new Coords(4, 0));
        _pieceOnBoardPlacer.PlacePieceAt(new Bishop(col), new Coords(5, 0));
        _pieceOnBoardPlacer.PlacePieceAt(new Knight(col), new Coords(6, 0));
        _pieceOnBoardPlacer.PlacePieceAt(new Rook(col), new Coords(7, 0));
    }
}



public class PiecePlacer : IPiecePlacer
{
    private readonly IBoard _board;
    public PiecePlacer(IBoard board)
    {
        board = _board;
    }

    public void PlacePieceAt(Piece piece, Coords coords)
    {
        Square square = _board.GetAvailableSquares()[coords.X, coords.Y];
        square.Piece = piece;
    }
}

You will start to realize you have some dependencies that don't really make sense, they are highly coupled.

For example: your "setupBoard" method. Say you wanted to change, for an existing board, how to setup up its peices. Will you can't DI in a new setupMethod, because the method is written in the class Board. You would have to make a whole new board type, with a different setup, create an instance of it, copy the values of the existing baord, into this new instance, and then run setup, however, if you do as I did, you can now load any setup method into any Board, and then presto you board will now setup in the "new way" provided.

Or at least that is what I attempted to show, it's a bit pseudo still. I believe it will compile? But not 100% sure :D

However, it should get you thinking about how to DI behaviour into your classes, so you don't lock behaviour into a class, that is technically not "responsible" for handling said behaviour.

Think of it like this: The class Board, is like a blueprint for an instance of a said blueprint.

Say it's a blueprint for a nail. It has a point, a rod, and a head.

So you create a class Nail.

Now your boss wants you to also make "Screw" technically the manufacture is the same: head, rod, point. But heads are different (indent for screwdriver), rods are too (they have the rills) but point is the same. Pointy bit at the end.

In code, you would have to implement an implementation of point on BOTH Screw and Nail. What if you could just DI in the different parts?

So a Nail(flatHead, smoothRod, point); and Screw(indentedHead, rilledRod, point);

You can write a unit test for point. you now only have to write 5 unit tests, not 6. (Because point is reused in both)

You have loosely coupled the code. Your unit tests do not multiply when you add more classes. (You are using inheritance) You should be using Composition. Your unit tests are now additative. The unit tests required for you code is now Sum(Number of class) -> NOT NumberOfclasses^NumberOfClasses.

The code here is not complete, there are now some pretty obvious places here where the code is not really that coherent.

So I leave it to you, to read this, and realize that you can make you code much more readable if you separate responsibilities for actions out of classes that have too many.

S -> Single responsibility. A class should ever only have 1 reason to change.

Clean Code. "Code should read like well written prose".

I should read the "Board" class, and not be confused in the slightest what it does, simply by reading the method and variable names.

If you write code like THAT, your next code reviewer will kiss you. Because he is never confused about what your code does.

Don't worry too much about cycles and performance on writing code clean and solid. And the reason why you should not worry is this:

Code anyone can understand, anyone can help you improve. Code no one can understand, no one can help you improve.

The single most important parameter, or indicator for code quality is:

Can everyone understand it? (It's actually a binary value :) ) Then comes: Is the code flexible, can I make changes easily. Then comes: is the code durable, do the changes I make only effect the changes I requested. (If I change a datastructure in class A, will a method in class Z fail? if so, fragile code, not durable)

THEN comes performance. Can I make the code faster by knowing what a compiler does, knowing what my code does.

A compiler can be optimized to write your code, more efficiently. This is what compilers are for. High-level languages are for making code eaiser to read. If you write code no one can read, you have effectively killed the reason for a compiler. Just go write machine code, if you need to be that concerned about your cycles.

I will come back to this code when I have more time. :) The end game for me is to have a nicely decoupled, easy to change, without breaking anything codebase, that can be improved upon performance wise with little effort.

\$\endgroup\$
12
  • \$\begingroup\$ Thank you so much for this :). \$\endgroup\$ Jul 2, 2023 at 18:27
  • \$\begingroup\$ So by looking at your example, the way I do this is by having constructors which take interfaces as parameters, which means I can easily switch to a different implementation of the interface without consequences? \$\endgroup\$ Jul 2, 2023 at 18:34
  • 1
    \$\begingroup\$ More or less. Think of interfaces in your constructor as "behaviour holders", they represent a method you are DI'ing into the class, this is when you have a method that can be used in other classes, (IE and implementation of "MovePiece" for example, that will usually do the same, but lets say you want an implementation that does something different, and you don't want to rewrite all classes that uses the new method to move a piece? how do you do that? Interfaces! \$\endgroup\$ Jul 2, 2023 at 19:28
  • \$\begingroup\$ In more technical terms, you are abstracting out behaviour of a class to isolate side effects, or implementing a return that only generates your return value. This is a methodology to ensure "pure functions" Should likely be called pure methods in OO \$\endgroup\$ Jul 2, 2023 at 19:31
  • \$\begingroup\$ Also it allows you to extended classes that get the interface DI'ed in, with new implementaitons, without modifying them. This is good for the "O" in SOLID -> Open/Close principle \$\endgroup\$ Jul 2, 2023 at 19:33
1
\$\begingroup\$

@Morten Bork

I have tried to take on your advice and refactor some of my code based on your feedback to try to save you some time if I have understood your feedback correctly.

So instead of the AddDefaultPieces method, instead now I have this:

   private void PlaceStartingPieces()
    {
        PlaceBlackStartingPieces();
        PlaceWhiteStartingPieces();
    }

    private void PlaceBlackStartingPieces()
    {
        PieceColour col = PieceColour.Black;
        for (int x = 0; x < COL_COUNT; x++)
        {
            PlacePiece(new Pawn(col), new Coords(x, 6));
        }
        PlacePiece(new Rook(col), new Coords(0, 7));
        PlacePiece(new Knight(col), new Coords(1, 7));
        PlacePiece(new Bishop(col), new Coords(2, 7));
        PlacePiece(new Queen(col), new Coords(3, 7));
        PlacePiece(new King(col), new Coords(4, 7));
        PlacePiece(new Bishop(col), new Coords(5, 7));
        PlacePiece(new Knight(col), new Coords(6, 7));
        PlacePiece(new Rook(col), new Coords(7, 7));
    }

    private void PlaceWhiteStartingPieces()
    {
        PieceColour col = PieceColour.White;
        for (int x = 0; x < COL_COUNT; x++)
        {
            PlacePiece(new Pawn(col), new Coords(x, 1));
        }
        PlacePiece(new Rook(col), new Coords(0, 0));
        PlacePiece(new Knight(col), new Coords(1, 0));
        PlacePiece(new Bishop(col), new Coords(2, 0));
        PlacePiece(new Queen(col), new Coords(3, 0));
        PlacePiece(new King(col), new Coords(4, 0));
        PlacePiece(new Bishop(col), new Coords(5, 0));
        PlacePiece(new Knight(col), new Coords(6, 0));
        PlacePiece(new Rook(col), new Coords(7, 0));
    }

    private void PlacePiece(Piece piece, Coords coords)
    {
        Square square = GetSquareAt(coords);
        square.Piece = piece;
    }

So I no longer have a switch statement, and hopefully what the methods do now are more clear. I have also removed the dependency on Player for all but the Chess class, the Board and Piece classes now depend on the PieceColour enum instead.

I think that I want to try to remove the dependencies individual derived Piece classes, but I'm not sure how to do this with the Pawn and King classes, since they behave in unique ways that are important for the whole game to work.

After watching some lectures on SOLID and Clean Code, I am seeing so much that I've done messily with some of my long methods and functions, and all the dependencies that I have everywhere that make the code rigid and fragile, which especially came clear when trying to make the simple changes here.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ You understood what I meant perfectly, again, I am sorry for the incoherent answer I provided, I was just a bit pressed for time, on such a large post. I will spend some time this weekend working on it, and you have understood perfectly what I meant, I hope you also agree that this is more ... flexible .. when you get requirement changes. Specifically the ability to EXTEND code NOT modify code, is the most singular powerful concept in code. Makes changes so easy to handle, and does NOT cause massive rewrites. \$\endgroup\$ Jun 30, 2023 at 5:46
  • \$\begingroup\$ Lets say you wanted to implement a "randomized" type of starting position for a chess game, where the officer rank is randomized. How would you implement that, but allow all other components to remain unaffected? (Rules are still the same etc.) Just the starting position is different? You wrap the "placeColorStartingPiece" method in an interface, and DI the method into the parent class. Now you can make a "randomized" PlaceStartingPiece method, and unit test it, compared to the "standard" piece placement. And the rest of the classes won't know about it, because the interface is used. \$\endgroup\$ Jun 30, 2023 at 5:53
  • \$\begingroup\$ Also, I will add unit tests to my rewrite to show what I mean, when I say "easier to test" \$\endgroup\$ Jun 30, 2023 at 5:57
0
\$\begingroup\$

@Morten Bork

I now see how difficult I've made to change this code. I am midway through trying to rewrite the Chess class code, but wanted to post the code I am happy with so far to make sure I am doing it correctly.

Board

public interface IBoard
{
    int ColumnCount { get; }
    int RowCount { get; }
    ISquare[,] GetSquares();
    void SetSquares(ISquare[,] squares);
    ISquare GetSquareAt(Coords coords);
    ICollection<IPiece> GetPieces(PieceColour colour);
    Coords GetCoordsOfPiece(IPiece piece);
    void MakeMove(IChessMove move);
    void UndoLastMove();
    IChessMove GetPreviousMove();

}

class Board : IBoard
{
    private readonly IBoardCreator _boardCreator;
    private readonly ISquareProvider _squareProvider;
    private readonly IPieceProvider _pieceProvider;
    private readonly IMoveMaker _moveMaker;

    private const int ROW_COUNT = 8;
    private const int COL_COUNT = 8;
    private ISquare[,] _squares;
    private IEnumerable<PieceMovement> _moveStack;
    public Board(IBoardCreator boardCreator, ISquareProvider squareProvider, IPieceProvider pieceProvider, IMoveMaker moveMaker)
    {
        _boardCreator = boardCreator;
        _squareProvider = squareProvider;
        _pieceProvider = pieceProvider;
        _moveMaker = moveMaker;

        _boardCreator.SetBoard(this);
        _squareProvider.SetBoard(this);
        _pieceProvider.SetBoard(this);
        _moveMaker.SetBoard(this);

        _boardCreator.CreateBoard();
    }
    public int RowCount { get { return ROW_COUNT; } }
    public int ColumnCount { get { return COL_COUNT; } }

    public ISquare[,] GetSquares() => _squares;
    public void SetSquares(ISquare[,] squares) => _squares = squares;
    public ISquare GetSquareAt(Coords coords) => _squareProvider.GetSquareAt(coords);
    public ICollection<IPiece> GetPieces(PieceColour colour) => _pieceProvider.GetPieces(colour);
    public Coords GetCoordsOfPiece(IPiece piece) => _pieceProvider.GetCoordsOfPiece(piece);
    public void MakeMove(IChessMove move) => _moveMaker.MakeMove(move);
    public IChessMove GetPreviousMove() => _moveMaker.GetLastMove();
    public void UndoLastMove() => _moveMaker.UndoLastMove();
}

Board Creator

public interface IBoardCreator
{
    event EventHandler<PieceChangedEventArgs> PieceInSquareChanged;
    void SetBoard(IBoard board);
    void CreateBoard();
}

public interface IStartingPositionSetup
{
    void SetBoard(IBoard board);
    void PlaceStartingPieces();
}

public interface IPiecePlacer
{
    void SetBoard(IBoard board);
    void PlacePiece(IPiece piece, Coords coords);
}

class BoardCreator : IBoardCreator
{
    private IBoard _board;
    private readonly IStartingPositionSetup _startingPositionSetup;

    public event EventHandler<PieceChangedEventArgs> PieceInSquareChanged;
    public BoardCreator(IStartingPositionSetup startingPositionSetup)
    {
        _startingPositionSetup = startingPositionSetup;
    }
    public void SetBoard(IBoard board) => _board = board;
    public void CreateBoard()
    {
        _board.SetSquares(new Square[_board.ColumnCount, _board.RowCount]);
        Square s;
        for (int y = 0; y < _board.RowCount; y++)
        {
            for (int x = 0; x < _board.ColumnCount; x++)
            {
                s = new Square(x, y);
                s.PieceChanged += OnPieceInSquareChanged;
                _board.GetSquares()[x, y] = s;
            }
        }
        _startingPositionSetup.PlaceStartingPieces();
    }
    private void OnPieceInSquareChanged(object sender, PieceChangedEventArgs e)
    {
        if (PieceInSquareChanged != null)
        {
            PieceInSquareChanged.Invoke(this, e);
        }
    }
}

class StartingPositionSetup : IStartingPositionSetup
{
    private IBoard _board;
    private readonly IPiecePlacer _piecePlacer;
    public StartingPositionSetup(IPiecePlacer piecePlacer)
    {
        _piecePlacer = piecePlacer;
    }
    public void SetBoard(IBoard board)
    {
        _board = board;
        _piecePlacer.SetBoard(_board);
    }
    public void PlaceStartingPieces()
    {
        PlaceBlackStartingPieces();
        PlaceWhiteStartingPieces();
    }
    private void PlaceBlackStartingPieces()
    {
        PieceColour col = PieceColour.Black;
        for (int x = 0; x < _board.ColumnCount; x++)
        {
            _piecePlacer.PlacePiece(new Pawn(col), new Coords(x, 6));
        }
        _piecePlacer.PlacePiece(new Rook(col), new Coords(0, 7));
        _piecePlacer.PlacePiece(new Knight(col), new Coords(1, 7));
        _piecePlacer.PlacePiece(new Bishop(col), new Coords(2, 7));
        _piecePlacer.PlacePiece(new Queen(col), new Coords(3, 7));
        _piecePlacer.PlacePiece(new King(col), new Coords(4, 7));
        _piecePlacer.PlacePiece(new Bishop(col), new Coords(5, 7));
        _piecePlacer.PlacePiece(new Knight(col), new Coords(6, 7));
        _piecePlacer.PlacePiece(new Rook(col), new Coords(7, 7));
    }

    private void PlaceWhiteStartingPieces()
    {
        PieceColour col = PieceColour.White;
        for (int x = 0; x < _board.ColumnCount; x++)
        {
            _piecePlacer.PlacePiece(new Pawn(col), new Coords(x, 1));
        }
        _piecePlacer.PlacePiece(new Rook(col), new Coords(0, 0));
        _piecePlacer.PlacePiece(new Knight(col), new Coords(1, 0));
        _piecePlacer.PlacePiece(new Bishop(col), new Coords(2, 0));
        _piecePlacer.PlacePiece(new Queen(col), new Coords(3, 0));
        _piecePlacer.PlacePiece(new King(col), new Coords(4, 0));
        _piecePlacer.PlacePiece(new Bishop(col), new Coords(5, 0));
        _piecePlacer.PlacePiece(new Knight(col), new Coords(6, 0));
        _piecePlacer.PlacePiece(new Rook(col), new Coords(7, 0));
    }   
}

class PiecePlacer : IPiecePlacer
{
    private IBoard _board;
    public void SetBoard(IBoard board) => _board = board;
    public void PlacePiece(IPiece piece, Coords coords)
    {
        ISquare square = _board.GetSquareAt(coords);
        square.Piece = piece;
    }
}

MoveMaker

public interface IMoveMaker
{
    void SetBoard(IBoard board);
    void MakeMove(IChessMove move);
    void UndoLastMove();
    IChessMove GetLastMove();
}

class MoveMaker : IMoveMaker
{
    private IBoard _board;
    private Stack<IChessMove> _moveStack;
    public void SetBoard(IBoard board)
    {
        _board = board;
        ResetMoveStack();
    }
    public void MakeMove(IChessMove move)
    {
        try
        {
            move.ExecuteMove(_board);
            _moveStack.Push(move);
        }
        catch (Exception e)
        {
            throw e;
        }
    }

    public void UndoLastMove()
    {
        IChessMove lastMove = _moveStack.Pop();
        lastMove.ReverseMove(_board);
    }

    public IChessMove GetLastMove() => _moveStack.Peek();

    private void ResetMoveStack()
    {
        _moveStack = new Stack<IChessMove>();
    }
}

PieceProvider

public interface IPieceProvider
{
    void SetBoard(IBoard board);
    ICollection<IPiece> GetPieces(PieceColour colour);
    Coords GetCoordsOfPiece(IPiece piece);
}

class PieceProvider : IPieceProvider
{
    private IBoard _board;
    public void SetBoard(IBoard board) => _board = board;
    public ICollection<IPiece> GetPieces(PieceColour colour)
    {
        ICollection<IPiece> pieces = new List<IPiece>();
        IPiece p;
        for (int y = 0; y < _board.RowCount; y++)
        {
            for (int x = 0; x < _board.ColumnCount; x++)
            {
                p = _board.GetSquareAt(new Coords(x, y)).Piece;
                if (p != null && p.Colour == colour)
                {
                    pieces.Add(p);
                }
            }
        }
        return pieces;
    }

    public Coords GetCoordsOfPiece(IPiece piece)
    {
        if (piece == null) throw new System.ArgumentNullException(nameof(piece));
        for (int y = 0; y < _board.RowCount; y++)
        {
            for (int x = 0; x < _board.ColumnCount; x++)
            {
                if (_board.GetSquares()[x, y].Piece == piece)
                {
                    return new Coords(x, y);
                }
            }
        }
        throw new System.ArgumentOutOfRangeException(nameof(piece), "Could not find piece");
    }
}

SquareProvider

public interface ISquareProvider
{
    void SetBoard(IBoard board);
    ISquare GetSquareAt(Coords coords);
}

class SquareProvider : ISquareProvider
{
    private IBoard _board;
    public void SetBoard(IBoard board) => _board = board;
    public ISquare GetSquareAt(Coords coords) => _board.GetSquares()[coords.X, coords.Y];
}

Do you think that maybe some of my classes might need breaking up further? For example, the MoveMaker class, should I break it up into a MoveMaker and MoveUndoer?

Thanks so much for your help so far again :)

\$\endgroup\$
9

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.