├── .gitignore ├── ChessConsole.sln ├── ChessConsole ├── App.config ├── ChessBoard.cs ├── ChessConsole.csproj ├── ChessConsole.csproj.user ├── ChessGame.cs ├── ConsoleGraphics.cs ├── Direction.cs ├── Piece.cs ├── Pieces │ ├── Bishop.cs │ ├── King.cs │ ├── Knight.cs │ ├── Pawn.cs │ ├── Queen.cs │ └── Rook.cs ├── Program.cs └── Properties │ └── AssemblyInfo.cs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | bin/ 3 | obj/ -------------------------------------------------------------------------------- /ChessConsole.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2026 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChessConsole", "ChessConsole\ChessConsole.csproj", "{66F5E978-F5C6-4A48-A689-A6F1E22BE741}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {66F5E978-F5C6-4A48-A689-A6F1E22BE741}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {66F5E978-F5C6-4A48-A689-A6F1E22BE741}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {66F5E978-F5C6-4A48-A689-A6F1E22BE741}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {66F5E978-F5C6-4A48-A689-A6F1E22BE741}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {9C90AB71-4FB3-4E52-ADEE-1C2E6C5EFDDF} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /ChessConsole/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ChessConsole/ChessBoard.cs: -------------------------------------------------------------------------------- 1 | using ChessConsole.Pieces; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | 7 | namespace ChessConsole 8 | { 9 | /// 10 | /// Provides the data structure and algorithms for dealing with a chess board 11 | /// 12 | public class ChessBoard 13 | { 14 | /// 15 | /// Represents one chess cell in the 16 | /// 17 | public class Cell 18 | { 19 | /// 20 | /// The parent chess board 21 | /// 22 | public ChessBoard Parent 23 | { 24 | private set; 25 | get; 26 | } 27 | 28 | /// 29 | /// 0-7 -> A-H mapping on an actual chessboard 30 | /// 31 | public int X; 32 | /// 33 | /// 0-7 -> 1-8 mapping on an actual chessboard 34 | /// 35 | public int Y; 36 | 37 | /// 38 | /// The piece present at the cell (can be null). Should only be set by . 39 | /// 40 | public Piece Piece; 41 | 42 | /// 43 | /// All the pieces that can hit this cell. 44 | /// 45 | public List HitBy; 46 | 47 | public Cell(ChessBoard parent, int x, int y) 48 | { 49 | Parent = parent; 50 | HitBy = new List(); 51 | X = x; 52 | Y = y; 53 | } 54 | 55 | /// 56 | /// Returns cells on the board in a relative direction to this one. 57 | /// 58 | /// The X-direction component in which to search 59 | /// The Y-direction component in which to search 60 | /// The ammount of consecutive cells to return (until outside of the board) 61 | /// Collection of cells which are in the line of sight 62 | public IEnumerable OpenLineOfSight(int dirX, int dirY, int desiredCount = 1) 63 | { 64 | for (int i = 1; i <= desiredCount; i++) 65 | { 66 | //Query the parent for a cell, if null the cell is out of the board and we should stop 67 | Cell cell = Parent.GetCell(X + dirX * i, Y + dirY * i); 68 | if (cell == null) yield break; 69 | 70 | yield return cell; 71 | 72 | //Stop anyway as line of sight is blocked 73 | if (cell.Piece != null) 74 | yield break; 75 | } 76 | } 77 | 78 | /// 79 | /// Returns a cell on the board relative to this one. 80 | /// 81 | /// Relative X-coordinate of the cell 82 | /// Relative X-coordinate of the cell 83 | /// Node at (x, y) position or null if index is out of bounds 84 | public Cell Open(int x, int y) 85 | { 86 | //Query the parent for a cell, if null the cell is out of the board and we should not return 87 | Cell cell = Parent.GetCell(X + x, Y + y); 88 | return cell ?? null; 89 | } 90 | } 91 | 92 | /// 93 | /// Contains information about the cells and the links between them 94 | /// 95 | private Cell[,] cells; 96 | 97 | /// 98 | /// The cell to hit for en passant 99 | /// 100 | public Cell EnPassant 101 | { 102 | private set; 103 | get; 104 | } 105 | 106 | /// 107 | /// The cell where the pawn will be captured after en passant is performed 108 | /// 109 | public Cell EnPassantCapture 110 | 111 | { 112 | private set; 113 | get; 114 | } 115 | 116 | /// 117 | /// List holding all the existing pieces 118 | /// 119 | private List pieces = new List(); 120 | 121 | private Piece blackKing; 122 | private Piece whiteKing; 123 | 124 | /// 125 | /// Caches the method's result 126 | /// 127 | private bool inCheck; 128 | 129 | public ChessBoard() 130 | { 131 | Reset(); 132 | } 133 | 134 | #region Getters 135 | 136 | /// 137 | /// Get cell by absolute coordinates 138 | /// 139 | /// Absolute X-coord 140 | /// Absolute Y-coord 141 | /// Node at (x, y) position or null if index is out of bounds 142 | public Cell GetCell(int x, int y) 143 | { 144 | if (x < 0 || cells.GetLength(0) <= x || y < 0 || cells.GetLength(1) <= y) return null; 145 | 146 | return cells[x, y]; 147 | } 148 | 149 | #endregion 150 | 151 | #region HelperMethods 152 | 153 | /// 154 | /// Adds a piece in the beggining of the chess game, can also be used for promotion 155 | /// 156 | /// The cell to add to 157 | /// 158 | private void addPiece(Cell cell, Piece piece) 159 | { 160 | cell.Piece = piece; 161 | pieces.Add(piece); 162 | piece.OnPlace(cell); 163 | } 164 | 165 | #endregion 166 | 167 | #region InterfaceMethods 168 | 169 | /// 170 | /// Resets the board state 171 | /// 172 | public void Reset() 173 | { 174 | cells = new Cell[8, 8]; 175 | for (int i = 0; i < 8; i++) 176 | { 177 | for (int j = 0; j < 8; j++) 178 | { 179 | cells[i, j] = new Cell(this, i, j); 180 | } 181 | } 182 | 183 | pieces.Clear(); 184 | 185 | EnPassant = null; 186 | EnPassantCapture = null; 187 | 188 | addPiece(cells[0, 0], new Rook(PlayerColor.White)); 189 | addPiece(cells[1, 0], new Knight(PlayerColor.White)); 190 | addPiece(cells[2, 0], new Bishop(PlayerColor.White)); 191 | addPiece(cells[3, 0], new Queen(PlayerColor.White)); 192 | addPiece(cells[4, 0], (whiteKing = new King(PlayerColor.White))); 193 | addPiece(cells[5, 0], new Bishop(PlayerColor.White)); 194 | addPiece(cells[6, 0], new Knight(PlayerColor.White)); 195 | addPiece(cells[7, 0], new Rook(PlayerColor.White)); 196 | 197 | addPiece(cells[0, 1], new Pawn(PlayerColor.White)); 198 | addPiece(cells[1, 1], new Pawn(PlayerColor.White)); 199 | addPiece(cells[2, 1], new Pawn(PlayerColor.White)); 200 | addPiece(cells[3, 1], new Pawn(PlayerColor.White)); 201 | addPiece(cells[4, 1], new Pawn(PlayerColor.White)); 202 | addPiece(cells[5, 1], new Pawn(PlayerColor.White)); 203 | addPiece(cells[6, 1], new Pawn(PlayerColor.White)); 204 | addPiece(cells[7, 1], new Pawn(PlayerColor.White)); 205 | 206 | addPiece(cells[0, 6], new Pawn(PlayerColor.Black)); 207 | addPiece(cells[1, 6], new Pawn(PlayerColor.Black)); 208 | addPiece(cells[2, 6], new Pawn(PlayerColor.Black)); 209 | addPiece(cells[3, 6], new Pawn(PlayerColor.Black)); 210 | addPiece(cells[4, 6], new Pawn(PlayerColor.Black)); 211 | addPiece(cells[5, 6], new Pawn(PlayerColor.Black)); 212 | addPiece(cells[6, 6], new Pawn(PlayerColor.Black)); 213 | addPiece(cells[7, 6], new Pawn(PlayerColor.Black)); 214 | 215 | addPiece(cells[0, 7], new Rook(PlayerColor.Black)); 216 | addPiece(cells[1, 7], new Knight(PlayerColor.Black)); 217 | addPiece(cells[2, 7], new Bishop(PlayerColor.Black)); 218 | addPiece(cells[3, 7], new Queen(PlayerColor.Black)); 219 | addPiece(cells[4, 7], (blackKing = new King(PlayerColor.Black))); 220 | addPiece(cells[5, 7], new Bishop(PlayerColor.Black)); 221 | addPiece(cells[6, 7], new Knight(PlayerColor.Black)); 222 | addPiece(cells[7, 7], new Rook(PlayerColor.Black)); 223 | 224 | foreach (Piece piece in pieces) 225 | { 226 | piece.Recalculate(); 227 | } 228 | } 229 | 230 | /// 231 | /// Called at the start of every turn. Recalcualtes legal moves. 232 | /// 233 | /// The player whose turn is to move 234 | /// Whether the player had any moves 235 | public bool TurnStart(PlayerColor currentPlayer) 236 | { 237 | inCheck = IsInCheck(currentPlayer, false); 238 | bool anyLegalMove = false; 239 | //Clear cell hit lists 240 | foreach (Cell cell in cells) 241 | { 242 | cell.HitBy.Clear(); 243 | } 244 | 245 | //Recalculate possible moves and hit lists for cells 246 | foreach (Piece piece in pieces) 247 | { 248 | piece.Recalculate(); 249 | } 250 | 251 | //Calculate legal moves 252 | foreach (Piece piece in pieces) 253 | { 254 | piece.LegalMoves.Clear(); 255 | foreach (Cell move in piece.PossibleMoves) 256 | { 257 | if (piece.Color == currentPlayer && isMoveLegal(piece, move)) 258 | { 259 | piece.LegalMoves.Add(move); 260 | anyLegalMove = true; 261 | } 262 | } 263 | } 264 | 265 | return anyLegalMove; 266 | } 267 | 268 | /// 269 | /// Validates if a move is legal for a given piece 270 | /// 271 | /// Piece to move 272 | /// Where the piece moves 273 | /// 274 | private bool isMoveLegal(Piece piece, Cell move) 275 | { 276 | Piece currentKing = piece.Color == PlayerColor.White ? whiteKing : blackKing; 277 | //The strategy is to try everything that can fail and return true only if nothing fails 278 | 279 | //If it's the king check if it moved into a check (or didn't move out that's really the same thing) 280 | if (piece is King) 281 | { 282 | //If some enemy hits where we move we can't move with the king 283 | foreach (Piece hitter in move.HitBy) 284 | { 285 | if (hitter.Parent != move && hitter.Color != piece.Color) 286 | return false; 287 | } 288 | 289 | //Validate castling 290 | if (Math.Abs(move.X - piece.Parent.X) == 2) 291 | { 292 | //You can't castle in check 293 | if (inCheck) 294 | return false; 295 | 296 | //Check if some enemy hits the middle castling 297 | foreach (Piece hitter in GetCell(move.X > piece.Parent.X ? move.X - 1 : move.X + 1, move.Y).HitBy) 298 | { 299 | if (hitter.Color != piece.Color) 300 | return false; 301 | } 302 | } 303 | } 304 | else //Non-king pieces 305 | { 306 | if (inCheck) //If player is in in check and if move resolves that 307 | { 308 | //Let's try capturing or blocking the attacker, keep in mind that we can't unblock another attacker 309 | foreach (Piece hitter in currentKing.Parent.HitBy) 310 | { 311 | if (hitter.Color == currentKing.Color) continue; //Same color don't care 312 | if (hitter.Parent == move) continue; //Was captured 313 | if (hitter.IsBlockedIfMove(piece.Parent, move, currentKing.Parent)) continue; 314 | 315 | return false; 316 | } 317 | } 318 | 319 | //Check if a blocker moving away results in a check 320 | //This also prevents pieces capturing an attacker and exposing the king to another 321 | foreach (Piece hitter in piece.Parent.HitBy) 322 | { 323 | if (hitter.Color == currentKing.Color) continue; //If it's the same color we don't care 324 | if (hitter.Parent == move) continue; //If we hit it it can not block 325 | 326 | if (!hitter.IsBlockedIfMove(piece.Parent, move, currentKing.Parent)) 327 | return false; 328 | } 329 | } 330 | 331 | 332 | return true; 333 | } 334 | 335 | /// 336 | /// Checks if a player is currently in check (their king can be hit) 337 | /// 338 | /// The player's color to check 339 | /// Uses the cached value for the current turn 340 | /// 341 | public bool IsInCheck(PlayerColor player, bool useCache = true) 342 | { 343 | if (useCache) 344 | return inCheck; 345 | 346 | if (player == PlayerColor.White) 347 | return whiteKing.Parent.HitBy.Any(hitter => hitter.Color != player); 348 | else 349 | return blackKing.Parent.HitBy.Any(hitter => hitter.Color != player); 350 | } 351 | 352 | /// 353 | /// Move a piece from one cell the the other, after this function is called the turn MUST end 354 | /// 355 | /// Node where the moved piece is 356 | /// Node to move to 357 | /// The option chosed when promoting a pawn, will be ignored if the movement does not involve pormotion. 358 | public void Move(Cell from, Cell to, PromoteOptions promoteOption) 359 | { 360 | //Capture a piece if moved on it 361 | if (to.Piece != null) 362 | pieces.Remove(to.Piece); 363 | 364 | to.Piece = from.Piece; 365 | from.Piece = null; 366 | 367 | //Handles en passant captures 368 | if (to == EnPassant && to.Piece is Pawn) 369 | { 370 | pieces.Remove(EnPassantCapture.Piece); 371 | EnPassantCapture.Piece = null; 372 | } 373 | 374 | //Castling to the right 375 | if (to.Piece is King && to.X - from.X == 2) 376 | { 377 | Move(GetCell(7, to.Y), GetCell(to.X - 1, to.Y), promoteOption); //Move the rook as well 378 | } 379 | 380 | //Castling to the left 381 | if (to.Piece is King && to.X - from.X == -2) 382 | { 383 | Move(GetCell(0, to.Y), GetCell(to.X + 1, to.Y), promoteOption); //Move the rook as well 384 | } 385 | 386 | //Handles promotion 387 | if (to.Piece is Pawn && to.Y == (to.Piece.Color == PlayerColor.White ? 7 : 0)) 388 | { 389 | Piece promoted = null; //we have to set it to null cuz C# complains 390 | switch (promoteOption) 391 | { 392 | case PromoteOptions.Queen: 393 | promoted = new Queen(to.Piece); 394 | break; 395 | case PromoteOptions.Rook: 396 | promoted = new Rook(to.Piece); 397 | break; 398 | case PromoteOptions.Bishop: 399 | promoted = new Bishop(to.Piece); 400 | break; 401 | case PromoteOptions.Knight: 402 | promoted = new Knight(to.Piece); 403 | break; 404 | } 405 | 406 | //Update the list with the new promoted piece 407 | pieces.Remove(to.Piece); 408 | to.Piece = promoted; 409 | promoted.OnPlace(to); //Place it otherwise weird bugs occur 410 | pieces.Add(promoted); 411 | } 412 | 413 | //The code has to be in this exact order to prevent from listeners firing when we move into our own listened cells. 414 | //Recalculate possible moves 415 | to.Piece.OnMove(to); 416 | to.Piece.Recalculate(); 417 | 418 | //Resets en passant 419 | EnPassant = null; 420 | EnPassantCapture = null; 421 | 422 | //Handles en passant detection 423 | if (to.Piece is Pawn && Math.Abs(to.Y - from.Y) == 2) 424 | { 425 | EnPassant = GetCell(to.X, (from.Y > to.Y) ? from.Y - 1 : from.Y + 1); 426 | EnPassantCapture = to; 427 | } 428 | } 429 | 430 | /// 431 | /// Is a piece (usually a pawn) promotable if it's moving from a cell to another 432 | /// 433 | /// The cell where the piece moves from 434 | /// The cell where the piece moves to 435 | /// Whether the piece on from is promotable 436 | public bool IsPromotable(Cell from, Cell to) 437 | { 438 | return from.Piece is Pawn && to.Y == (from.Piece.Color == PlayerColor.White ? 7 : 0); 439 | } 440 | 441 | #endregion 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /ChessConsole/ChessConsole.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {66F5E978-F5C6-4A48-A689-A6F1E22BE741} 8 | Exe 9 | ChessConsole 10 | ChessConsole 11 | v4.6.1 12 | 512 13 | true 14 | 15 | 16 | AnyCPU 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | AnyCPU 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /ChessConsole/ChessConsole.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | ShowAllFiles 5 | 6 | -------------------------------------------------------------------------------- /ChessConsole/ChessGame.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace ChessConsole 5 | { 6 | public enum PlayerColor 7 | { 8 | White, Black 9 | } 10 | 11 | public enum PlayerState 12 | { 13 | Idle, Holding, AwaitPromote, GameOver 14 | } 15 | 16 | public enum PromoteOptions 17 | { 18 | Queen = 0, Rook = 1, Bishop = 2, Knight = 3 19 | } 20 | 21 | public class ChessGame 22 | { 23 | /// 24 | /// False indicates the game should exit 25 | /// 26 | public bool Running 27 | { 28 | private set; 29 | get; 30 | } 31 | 32 | private PlayerState playerState; 33 | 34 | /// 35 | /// Currently selected promote option 36 | /// 37 | private PromoteOptions promoteOption; 38 | 39 | /// 40 | /// True for white, false for black 41 | /// 42 | private PlayerColor currentPlayer; 43 | 44 | /// 45 | /// Coordinates for the virtual cursor on the board 46 | /// 47 | private int cursorX, cursorY; 48 | 49 | /// 50 | /// The actual chess board 51 | /// 52 | private ChessBoard board; 53 | 54 | /// 55 | /// Currently holded piece's parent cell 56 | /// 57 | private ChessBoard.Cell holdedNode = null; 58 | 59 | /// 60 | /// Where to move 61 | /// 62 | private ChessBoard.Cell moveTo = null; 63 | 64 | public ChessGame() 65 | { 66 | Running = true; 67 | board = new ChessBoard(); 68 | currentPlayer = PlayerColor.White; 69 | turnStart(); 70 | } 71 | 72 | #region PublicInterfaceCommands 73 | public void Update() 74 | { 75 | if (Console.KeyAvailable) 76 | { 77 | ConsoleKeyInfo keyInfo = Console.ReadKey(true); 78 | 79 | if (keyInfo.Key == ConsoleKey.LeftArrow && cursorX > 0 && playerState != PlayerState.AwaitPromote) 80 | cursorX--; 81 | else if (keyInfo.Key == ConsoleKey.RightArrow && cursorX < 7 && playerState != PlayerState.AwaitPromote) 82 | cursorX++; 83 | else if (keyInfo.Key == ConsoleKey.UpArrow) 84 | { 85 | if (playerState != PlayerState.AwaitPromote && cursorY < 7) 86 | cursorY++; 87 | else if ((int)promoteOption > 0) 88 | promoteOption--; 89 | } 90 | else if (keyInfo.Key == ConsoleKey.DownArrow) 91 | { 92 | if (playerState != PlayerState.AwaitPromote && cursorY > 0) 93 | cursorY--; 94 | else if ((int)promoteOption < 3) 95 | promoteOption++; 96 | } 97 | else if (keyInfo.Key == ConsoleKey.Enter) 98 | interact(); 99 | else if (keyInfo.Key == ConsoleKey.D) 100 | debugInteract(); 101 | else if (keyInfo.Key == ConsoleKey.Escape) 102 | cancel(); 103 | } 104 | } 105 | 106 | /// 107 | /// Draws the game 108 | /// 109 | /// ConsoleGraphics object to draw with/to 110 | public void Draw(ConsoleGraphics g) 111 | { 112 | g.FillArea(new CChar(' ', ConsoleColor.Black, ConsoleColor.DarkGray), 10, 5, 8, 8); 113 | 114 | //7-j everywhere cuz it's reversed in chess 115 | for (int i = 0; i < 8; i++) 116 | { 117 | for (int j = 0; j < 8; j++) 118 | { 119 | //Draw the symbol 120 | ChessBoard.Cell cell = board.GetCell(i, j); 121 | if (cell.Piece != null) 122 | { 123 | g.DrawTransparent(cell.Piece.Char, (cell.Piece.Color == PlayerColor.White) ? ConsoleColor.White : ConsoleColor.Black, 10 + i, 5 + (7 - j)); 124 | if (cell.Piece.LegalMoves.Count == 0) 125 | { 126 | g.SetBackground(ConsoleColor.DarkRed, 10 + i, 5 + (7 - j)); 127 | } 128 | } 129 | 130 | if (cell.HitBy.Contains(debugPiece)) 131 | g.SetBackground(ConsoleColor.DarkMagenta, 10 + i, 5 + (7 - j)); 132 | } 133 | } 134 | 135 | if (holdedNode != null && playerState == PlayerState.Holding) 136 | { 137 | //Highlight legal moves 138 | foreach (ChessBoard.Cell move in holdedNode.Piece.LegalMoves) 139 | { 140 | g.SetBackground(ConsoleColor.DarkGreen, 10 + move.X, 5 + (7 - move.Y)); 141 | } 142 | } 143 | 144 | //Sets the cursor color -> yellow 145 | g.SetBackground(ConsoleColor.DarkYellow, 10 + cursorX, 5 + (7 - cursorY)); 146 | 147 | //TODO: Remove en passant testing 148 | /*if (board.EnPassant != null) 149 | g.SetBackground(ConsoleColor.DarkCyan, 10 + board.EnPassant.X, 5 + (7 - board.EnPassant.Y)); 150 | 151 | if (board.EnPassantCapture != null) 152 | g.SetBackground(ConsoleColor.DarkMagenta, 10 + board.EnPassantCapture.X, 5 + (7 - board.EnPassantCapture.Y));*/ 153 | 154 | //Lighten for checkerboard pattern 155 | for (int i = 0; i < 8; i++) 156 | { 157 | for (int j = 0; j < 8; j++) 158 | { 159 | if ((i + j) % 2 == 1) g.LightenBackground(10 + i, 5 + j); 160 | } 161 | } 162 | 163 | //Promotion option menu 164 | 165 | if (playerState == PlayerState.AwaitPromote) 166 | { 167 | g.DrawTextTrasparent("Queen", promoteOption == PromoteOptions.Queen ? ConsoleColor.Yellow : ConsoleColor.White, 22, 7); 168 | g.DrawTextTrasparent("Rook", promoteOption == PromoteOptions.Rook ? ConsoleColor.Yellow : ConsoleColor.White, 22, 9); 169 | g.DrawTextTrasparent("Bishop", promoteOption == PromoteOptions.Bishop ? ConsoleColor.Yellow : ConsoleColor.White, 22, 11); 170 | g.DrawTextTrasparent("Knight", promoteOption == PromoteOptions.Knight ? ConsoleColor.Yellow : ConsoleColor.White, 22, 13); 171 | } 172 | else 173 | { 174 | g.ClearArea(22, 7, 6, 7); 175 | } 176 | } 177 | 178 | #endregion 179 | 180 | #region EventHandlerLikeMethods 181 | 182 | /// 183 | /// Happens when the user presses the enter key 184 | /// 185 | private void interact() 186 | { 187 | switch (playerState) 188 | { 189 | case PlayerState.Idle: 190 | holdedNode = board.GetCell(cursorX, cursorY); 191 | 192 | if (holdedNode.Piece == null || holdedNode.Piece.Color != currentPlayer || holdedNode.Piece.LegalMoves.Count == 0) 193 | { 194 | holdedNode = null; 195 | return; 196 | } 197 | else playerState = PlayerState.Holding; 198 | 199 | 200 | break; 201 | case PlayerState.Holding: 202 | playerState = PlayerState.Holding; 203 | 204 | moveTo = board.GetCell(cursorX, cursorY); 205 | 206 | if (!holdedNode.Piece.LegalMoves.Contains(moveTo)) 207 | { 208 | moveTo = null; 209 | return; 210 | } 211 | 212 | if (board.IsPromotable(holdedNode, moveTo)) 213 | showPromote(); 214 | else 215 | turnOver(); 216 | 217 | break; 218 | case PlayerState.AwaitPromote: 219 | turnOver(); 220 | break; 221 | case PlayerState.GameOver: 222 | Running = false; 223 | break; 224 | } 225 | } 226 | 227 | 228 | private Piece debugPiece; 229 | private void debugInteract() 230 | { 231 | debugPiece = board.GetCell(cursorX, cursorY).Piece; 232 | } 233 | 234 | /// 235 | /// Happens when the user presses the escape key 236 | /// 237 | private void cancel() 238 | { 239 | playerState = PlayerState.Idle; 240 | holdedNode = null; 241 | } 242 | 243 | #endregion 244 | 245 | #region EventLikeMethods 246 | /// 247 | /// Called on every turn start 248 | /// 249 | private void turnStart() 250 | { 251 | board.TurnStart(currentPlayer); 252 | } 253 | 254 | /// 255 | /// Shows promotion dialog (set's the state) 256 | /// 257 | private void showPromote() 258 | { 259 | playerState = PlayerState.AwaitPromote; 260 | promoteOption = PromoteOptions.Queen; //reset the menu 261 | } 262 | 263 | /// 264 | /// Called when the turn is passed to the other player 265 | /// 266 | private void turnOver() 267 | { 268 | board.Move(holdedNode, moveTo, promoteOption); 269 | holdedNode = null; 270 | moveTo = null; 271 | playerState = PlayerState.Idle; 272 | currentPlayer = currentPlayer == PlayerColor.White ? PlayerColor.Black : PlayerColor.White; 273 | turnStart(); 274 | } 275 | #endregion 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /ChessConsole/ConsoleGraphics.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ChessConsole 4 | { 5 | /// 6 | /// Colored console character for the and 7 | /// 8 | public struct CChar : IEquatable 9 | { 10 | public ConsoleColor Foreground; 11 | public ConsoleColor Background; 12 | /// 13 | /// Actual character value 14 | /// 15 | public char C; 16 | 17 | public CChar(char c = ' ', ConsoleColor foreground = ConsoleColor.White, ConsoleColor background = ConsoleColor.Black) 18 | { 19 | Foreground = foreground; 20 | Background = background; 21 | C = c; 22 | } 23 | 24 | public bool Equals(CChar other) 25 | { 26 | return other.Foreground == Foreground && other.Background == Background && other.C == C; 27 | } 28 | 29 | public static bool operator==(CChar lhs, CChar rhs) 30 | { 31 | return lhs.Equals(rhs); 32 | } 33 | 34 | public static bool operator!=(CChar lhs, CChar rhs) 35 | { 36 | return !lhs.Equals(rhs); 37 | } 38 | } 39 | 40 | /// 41 | /// Handles double buffered C# console 42 | /// 43 | public class ConsoleGraphics 44 | { 45 | /// 46 | /// First everything is drawn to the back buffer for double buffering purposes 47 | /// you can "swap" buffers with 48 | /// 49 | private CChar[,] backBuffer; 50 | 51 | /// 52 | /// Current console's colored character buffer 53 | /// you can "swap" buffers with 54 | /// 55 | private CChar[,] frontBuffer; 56 | 57 | public ConsoleGraphics() 58 | { 59 | backBuffer = new CChar[Console.BufferWidth, Console.BufferHeight]; 60 | frontBuffer = new CChar[Console.BufferWidth, Console.BufferHeight]; 61 | } 62 | 63 | 64 | 65 | #region DrawMethods 66 | 67 | /// 68 | /// Clears the back buffer, next SwapBuffer 69 | /// 70 | public void Clear() 71 | { 72 | for (int i = 0; i < backBuffer.GetLength(0); i++) 73 | { 74 | for (int j = 0; j < backBuffer.GetLength(1); j++) 75 | { 76 | backBuffer[i, j] = new CChar(); 77 | } 78 | } 79 | } 80 | 81 | /// 82 | /// Draws a colored character to the back buffer 83 | /// 84 | /// The colored character to draw 85 | /// X-coord in console buffer 86 | /// Y-coord in console buffer 87 | public void Draw(CChar cchar, int x, int y) 88 | { 89 | backBuffer[x, y] = cchar; 90 | } 91 | 92 | /// 93 | /// Draws a colored character to the back buffer, it doesn't change background color 94 | /// 95 | /// The character to draw 96 | /// Color of the character 97 | /// X-coord in console buffer 98 | /// Y-coord in console buffer 99 | public void DrawTransparent(char c, ConsoleColor foreground, int x, int y) 100 | { 101 | backBuffer[x, y].C = c; 102 | backBuffer[x, y].Foreground = foreground; 103 | } 104 | 105 | /// 106 | /// Draws an area of colored characters to the back buffer. 107 | /// The arrays length is used as area width and height. 108 | /// 109 | /// The colored character area to draw 110 | /// Starting X-coord in console buffer 111 | /// Starting Y-coord in console buffer 112 | public void DrawArea(CChar[,] cchars, int x, int y) 113 | { 114 | for (int i = 0; i < cchars.GetLength(0); i++) 115 | { 116 | for (int j = 0; j < cchars.GetLength(1); j++) 117 | { 118 | backBuffer[x + i, y + j] = cchars[i, j]; 119 | } 120 | } 121 | } 122 | 123 | /// 124 | /// Draws text to the screen. Multiline is not handled. 125 | /// 126 | /// The text to draw 127 | /// Foreground color of text 128 | /// Background color of text 129 | /// Starting X-coord in console buffer 130 | /// Starting Y-coord in console buffer 131 | public void DrawText(string text, ConsoleColor foreground, ConsoleColor background, int x, int y) 132 | { 133 | CChar[,] area = new CChar[text.Length, 1]; 134 | for (int i = 0; i < text.Length; i++) 135 | { 136 | area[i, 0] = new CChar(text[i], foreground, background); 137 | } 138 | 139 | DrawArea(area, x, y); 140 | } 141 | 142 | /// 143 | /// Draws text to the screen with a transparent background. Multiline is not handled. 144 | /// 145 | /// The text to draw 146 | /// Foreground color of text 147 | /// Starting X-coord in console buffer 148 | /// Starting Y-coord in console buffer 149 | public void DrawTextTrasparent(string text, ConsoleColor foreground, int x, int y) 150 | { 151 | CChar[,] area = new CChar[text.Length, 1]; 152 | for (int i = 0; i < text.Length; i++) 153 | { 154 | area[i, 0] = new CChar(text[i], foreground, backBuffer[x + i, y].Background); 155 | } 156 | 157 | DrawArea(area, x, y); 158 | } 159 | 160 | /// 161 | /// Fills an area of the back buffer with one specic colored character 162 | /// The arrays length is used as area width and height. 163 | /// 164 | /// The colored character area to draw 165 | /// Starting X-coord in console buffer 166 | /// Starting Y-coord in console buffer 167 | /// Width of the area 168 | /// Height of the area 169 | public void FillArea(CChar cchar, int x, int y, int width, int height) 170 | { 171 | for (int i = 0; i < width; i++) 172 | { 173 | for (int j = 0; j < height; j++) 174 | { 175 | backBuffer[x + i, y + j] = cchar; 176 | } 177 | } 178 | } 179 | 180 | /// 181 | /// Clears area on the screen 182 | /// 183 | /// Starting X-coord in console buffer 184 | /// Starting Y-coord in console buffer 185 | /// Width of the area 186 | /// Height of the area 187 | public void ClearArea(int x, int y, int width, int height) 188 | { 189 | for (int i = 0; i < width; i++) 190 | { 191 | for (int j = 0; j < height; j++) 192 | { 193 | backBuffer[x + i, y + j] = new CChar(); 194 | } 195 | } 196 | } 197 | 198 | #endregion 199 | 200 | #region Darken_Lighten 201 | 202 | /// 203 | /// Darkens the background color of a colored character in the back buffer. 204 | /// If background color is already dark or no dark version exists it leaves it unchanged. 205 | /// 206 | /// X-coord in console buffer 207 | /// Y-coord in console buffer 208 | public void DarkenBackground(int x, int y) 209 | { 210 | switch (backBuffer[x, y].Background) 211 | { 212 | case ConsoleColor.Blue: 213 | backBuffer[x, y].Background = ConsoleColor.DarkBlue; 214 | break; 215 | case ConsoleColor.Green: 216 | backBuffer[x, y].Background = ConsoleColor.DarkGreen; 217 | break; 218 | case ConsoleColor.Yellow: 219 | backBuffer[x, y].Background = ConsoleColor.DarkYellow; 220 | break; 221 | case ConsoleColor.Magenta: 222 | backBuffer[x, y].Background = ConsoleColor.DarkMagenta; 223 | break; 224 | case ConsoleColor.Gray: 225 | backBuffer[x, y].Background = ConsoleColor.DarkGray; 226 | break; 227 | case ConsoleColor.Cyan: 228 | backBuffer[x, y].Background = ConsoleColor.DarkCyan; 229 | break; 230 | case ConsoleColor.Red: 231 | backBuffer[x, y].Background = ConsoleColor.DarkRed; 232 | break; 233 | } 234 | } 235 | 236 | /// 237 | /// Darkens the foreground color of a colored character in the back buffer. 238 | /// If foreground color is already dark or no dark version exists it leaves it unchanged. 239 | /// 240 | /// X-coord in console buffer 241 | /// Y-coord in console buffer 242 | public void DarkenForeground(int x, int y) 243 | { 244 | switch (backBuffer[x, y].Foreground) 245 | { 246 | case ConsoleColor.Blue: 247 | backBuffer[x, y].Foreground = ConsoleColor.DarkBlue; 248 | break; 249 | case ConsoleColor.Green: 250 | backBuffer[x, y].Foreground = ConsoleColor.DarkGreen; 251 | break; 252 | case ConsoleColor.Yellow: 253 | backBuffer[x, y].Foreground = ConsoleColor.DarkYellow; 254 | break; 255 | case ConsoleColor.Magenta: 256 | backBuffer[x, y].Foreground = ConsoleColor.DarkMagenta; 257 | break; 258 | case ConsoleColor.Gray: 259 | backBuffer[x, y].Foreground = ConsoleColor.DarkGray; 260 | break; 261 | case ConsoleColor.Cyan: 262 | backBuffer[x, y].Foreground = ConsoleColor.DarkCyan; 263 | break; 264 | case ConsoleColor.Red: 265 | backBuffer[x, y].Foreground = ConsoleColor.DarkRed; 266 | break; 267 | } 268 | } 269 | 270 | /// 271 | /// Lightens the background color of a colored character in the back buffer. 272 | /// If background color is already light or no light version exists it leaves it unchanged. 273 | /// 274 | /// X-coord in console buffer 275 | /// Y-coord in console buffer 276 | public void LightenBackground(int x, int y) 277 | { 278 | switch (backBuffer[x, y].Background) 279 | { 280 | case ConsoleColor.DarkBlue: 281 | backBuffer[x, y].Background = ConsoleColor.Blue; 282 | break; 283 | case ConsoleColor.DarkGreen: 284 | backBuffer[x, y].Background = ConsoleColor.Green; 285 | break; 286 | case ConsoleColor.DarkYellow: 287 | backBuffer[x, y].Background = ConsoleColor.Yellow; 288 | break; 289 | case ConsoleColor.DarkMagenta: 290 | backBuffer[x, y].Background = ConsoleColor.Magenta; 291 | break; 292 | case ConsoleColor.DarkGray: 293 | backBuffer[x, y].Background = ConsoleColor.Gray; 294 | break; 295 | case ConsoleColor.DarkCyan: 296 | backBuffer[x, y].Background = ConsoleColor.Cyan; 297 | break; 298 | case ConsoleColor.DarkRed: 299 | backBuffer[x, y].Background = ConsoleColor.Red; 300 | break; 301 | } 302 | } 303 | 304 | /// 305 | /// Lightens the foreground color of a colored character in the back buffer. 306 | /// If foreground color is already light or no light version exists it leaves it unchanged. 307 | /// 308 | /// X-coord in console buffer 309 | /// Y-coord in console buffer 310 | public void LightenForeground(int x, int y) 311 | { 312 | switch (backBuffer[x, y].Foreground) 313 | { 314 | case ConsoleColor.DarkBlue: 315 | backBuffer[x, y].Foreground = ConsoleColor.Blue; 316 | break; 317 | case ConsoleColor.DarkGreen: 318 | backBuffer[x, y].Foreground = ConsoleColor.Green; 319 | break; 320 | case ConsoleColor.DarkYellow: 321 | backBuffer[x, y].Foreground = ConsoleColor.Yellow; 322 | break; 323 | case ConsoleColor.DarkMagenta: 324 | backBuffer[x, y].Foreground = ConsoleColor.Magenta; 325 | break; 326 | case ConsoleColor.DarkGray: 327 | backBuffer[x, y].Foreground = ConsoleColor.Gray; 328 | break; 329 | case ConsoleColor.DarkCyan: 330 | backBuffer[x, y].Foreground = ConsoleColor.Cyan; 331 | break; 332 | case ConsoleColor.DarkRed: 333 | backBuffer[x, y].Foreground = ConsoleColor.Red; 334 | break; 335 | } 336 | } 337 | 338 | #endregion 339 | 340 | #region Color Getters/Setters 341 | /// 342 | /// Sets the background color of the back buffer at (x, y) 343 | /// 344 | /// New background color 345 | /// X-coord in console buffer 346 | /// Y-coord in console buffer 347 | public void SetBackground(ConsoleColor color, int x, int y) 348 | { 349 | backBuffer[x, y].Background = color; 350 | } 351 | 352 | /// 353 | /// Gets the background color of the back buffer at (x, y) 354 | /// 355 | /// X-coord in console buffer 356 | /// Y-coord in console buffer 357 | /// Background color at (x, y) 358 | public ConsoleColor GetBackground(int x, int y) 359 | { 360 | return backBuffer[x, y].Background; 361 | } 362 | 363 | /// 364 | /// Sets the foreground color of the back buffer at (x, y) 365 | /// 366 | /// New foreground color 367 | /// X-coord in console buffer 368 | /// Y-coord in console buffer 369 | public void SetForeground(ConsoleColor color, int x, int y) 370 | { 371 | backBuffer[x, y].Foreground = color; 372 | } 373 | 374 | /// 375 | /// Gets the foreground color of the back buffer at (x, y) 376 | /// 377 | /// X-coord in console buffer 378 | /// Y-coord in console buffer 379 | /// Foreground color at (x, y) 380 | public ConsoleColor GetForeground(int x, int y) 381 | { 382 | return backBuffer[x, y].Foreground; 383 | } 384 | #endregion 385 | 386 | /// 387 | /// Overwrites the FrontBuffer and redraws the character if it's different from the BackBuffer 388 | /// 389 | public void SwapBuffers() 390 | { 391 | for (int i = 0; i < backBuffer.GetLength(0); i++) 392 | { 393 | for (int j = 0; j < backBuffer.GetLength(1); j++) 394 | { 395 | if (frontBuffer[i, j] != backBuffer[i, j]) 396 | { 397 | Console.SetCursorPosition(i, j); 398 | Console.ForegroundColor = backBuffer[i, j].Foreground; 399 | Console.BackgroundColor = backBuffer[i, j].Background; 400 | Console.Write(backBuffer[i, j].C); 401 | frontBuffer[i, j] = backBuffer[i, j]; 402 | } 403 | } 404 | } 405 | } 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /ChessConsole/Direction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace ChessConsole 6 | { 7 | /// 8 | /// Contains possible moves and handles line of sight checks 9 | /// 10 | public class Direction 11 | { 12 | /// 13 | /// The piece whose moves are represented by this object 14 | /// 15 | public Piece Piece 16 | { 17 | private set; 18 | get; 19 | } 20 | 21 | /// 22 | /// The X-direction 23 | /// 24 | public int X 25 | { 26 | private set; 27 | get; 28 | } 29 | 30 | /// 31 | /// The Y-direction 32 | /// 33 | public int Y 34 | { 35 | private set; 36 | get; 37 | } 38 | 39 | /// 40 | /// The possible moves you can make in this direction including the line of sight blocking piece that may or may not be hittable. 41 | /// See also 42 | /// 43 | private List possibleMoves; 44 | 45 | /// 46 | /// The possible moves you can make in this direction 47 | /// 48 | /// Are the enemy pieces hittable 49 | /// An enumeration of possible moves 50 | public IEnumerable GetPossibleMoves(bool enemyHittable = true) 51 | { 52 | if (possibleMoves.Count == 0) 53 | yield break; 54 | 55 | for (int i = 0; i < possibleMoves.Count - 1; i++) 56 | { 57 | yield return possibleMoves[i]; 58 | } 59 | 60 | if (possibleMoves.Last().Piece == null) 61 | yield return possibleMoves.Last(); 62 | else if (enemyHittable && possibleMoves.Last().Piece.Color != Piece.Color) 63 | yield return possibleMoves.Last(); 64 | } 65 | 66 | /// 67 | /// The count of possible moves 68 | /// 69 | /// Are the enemy pieces hittable 70 | /// The count of possible moves 71 | public int GetPossibleMoveCount(bool enemyHittable = true) 72 | { 73 | if (possibleMoves.Count == 0) 74 | return 0; 75 | 76 | if (possibleMoves.Last().Piece == null) 77 | return possibleMoves.Count; 78 | else if (!enemyHittable || possibleMoves.Last().Piece.Color == Piece.Color) 79 | return possibleMoves.Count - 1; 80 | else 81 | return possibleMoves.Count; 82 | } 83 | 84 | /// 85 | /// The number of moves that we could take, considering no blocking or out of board. 86 | /// 87 | public int DesiredCount 88 | { 89 | private set; 90 | get; 91 | } 92 | 93 | /// 94 | /// Tells if the direction should update the hit graph of possible move cells 95 | /// 96 | private bool updateHitGraph; 97 | 98 | public Direction(Piece piece, int x, int y, int desiredCount = 8, bool updateHitGraph = true) 99 | { 100 | Piece = piece; 101 | X = x; 102 | Y = y; 103 | DesiredCount = desiredCount; 104 | this.updateHitGraph = updateHitGraph; 105 | 106 | possibleMoves = new List(); 107 | possibleMoves.AddRange(piece.Parent.OpenLineOfSight(x, y, desiredCount)); 108 | 109 | foreach (ChessBoard.Cell move in possibleMoves) 110 | { 111 | if (updateHitGraph) 112 | move.HitBy.Add(Piece); 113 | } 114 | } 115 | 116 | /// 117 | /// Tells if the moved piece on the cell changed the hit state of the blocked 118 | /// 119 | /// Where the piece stands right now 120 | /// Where the piece is moved 121 | /// Hit tests this piece 122 | /// If blocked is hittable after moving the from 123 | public bool IsBlockedIfMove(ChessBoard.Cell from, ChessBoard.Cell to, ChessBoard.Cell blocked) 124 | { 125 | if (possibleMoves.Contains(blocked) && !possibleMoves.Contains(to)) 126 | { 127 | //The blocked is hittable to begin with and we don't block it with a new blocker 128 | //To may still equal blocked but direction should not care about that 129 | return false; 130 | } 131 | else if (possibleMoves.Contains(from)) 132 | { 133 | int toIndex = possibleMoves.IndexOf(to); 134 | if (0 <= toIndex && toIndex < possibleMoves.Count - 1) 135 | return true; //The blocker closer to the piece 136 | else 137 | { 138 | //If we moved further 139 | foreach (ChessBoard.Cell move in from.OpenLineOfSight(X, Y, DesiredCount - possibleMoves.Count)) 140 | { 141 | if (move == to) //The blocker moved into the new path 142 | return true; 143 | if (move == blocked) //The blocked is hittable 144 | return false; 145 | } 146 | } 147 | } 148 | 149 | //Happens when the blocker was not cotained and the blocked was not contained a perfect combination for nothing happening 150 | return true; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /ChessConsole/Piece.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ChessConsole 4 | { 5 | /// 6 | /// Represents an abstract chess piece 7 | /// 8 | public abstract class Piece 9 | { 10 | /// 11 | /// Color of piece 12 | /// 13 | public PlayerColor Color 14 | { 15 | private set; 16 | get; 17 | } 18 | 19 | /// 20 | /// False by default, set to true upon first move 21 | /// 22 | public bool Moved 23 | { 24 | protected set; 25 | get; 26 | } 27 | 28 | /// 29 | /// All the moves possible to make with this piece 30 | /// 31 | public abstract IEnumerable PossibleMoves 32 | { 33 | get; 34 | } 35 | 36 | /// 37 | /// All the moves legal to make with this piece. It's a subset of . 38 | /// See also . 39 | /// 40 | public List LegalMoves 41 | { 42 | private set; 43 | get; 44 | } 45 | 46 | public ChessBoard.Cell Parent 47 | { 48 | private set; 49 | get; 50 | } 51 | 52 | public Piece(PlayerColor color) 53 | { 54 | Color = color; 55 | Moved = false; 56 | LegalMoves = new List(); 57 | } 58 | 59 | /// 60 | /// Called when the piece is first placed or when the piece is replaced after promotion. 61 | /// Does not recalculate just yet, you have to call for that. 62 | /// 63 | public void OnPlace(ChessBoard.Cell cell) 64 | { 65 | Parent = cell; 66 | } 67 | 68 | /// 69 | /// Called when the piece is moved. 70 | /// Does not recalculate just yet, you have to call for that. 71 | /// 72 | public void OnMove(ChessBoard.Cell cell) 73 | { 74 | Parent = cell; 75 | Moved = true; 76 | } 77 | 78 | /// 79 | /// Recalculates the possible moves and updates the hit graph 80 | /// 81 | public abstract void Recalculate(); 82 | 83 | /// 84 | /// Tells if the moved piece on the cell changed the hit state of the blocked 85 | /// 86 | /// Where the piece stands right now 87 | /// Where the piece is moved 88 | /// Hit tests this piece 89 | /// If blocked is hittable after moving the from 90 | public abstract bool IsBlockedIfMove(ChessBoard.Cell from, ChessBoard.Cell to, ChessBoard.Cell blocked); 91 | 92 | public abstract char Char { get; } 93 | 94 | protected virtual bool canHit(ChessBoard.Cell cell) 95 | { 96 | return cell != null && cell.Piece != null && cell.Piece.Color != Color; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /ChessConsole/Pieces/Bishop.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ChessConsole.Pieces 4 | { 5 | public class Bishop : Piece 6 | { 7 | /// 8 | /// Represents the directions of movement 9 | /// 10 | private Direction[] directions = new Direction[4]; 11 | 12 | public Bishop(PlayerColor color) 13 | : base(color) 14 | { 15 | for (int i = 0; i < 4; i++) 16 | { 17 | directions[i] = null; 18 | } 19 | } 20 | 21 | public Bishop(Piece promote) 22 | : this(promote.Color) 23 | { 24 | Moved = promote.Moved; 25 | } 26 | 27 | public override IEnumerable PossibleMoves 28 | { 29 | get 30 | { 31 | foreach (Direction direction in directions) 32 | { 33 | foreach (ChessBoard.Cell cell in direction.GetPossibleMoves()) 34 | { 35 | yield return cell; 36 | } 37 | } 38 | } 39 | } 40 | 41 | public override void Recalculate() 42 | { 43 | //Open up left direction and listen to it 44 | directions[0] = new Direction(this, -1, 1); 45 | //Open up right direction and listen to it 46 | directions[1] = new Direction(this, 1, 1); 47 | //Open down left direction and listen to it 48 | directions[2] = new Direction(this, -1, -1); 49 | //Open down right direction and listen to it 50 | directions[3] = new Direction(this, 1, -1); 51 | } 52 | 53 | public override bool IsBlockedIfMove(ChessBoard.Cell from, ChessBoard.Cell to, ChessBoard.Cell blocked) 54 | { 55 | foreach (Direction direction in directions) 56 | { 57 | //If any direction can hit the blocked return false 58 | if (!direction.IsBlockedIfMove(from, to, blocked)) return false; 59 | } 60 | 61 | return true; 62 | } 63 | 64 | public override char Char => 'B'; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ChessConsole/Pieces/King.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ChessConsole.Pieces 4 | { 5 | public class King : Piece 6 | { 7 | /// 8 | /// Represents the directions of movement 9 | /// 10 | private Direction[] directions = new Direction[8]; 11 | 12 | /// 13 | /// Shows if we can castle to the left 14 | /// 15 | private bool canCastleLeft; 16 | 17 | /// 18 | /// Shows if we can castle to the right 19 | /// 20 | private bool canCastleRight; 21 | 22 | public King(PlayerColor color) 23 | : base(color) 24 | { 25 | for (int i = 0; i < 8; i++) 26 | { 27 | directions[i] = null; 28 | } 29 | } 30 | 31 | public override IEnumerable PossibleMoves 32 | { 33 | get 34 | { 35 | foreach (Direction direction in directions) 36 | { 37 | foreach (ChessBoard.Cell cell in direction.GetPossibleMoves()) 38 | { 39 | yield return cell; 40 | } 41 | 42 | if (canCastleLeft) 43 | { 44 | yield return Parent.Parent.GetCell(2, (Color == PlayerColor.White) ? 0 : 7); 45 | } 46 | 47 | if (canCastleRight) 48 | { 49 | yield return Parent.Parent.GetCell(6, (Color == PlayerColor.White) ? 0 : 7); 50 | } 51 | } 52 | } 53 | } 54 | 55 | public override void Recalculate() 56 | { 57 | //If moved castling is not possible anymore an we should also remove listeners 58 | if (!Moved) 59 | { 60 | //Set it to true we'll set it to false if it wasn't true 61 | canCastleLeft = true; 62 | 63 | //Checks if the left rook is still in place and haven't moved yet 64 | ChessBoard.Cell leftRookCell = Parent.Parent.GetCell(0, (Color == PlayerColor.White) ? 0 : 7); 65 | if (leftRookCell.Piece == null || !(leftRookCell.Piece is Rook) || leftRookCell.Piece.Color != Color || leftRookCell.Piece.Moved) 66 | canCastleLeft = false; 67 | else 68 | { 69 | //Checks pieces that could block the castle 70 | for (int i = 1; i <= 3; i++) 71 | { 72 | if (Parent.Parent.GetCell(i, (Color == PlayerColor.White) ? 0 : 7).Piece != null) 73 | canCastleLeft = false; 74 | } 75 | } 76 | 77 | //Set it to true we'll set it to false if it wasn't true 78 | canCastleRight = true; 79 | 80 | //Checks if the right rook is still in place and haven't moved yet 81 | ChessBoard.Cell rightRookCell = Parent.Parent.GetCell(7, (Color == PlayerColor.White) ? 0 : 7); 82 | if (rightRookCell.Piece == null || !(rightRookCell.Piece is Rook) || rightRookCell.Piece.Color != Color || rightRookCell.Piece.Moved) 83 | canCastleRight = false; 84 | else 85 | { 86 | //Checks pieces that could block the castle 87 | for (int i = 5; i <= 6; i++) 88 | { 89 | if (Parent.Parent.GetCell(i, (Color == PlayerColor.White) ? 0 : 7).Piece != null) 90 | canCastleRight = false; 91 | } 92 | } 93 | } 94 | 95 | //Open upward direction and listen to it 96 | directions[0] = new Direction(this, 0, 1, 1); 97 | //Open downward direction and listen to it 98 | directions[1] = new Direction(this, 0, -1, 1); 99 | //Open leftward direction and listen to it 100 | directions[2] = new Direction(this, -1, 0, 1); 101 | //Open rightward direction and listen to it 102 | directions[3] = new Direction(this, 1, 0, 1); 103 | //Open up left direction and listen to it 104 | directions[4] = new Direction(this, -1, 1, 1); 105 | //Open up right direction and listen to it 106 | directions[5] = new Direction(this, 1, 1, 1); 107 | //Open down left direction and listen to it 108 | directions[6] = new Direction(this, -1, -1, 1); 109 | //Open down right direction and listen to it 110 | directions[7] = new Direction(this, 1, -1, 1); 111 | } 112 | 113 | public override bool IsBlockedIfMove(ChessBoard.Cell from, ChessBoard.Cell to, ChessBoard.Cell blocked) 114 | { 115 | foreach (Direction direction in directions) 116 | { 117 | //If any direction can hit the blocked return false 118 | if (!direction.IsBlockedIfMove(from, to, blocked)) 119 | return false; 120 | } 121 | 122 | return true; 123 | } 124 | 125 | public override char Char => 'K'; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /ChessConsole/Pieces/Knight.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ChessConsole.Pieces 4 | { 5 | public class Knight : Piece 6 | { 7 | /// 8 | /// Possible places where the knight can jump 9 | /// 10 | private ChessBoard.Cell[] possibleCells = new ChessBoard.Cell[8]; 11 | 12 | public Knight(PlayerColor color) 13 | : base(color) 14 | { 15 | for (int i = 0; i < 8; i++) 16 | { 17 | possibleCells[i] = null; 18 | } 19 | } 20 | 21 | public Knight(Piece promote) 22 | : this(promote.Color) 23 | { 24 | Moved = promote.Moved; 25 | } 26 | 27 | public override IEnumerable PossibleMoves 28 | { 29 | get 30 | { 31 | foreach (ChessBoard.Cell cell in possibleCells) 32 | { 33 | if (cell != null && (cell.Piece == null || cell.Piece.Color != Color)) 34 | yield return cell; 35 | } 36 | } 37 | } 38 | 39 | public override void Recalculate() 40 | { 41 | //2 up 1 left 42 | possibleCells[0] = Parent.Open(-1, 2); 43 | //2 down 1 left 44 | possibleCells[1] = Parent.Open(-1, -2); 45 | //2 up 1 right 46 | possibleCells[2] = Parent.Open(1, 2); 47 | //2 down 1 right 48 | possibleCells[3] = Parent.Open(1, -2); 49 | //1 up 2 left 50 | possibleCells[4] = Parent.Open(-2, 1); 51 | //1 down 2 left 52 | possibleCells[5] = Parent.Open(-2, -1); 53 | //1 up 2 right 54 | possibleCells[6] = Parent.Open(2, 1); 55 | //1 down 2 right 56 | possibleCells[7] = Parent.Open(2, -1); 57 | 58 | for (int i = 0; i < 8; i++) 59 | { 60 | if (possibleCells[i] != null) 61 | possibleCells[i].HitBy.Add(this); 62 | } 63 | } 64 | 65 | public override bool IsBlockedIfMove(ChessBoard.Cell from, ChessBoard.Cell to, ChessBoard.Cell blocked) 66 | { 67 | //The knight's hits cannot be blocked 68 | for (int i = 0; i < 8; i++) 69 | if (possibleCells[i] == blocked) 70 | return false; 71 | 72 | return true; 73 | } 74 | 75 | public override char Char => 'H'; //H for horse as we are using K for king 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ChessConsole/Pieces/Pawn.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ChessConsole.Pieces 4 | { 5 | public class Pawn : Piece 6 | { 7 | /// 8 | /// Represents the forward direction moves of the pawn 9 | /// 10 | private Direction forward = null; 11 | 12 | /// 13 | /// Represents the hittables of the pawn 14 | /// 15 | private ChessBoard.Cell[] hits = new ChessBoard.Cell[2]; 16 | 17 | public Pawn(PlayerColor color) 18 | : base(color) 19 | { 20 | hits[0] = hits[1] = null; 21 | } 22 | 23 | public override IEnumerable PossibleMoves 24 | { 25 | get 26 | { 27 | foreach (ChessBoard.Cell cell in forward.GetPossibleMoves(false)) 28 | { 29 | yield return cell; 30 | } 31 | 32 | if (canHit(hits[0])) 33 | yield return hits[0]; 34 | if (canHit(hits[1])) 35 | yield return hits[1]; 36 | } 37 | } 38 | 39 | public override void Recalculate() 40 | { 41 | //Open forward direction and listen to it 42 | forward = new Direction(this, 0, (Color == PlayerColor.White) ? 1 : -1, Moved ? 1 : 2, false); 43 | 44 | hits[0] = Parent.Open(-1, (Color == PlayerColor.White) ? 1 : -1); 45 | hits[1] = Parent.Open( 1, (Color == PlayerColor.White) ? 1 : -1); 46 | 47 | if (hits[0] != null) 48 | hits[0].HitBy.Add(this); 49 | if (hits[1] != null) 50 | hits[1].HitBy.Add(this); 51 | } 52 | 53 | public override bool IsBlockedIfMove(ChessBoard.Cell from, ChessBoard.Cell to, ChessBoard.Cell blocked) 54 | { 55 | //The pawn's hits cannot be blocked 56 | return hits[0] != blocked && hits[1] != blocked; 57 | } 58 | 59 | public override char Char => 'P'; 60 | 61 | protected override bool canHit(ChessBoard.Cell cell) 62 | { 63 | //Handling en passant over here 64 | return base.canHit(cell) || (cell != null && cell == cell.Parent.EnPassant); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ChessConsole/Pieces/Queen.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ChessConsole.Pieces 4 | { 5 | public class Queen : Piece 6 | { 7 | /// 8 | /// Represents the directions of movement 9 | /// 10 | private Direction[] directions = new Direction[8]; 11 | 12 | public Queen(PlayerColor color) 13 | : base(color) 14 | { 15 | for (int i = 0; i < 8; i++) 16 | { 17 | directions[i] = null; 18 | } 19 | } 20 | 21 | public Queen(Piece promote) 22 | : this(promote.Color) 23 | { 24 | Moved = promote.Moved; 25 | } 26 | 27 | public override IEnumerable PossibleMoves 28 | { 29 | get 30 | { 31 | foreach (Direction direction in directions) 32 | { 33 | foreach (ChessBoard.Cell cell in direction.GetPossibleMoves()) 34 | { 35 | yield return cell; 36 | } 37 | } 38 | } 39 | } 40 | 41 | public override void Recalculate() 42 | { 43 | //Open upward direction and listen to it 44 | directions[0] = new Direction(this, 0, 1); 45 | //Open downward direction and listen to it 46 | directions[1] = new Direction(this, 0, -1); 47 | //Open leftward direction and listen to it 48 | directions[2] = new Direction(this, -1, 0); 49 | //Open rightward direction and listen to it 50 | directions[3] = new Direction(this, 1, 0); 51 | //Open up left direction and listen to it 52 | directions[4] = new Direction(this, -1, 1); 53 | //Open up right direction and listen to it 54 | directions[5] = new Direction(this, 1, 1); 55 | //Open down left direction and listen to it 56 | directions[6] = new Direction(this, -1, -1); 57 | //Open down right direction and listen to it 58 | directions[7] = new Direction(this, 1, -1); 59 | } 60 | 61 | public override bool IsBlockedIfMove(ChessBoard.Cell from, ChessBoard.Cell to, ChessBoard.Cell blocked) 62 | { 63 | foreach (Direction direction in directions) 64 | { 65 | //If any direction can hit the blocked return false 66 | if (!direction.IsBlockedIfMove(from, to, blocked)) return false; 67 | } 68 | 69 | return true; 70 | } 71 | 72 | public override char Char => 'Q'; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ChessConsole/Pieces/Rook.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ChessConsole.Pieces 4 | { 5 | public class Rook : Piece 6 | { 7 | /// 8 | /// Represents the directions of movement 9 | /// 10 | private Direction[] directions = new Direction[4]; 11 | 12 | public Rook(PlayerColor color) 13 | : base(color) 14 | { 15 | for (int i = 0; i < 4; i++) 16 | { 17 | directions[i] = null; 18 | } 19 | } 20 | 21 | public Rook(Piece promote) 22 | : this(promote.Color) 23 | { 24 | Moved = promote.Moved; 25 | } 26 | 27 | public override IEnumerable PossibleMoves 28 | { 29 | get 30 | { 31 | foreach (Direction direction in directions) 32 | { 33 | foreach (ChessBoard.Cell cell in direction.GetPossibleMoves()) 34 | { 35 | yield return cell; 36 | } 37 | } 38 | } 39 | } 40 | 41 | public override void Recalculate() 42 | { 43 | //Open upward direction and listen to it 44 | directions[0] = new Direction(this, 0, 1); 45 | //Open downward direction and listen to it 46 | directions[1] = new Direction(this, 0, -1); 47 | //Open leftward direction and listen to it 48 | directions[2] = new Direction(this, -1, 0); 49 | //Open rightward direction and listen to it 50 | directions[3] = new Direction(this, 1, 0); 51 | } 52 | 53 | public override bool IsBlockedIfMove(ChessBoard.Cell from, ChessBoard.Cell to, ChessBoard.Cell blocked) 54 | { 55 | //If any direction can hit the blocked return false 56 | foreach (Direction direction in directions) 57 | { 58 | //If any direction can hit the blocked return false 59 | if (!direction.IsBlockedIfMove(from, to, blocked)) return false; 60 | } 61 | 62 | return true; 63 | } 64 | 65 | public override char Char => 'R'; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ChessConsole/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ChessConsole 4 | { 5 | public class Program 6 | { 7 | static ChessGame game; 8 | static void Main(string[] args) 9 | { 10 | Console.CursorVisible = false; 11 | ConsoleGraphics graphics = new ConsoleGraphics(); 12 | game = new ChessGame(); 13 | 14 | do 15 | { 16 | game.Draw(graphics); 17 | graphics.SwapBuffers(); 18 | game.Update(); 19 | } while (game.Running); 20 | 21 | Console.Read(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ChessConsole/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("ChessConsole")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("ChessConsole")] 13 | [assembly: AssemblyCopyright("Copyright © 2018")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("66f5e978-f5c6-4a48-a689-a6f1e22be741")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ConsoleChess 2 | 3 | A simple chess game in the C# console 4 | 5 | ## If you find an issue: 6 | 7 | To help development please provide the moves performed to reproduce this bug. If the bug is not always reproducable, steps can still help. 8 | 9 | Moves should be provided as: 10 | 11 | `FROM -> TO (promotion if promoted)` 12 | eg. `G2->F2` 13 | 14 | ## ChessBoard Features: 15 | 16 | - [x] All pieces with basic movement 17 | - [x] Line of sight testing for appropriate pieces 18 | - [x] En passant 19 | - [x] Castling 20 | - [x] Promotion 21 | - [x] Hit graphs for legal move resolution 22 | - [x] Legal move resolution - so that you can't put yourself in check 23 | - [x] Check and any legal move detection 24 | 25 | ## Documentation for the standalone ChessBoard: 26 | 27 | This section provides information to use the `ChessBoard` class with your own `Game` class. 28 | 29 | ### TurnStart 30 | 31 | You have to call this method everyturn and after every move. 32 | 33 | At turn start you might want to check for win and draw conditions. The two easiest (checkmate/stalemate) can be done this way: 34 | 35 | ```csharp 36 | bool hasMoves = board.TurnStart(currentPlayer); 37 | if (!hasMoves) 38 | { 39 | //Turn start sets up a cached inCheck value as well 40 | if (board.IsInCheck(currentPlayer)) 41 | { 42 | //Checkmate, win for (currentPlayer == PlayerColor.White) ? Black : White 43 | } 44 | else 45 | { 46 | //Stalemate, draw 47 | } 48 | } 49 | ``` 50 | 51 | ### Moving 52 | 53 | The board has no way of knowing that you picked a piece or are about to put down a piece. 54 | 55 | I'm going to provide you with typical examples of what you would want to accomplish. 56 | 57 | #### Picking a piece 58 | 59 | ```csharp 60 | Piece holding = board.GetCell(cursorX, cursorY).Piece; 61 | 62 | if (holding != null) 63 | { 64 | if (holding.LegalMoves.Count == 0) 65 | { 66 | //Do some sort of unpicking logic so that you can't get stuck with picking a piece that can't move 67 | holding = null; //As an example 68 | } 69 | else 70 | { 71 | //Set internal state variables to allow for movement etc. 72 | } 73 | } 74 | ``` 75 | 76 | #### Attempting to put down a piece 77 | 78 | Before attempting to put down a piece, you should ask the board if a promotion may occur. The system was designed to be able to cancel this promotion process. And you obviously need to not move before being able to cancel it. This is why the `Move` metod takes `PromoteOptions` as parameter. It will automatically perform the promotion of a pawn if it has to. If a promotion will however not occur, this parameter will be ignored so you can safely pass anything as your default. 79 | 80 | To decide if a promotion is about to occur, then use the `IsPromotable` function. 81 | 82 | ```csharp 83 | to = board.GetCell(cursorX, cursorY); 84 | 85 | if (holding != null) //We'll check, even though you should have logic to not be able to try move if you don't hold anything 86 | { 87 | //Check if it's contained as a legal move 88 | if (!holding.LegalMoves.Contains(to)) 89 | { 90 | //You might wanna cancel the current move here 91 | 92 | return; //assuming this is a function 93 | } 94 | 95 | //This function tells if your piece (usually a pawn) is about to be promoted 96 | if (board.IsPromotable(holding.Parent, to)) 97 | { 98 | //Handle promotion... 99 | } 100 | 101 | //Finally tell the board to move the piece 102 | board.Move(holding.Parent, to, currentPromoteOption); 103 | } 104 | ``` 105 | 106 | ### Starting a new game 107 | 108 | Constructing a chess board automatically resets the game, so that's a viable option. 109 | 110 | If you'd like the board also contains a Reset method which does exactly the same thing as the constructor. 111 | 112 | ```csharp 113 | 114 | board = new ChessBoard(); 115 | //... 116 | //Worst game loop ever 117 | while (!exit) 118 | { 119 | //... 120 | while (!gameOver) 121 | { 122 | //... 123 | } 124 | 125 | //... 126 | board.Reset(); 127 | } 128 | ``` --------------------------------------------------------------------------------