3
\$\begingroup\$

Introduction

I decided to program a two player Chess game in C# windows forms to help me to improve my programming skills and OOP skills. I have come to a working finished program (as far as I can tell) where two players on the same computer can play chess together.

Here is the Github link to my repo for easy access: https://github.com/Shinglington/prjChessForms.git

I have also copied the code into here so that the original code at the time of writing can be preserved.

Code

Chess Form

The Form that the game takes place in. Sets up controls for the layout and alternates letting each player make a move.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using System.Windows.Forms;

namespace prjChessForms
{
    public partial class Chess : Form
    {
        private Board _board;
        private TableLayoutPanel _layoutPanel;
        private Label[] _timerLabels;
        private System.Timers.Timer _timer;

        private SemaphoreSlim _semaphoreClick = new SemaphoreSlim(0, 1);
        private CancellationTokenSource cts = new CancellationTokenSource();
        private Coords _clickedCoords;
        private Coords _fromCoords = new Coords();
        private Coords _toCoords = new Coords();

        private GameResult _result;
        private Player[] _players;
        private Player _currentPlayer;
        public Chess()
        {
            InitializeComponent();
            CreatePlayers();
            SetupControls();
            StartGame();

        }

        public async Task StartGame()
        {
            await Play(cts.Token);
            OnGameOver();
        }


        public async Task Play(CancellationToken cToken)
        {
            _currentPlayer = _players[0];
            _result = GameResult.Unfinished;
            while (_result == GameResult.Unfinished)
            {
                try
                {
                    _timer.Elapsed += OnPlayerTimerTick;
                    _timer.Start();
                    ChessMove move = await GetPlayerMove(cToken);
                    _timer.Stop();
                    _timer.Elapsed -= OnPlayerTimerTick;
                    Rulebook.MakeMove(_board, _currentPlayer, move);
                    if (_currentPlayer == _players[1])
                    {
                        _currentPlayer = _players[0];
                    }
                    else
                    {
                        _currentPlayer = _players[1];
                    }
                    _result = Rulebook.GetGameResult(_board, _currentPlayer);
                }
                catch when (cToken.IsCancellationRequested)
                {
                    _result = GameResult.Time;
                }
            }
        }

        private async Task<ChessMove> GetPlayerMove(CancellationToken cToken)
        {

            _fromCoords = new Coords();
            _toCoords = new Coords();
            ChessMove move = new ChessMove();
            bool validMove = false;
            while (!validMove)
            {
                await _semaphoreClick.WaitAsync(cToken);
                if (_board.GetPieceAt(_clickedCoords) != null && _board.GetPieceAt(_clickedCoords).Owner == _currentPlayer)
                {
                    _fromCoords = _clickedCoords;
                    _toCoords = new Coords();
                    _board.ClearHighlights();
                    _board.HighlightAt(_fromCoords, System.Drawing.Color.AliceBlue);
                    foreach (ChessMove m in Rulebook.GetPossibleMoves(_board, _board.GetPieceAt(_fromCoords)))
                    {
                        _board.HighlightAt(m.EndCoords, System.Drawing.Color.Green);
                    }
                }
                else if (!_fromCoords.Equals(new Coords()))
                {
                    _toCoords = _clickedCoords;
                }
                // Check if move is valid now

                if (!_toCoords.Equals(new Coords()) && !_fromCoords.Equals(new Coords()))
                {
                    move = new ChessMove(_fromCoords, _toCoords);
                    validMove = Rulebook.CheckLegalMove(_board, _currentPlayer, move);
                }
            }
            _board.ClearHighlights();
            return move;
        }


        private void CreatePlayers()
        {
            _players = new Player[2];
            _players[0] = new HumanPlayer(PieceColour.White, new TimeSpan(0, 3, 0));
            _players[1] = new HumanPlayer(PieceColour.Black, new TimeSpan(0, 3, 0));
        }

        private void SetupControls()
        {

            // Timer
            _timer = new System.Timers.Timer(1000);
            _timerLabels = new Label[2];

            // Layout
            _layoutPanel = new TableLayoutPanel()
            {
                Parent = this,
                Dock = DockStyle.Fill,
            };
            _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, 5));
            _layoutPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 90));
            _layoutPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 5));

            // Board
            _board = new Board(_players)
            {
                Parent = _layoutPanel
            };
            _layoutPanel.SetCellPosition(_board, new TableLayoutPanelCellPosition(0, 1));
            foreach (Square square in _board.GetSquares())
            {
                square.Click += OnSquareClicked;
            }

            // White player label
            TableLayoutPanel whiteTable = new TableLayoutPanel()
            {
                Parent = _layoutPanel,
                Dock = DockStyle.Fill,
                ColumnStyles = { new ColumnStyle(SizeType.Percent, 50), new ColumnStyle(SizeType.Percent, 50) },
                RowStyles = { new RowStyle(SizeType.Percent, 100) },
            };
            _layoutPanel.SetCellPosition(whiteTable, new TableLayoutPanelCellPosition(0, 2));
            Label whiteLabel = new Label()
            {
                Parent = whiteTable,
                Dock = DockStyle.Fill,
                Text = _players[0].Colour.ToString(),
            };
            whiteTable.SetCellPosition(whiteLabel, new TableLayoutPanelCellPosition(0, 0));
            _timerLabels[0] = new Label()
            {
                Parent = whiteTable,
                Dock = DockStyle.Fill,
                Text = _players[0].RemainingTime.ToString(),
            };
            whiteTable.SetCellPosition(_timerLabels[0], new TableLayoutPanelCellPosition(1, 0));





            // Black player label
            TableLayoutPanel blackTable = new TableLayoutPanel()
            {
                Parent = _layoutPanel,
                Dock = DockStyle.Fill,
                ColumnStyles = { new ColumnStyle(SizeType.Percent, 50), new ColumnStyle(SizeType.Percent, 50) },
                RowStyles = { new RowStyle(SizeType.Percent, 100) },
            };
            _layoutPanel.SetCellPosition(blackTable, new TableLayoutPanelCellPosition(0, 0));
            Label blackLabel = new Label()
            {
                Parent = blackTable,
                Dock = DockStyle.Fill,
                Text = _players[1].Colour.ToString(),
            };
            blackTable.SetCellPosition(blackLabel, new TableLayoutPanelCellPosition(0, 0));

            _timerLabels[1] = new Label()
            {
                Parent = blackTable,
                Dock = DockStyle.Fill,
                Text = _players[1].RemainingTime.ToString(),
            };
            whiteTable.SetCellPosition(_timerLabels[1], new TableLayoutPanelCellPosition(1, 0));
        }

        private void OnSquareClicked(object sender, EventArgs e)
        {
            if (sender is Square square)
            {
                _clickedCoords = square.Coords;
                Console.WriteLine(_clickedCoords);
                _semaphoreClick.Release();
            }
        }

        private void OnPlayerTimerTick(object sender, ElapsedEventArgs e)
        {
            _currentPlayer.TickTime(new TimeSpan(0, 0, 1));
            Label timeLabel = _currentPlayer == _players[0] ? _timerLabels[0] : _timerLabels[1];
            timeLabel.Invoke((MethodInvoker)delegate
            {
                timeLabel.Text = _currentPlayer.RemainingTime.ToString();
            });

            if (TimeSpan.Compare(_currentPlayer.RemainingTime, new TimeSpan(0, 0, 0)) < 1)
            {
                _timer.Elapsed -= OnPlayerTimerTick;
                cts.Cancel();
            }
        }


        private void OnGameOver()
        {
            cts.Cancel();
            foreach (Square s in _board.GetSquares())
            {
                s.Click -= OnSquareClicked;
            }
            Player winner = null;
            if (_result == GameResult.Checkmate || _result == GameResult.Time)
            {
                winner = _currentPlayer == _players[0] ? _players[1] : _players[0];
            }
            MessageBox.Show(_result.ToString() + " ," + (winner != null ? winner.Colour.ToString() : "Nobody") + " Wins");
        }
    }
}

Board and Square classes

Board inherits TableLayoutPanel which displays the Board as a control. Each cell has a Square in it. Square inherits from Button and can have a piece in it.

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

namespace prjChessForms
{

    public struct Coords
    {
        public Coords(int x, int y)
        {
            X = x;
            Y = y;
        }
        public int X { get; }
        public int Y { get; }

        public override string ToString()
        {
            return Convert.ToString(Convert.ToChar(Convert.ToInt32('a') + X)) + Convert.ToString(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;
            }
        }

        public override int GetHashCode()
        {
            int hashCode = 367829482;
            hashCode = hashCode * -1521134295 + X.GetHashCode();
            hashCode = hashCode * -1521134295 + Y.GetHashCode();
            return hashCode;
        }
    }
    class Board : TableLayoutPanel
    {
        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();
            Display();
        }

        public void Display()
        {
            foreach (Square s in _squares)
            {
                s.Parent = this;
                SetCellPosition(s, new TableLayoutPanelCellPosition(s.Coords.X, RowCount - 1 - s.Coords.Y));
            }
        }

        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 ClearHighlights()
        {
            foreach (Square s in GetSquares())
            {
                s.ResetPanelColour();
            }
        }

        public void HighlightAt(Coords coords, Color highlightColour)
        {
            GetSquareAt(coords).BackColor = highlightColour;
        }

        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()
        {
            // Format
            Dock = DockStyle.Fill;
            Padding = new Padding(0);
            Margin = new Padding(0);

            ColumnCount = COL_COUNT;
            ColumnStyles.Clear();
            RowCount = ROW_COUNT;
            ColumnStyles.Clear();

            for (int c = 0; c < COL_COUNT; c++)
            {
                ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100 / COL_COUNT));
            }

            for (int r = 0; r < ROW_COUNT; r++)
            {
                RowStyles.Add(new RowStyle(SizeType.Percent, 100 / ROW_COUNT));
            }

            // Add squares
            _squares = new Square[ColumnCount, RowCount];
            for (int y = 0; y < ROW_COUNT; y++)
            {
                for (int x = 0; x < COL_COUNT; x++)
                {
                    _squares[x, y] = new Square(this, x, y);
                }
            }
            // Add pieces
            AddDefaultPieces();
        }
        private void AddDefaultPieces()
        {
            char[,] defaultPieces =
            {
                { 'P','P','P','P','P','P','P','P'},
                { 'R','N','B','Q','K','B','N','R'}
            };
            // Pieces
            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;
        }


    }
    class Square : Button
    {
        private Color _defaultPanelColour;
        private Piece _piece;
        public Square(Board board, int x, int y)
        {
            Parent = board;
            Coords = new Coords(x, y);
            _defaultPanelColour = (x + y) % 2 == 0 ? Color.SandyBrown : Color.LightGray;
            Piece = null;
            SetupSquare();
        }

        public Piece Piece
        {
            get
            {
                if (_piece != null && _piece.GetType() == typeof(GhostPawn))
                {
                    return null;
                }
                return _piece;
            }
            set
            {
                _piece = value;
                UpdateSquare();
            }
        }
        public Coords Coords { get; }

        public void ResetPanelColour()
        {
            BackColor = _defaultPanelColour;
        }

        public GhostPawn GetGhostPawn()
        {
            return (_piece != null && _piece.GetType() == typeof(GhostPawn)) ? (GhostPawn)_piece : null;
        }
        private void SetupSquare()
        {
            BackColor = _defaultPanelColour;
            Dock = DockStyle.Fill;
            UpdateSquare();
        }

        private void UpdateSquare()
        {
            Image = Piece != null ? Piece.Image : null;
        }

    }

}

Piece Class

Abstract Piece class is template for the other pieces Has a CanMove method which returns whether the piece can move from given start and end coordinates while ignoring complications of putting yourself in check etc.

using System;
using System.Drawing;

namespace prjChessForms
{
    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 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;
        }
    }
}

Rulebook

Static class which has methods which help with determining whether a move is allowed or not. Also can get the coordinates for where a piece is allowed to move to.

using System;
using System.Collections.Generic;

namespace prjChessForms
{
    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 void 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));
            }

            if (IsEnPassant(board, move))
            {
                GhostPawn ghostPawn = board.GetSquareAt(move.EndCoords).GetGhostPawn();
                Coords linkedPawnCoords = board.GetCoordsOfPiece(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);
            Promotions(board, move.EndCoords);



        }

        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 currentPlayer)
        {
            if (IsInStalemate(board, currentPlayer))
            {
                return GameResult.Stalemate;
            }
            else if (IsInCheckmate(board, currentPlayer))
            {
                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;
        }

        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;
        }


        private static void Promotions(Board board, Coords endCoords)
        {
            Piece p = board.GetPieceAt(endCoords);
            if (p.GetType() == typeof(Pawn)) 
            {
                if (endCoords.Y == 0 || endCoords.Y == board.RowCount - 1)
                {
                    board.GetSquareAt(endCoords).Piece = new Queen(p.Owner);
                }
            }
        }

    }
}

Player

Stores information about a player.

using System;
using System.Timers;
using System.Windows.Forms;

namespace prjChessForms
{
    abstract class Player
    {
        public Player(PieceColour colour, TimeSpan initialTime)
        {
            Colour = colour;
            RemainingTime = initialTime;
        }
        public TimeSpan RemainingTime { get; private set; }
        public PieceColour Colour { get; }

        public void TickTime(TimeSpan time)
        {
            RemainingTime = RemainingTime.Subtract(time);
        }

    }

    class HumanPlayer : Player
    {
        public HumanPlayer(PieceColour colour, TimeSpan initialTime) : base(colour, initialTime) { }
    }


    class ComputerPlayer : Player 
// Did not use this, but just put it in incase I wanted to add it later
    {
        public ComputerPlayer(PieceColour colour, TimeSpan initialTime) : base(colour, initialTime) { }

    }

}

Program

I don't know if you need this, it is just the default file really, but I've added it just in case.

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);
            Application.Run(new Chess());
        }
    }
}

Please take as much time as you need to criticize any aspects of my code to help me to improve my way of programming. I will try my best to answer any comments / questions if needed as soon as possible.

\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

Welcome to CodeReview! There is quite a lot to digest here. I have seen just a few things I would like to comment on. For such a fairly big post, you may not get one full answer but hopefully lots of little answers.

Overall, I like it. Both your post and your code is above average for a first time poster. Naming and style is decent. But there are still plenty of opportunities for improvement, or just areas with alternatives may be considered.

Coords Structure

This feels very wordy to me with so many explicit Convert calls:

public override string ToString()
{
    return Convert.ToString(Convert.ToChar(Convert.ToInt32('a') + X)) + Convert.ToString(Y + 1);
}

You should shorten it up with a string interpolation:

public override ToString() => $"{(char)('a' + X)}{Y + 1}";

That is shorter to read which makes it easier to follow IMO. My eyeballs can quickly scan the main thing without being distracted by 4 Convert calls.

Abstract Piece Class

I think its nice where you override ToString. Oddly, enough you do not do so with class abstract Piece. To me, and maybe its just me, but this class is just begging for these additions:

public string Name => GetType().Name;

public string Fullname => $"{Colour} {Name}";

public override ToString() => Name;

Now you can easily output "Pawn" or "White Pawn".

Chess Form

The private void OnPlayerTimerTick method has a line that could be better. Specifically, this conditional:

if (TimeSpan.Compare(_currentPlayer.RemainingTime, new TimeSpan(0, 0, 0)) < 1)

I think this is much easier to read and to understand the logic with:

if (_currentPlayer.RemainingTime > TimeSpan.Zero)

For your consideration

There is Principle of Separation of Concerns. To a good extent, you do this. Think of the 2 main chunks of your code. One is all related to the rules and objects for the game of Chess. That is the board, squares, pieces, and all the rules. Let's call this the Chess Logic Library.

The other main chunk is the Winform. Let's call this the Presentation UI.

Imagine if you were going to switch this to a Unity front-end UI. Or maybe a web frond-end. You would want to have the same Chess Logic Library used by the different Presentation UI's since the rules of chess would not change from Winform to web or Unity.

I offer this section purely as something for you to consider.

\$\endgroup\$
3
  • \$\begingroup\$ Thanks for the feedback! Since I'll (hopefully) get several small answers, which should I accept as the answer that solved my problem? \$\endgroup\$ Mar 4 at 7:36
  • 3
    \$\begingroup\$ @Shinglington I suggest waiting at least 3 more days for more answers to come. Many who answer here are not "point grubs", so few are offended if they do not get awarded points. If more answers come in, choose the one that you feel improves your code the most, or the answer where you learned the most. I think my answer only touched on some minor concerns, so I personally do not think it rises to the top. \$\endgroup\$
    – Rick Davin
    Mar 4 at 16:26
  • 1
    \$\begingroup\$ You should mention some design patterns when you discuss Separation of Concerns, not sure which would apply more, but MVVM and MVC are both applicable in this discussion. Provide links son the OP can do research. \$\endgroup\$
    – pacmaninbw
    Mar 6 at 17:17
2
\$\begingroup\$

General Observations

Here are some C# best practices you might want to follow:

  • Avoid writing long functions. The typical function should have max 40-50 lines of code. If method has more than 50 line of code, you must consider re factoring into separate private methods.
  • Don’t have number of classes in single file. Create a separate file for each class.
  • Always succeed the keywords if, else, do, while, for and foreach, with opening and closing parentheses, even though the language does not require it.
  • Avoid the use of var in place of dynamic.

You can always preserver the original code on GitHub by creating branches. Among other things, branches allows parallel development and bug fixing in the original code as well as new code.

As mentioned in the review by Rick Davin a best practice is to split the input and output (display) from the logic of the game. There are many design patterns to do this, one is Model View Viewmodel (MVVM), another is Model View Controller (MVC).

This is a personal preference but I would break up board.cs into 3 different files, one each for the Coords struct, the Board Class and the Square Class. While the Board Class depends on the Coords Struct and the Square Class neither Coords or Squares depend on the Board. Part of this personal preference is that I am an old C/C++ programmer and I prefer to keep files as simple as possible. This is also an extension of the Principle of Separation of Concerns mentioned by Rock Davin.

One major observation to make about good programming in general is that the problem is broken up into smaller and smaller pieces until each piece is easy to solve. This is handled by Object Oriented Design and Programming. You done a good job of creating the objects, but break unnecessary dependencies.

Inconsistent Coding

The vertical spacing in the files is inconsistent, sometimes there is no blank lines between methods / functions, sometimes there is 2 blank lines between methods but usually there is one blank line between methods. Always be consistent in your coding. My personal preference is one blank line, but as long as you are consistent it doesn't matter that much.

Unused Functions and Magic Numbers

The function GetHashCode() has no references which means it isn't used, why add unnecessary code.

        public override int GetHashCode()
        {
            int hashCode = 367829482;
            hashCode = hashCode * -1521134295 + X.GetHashCode();
            hashCode = hashCode * -1521134295 + Y.GetHashCode();
            return hashCode;
        }

It isn't clear what purpose of the above function was. It really isn't clear why -1521134295 was used in the code. A symbolic constant would have been helpful here.

Use the Resources

This is a comment on the GitHub repository, there are images used by the program, these images should not be in either the bin\Debug directory or the bin\Release directory since they are common images. If there is not a Microsoft Solution add an images folder / directory and use that.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you for the comments. I just have a few questions. Regarding my unused GetHashCode() override, I followed the guideline that when overriding Equals(), I should override GetHashCode(), from here. So I assume maybe this rule doesn't apply to my code because I am not using hash tables? \$\endgroup\$ Mar 6 at 18:42

Your Answer

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

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