The goal of this project is to implement a command line Chess game using Ruby.
This is a project from The Odin Project.
Open your Terminal/Command Line. Navigate to the directory where your version will live. Type in the following:
$ git clone https://github.com/JonathanYiv/chess.git
$ cd chess
$ ruby chess.rb
-
I reflected on how to represent the chess pieces. They will have multiple values: their x-position, y-position, moveset, and icon. I could do a several layers deep mixture of hash/array, but accessing the necessary data would be ugly. Therefore, I will use classes.
-
Had a discussion with @benjaminapetersen about Composition vs Inheritance. Read about 'has-a' vs 'is-a' relationships. Realized that for the scope of this project, chess pieces don't need to inherit anything nor would their small pieces of functionality need to be extracted into something like a module.
-
I have been looking forward to this project for a very long time. I heard talk of it in the chat, then peeked forward at it earlier, and was excited to tackle this. It will definitely be a notch in my belt.
-
I think the most difficult things to implement will be the various odd rules, like 'en passant' and 'promotion of pawns.'
-
I wonder how long this will take me. Well, here we go.
-
Castling
-
Description
-
The King moves two spaces to the right or left
-
The Rook moves over the King to be on the opposite side of the King
-
-
Conditions
-
The King has never moved
-
The Rook has never moved
-
The squares between the King and Rook are unoccupied
-
The King will not castle out of, through, or into check
-
-
-
En Passant
-
Description
- After a Pawn moves two ranks forward, an enemy adjacent Pawn may capture the Pawn diagonally on the turn immediately afterward.
-
Condition
-
The capturing Pawn must be on its fifth rank.
-
The captured Pawn must be on an adjacent square.
-
The captured Pawn must have moved two squares in a single move.
-
The capture can only be made on the move immediately after the double-step move.
-
-
-
Promotion
-
Description
-
When a Pawn reaches its eighth rank, it can be replaced by a: Queen, Knight, Rook, or Bishop
-
The choice is not limited to previously captured pieces.
-
-
-
Check
-
Description
-
When a King is under threat of capture.
-
It can be exited by interposing a piece, capturing the threatening piece, or moving the King.
-
If the player can not exit check, then it is checkmate and the player loses.
-
Kings may not move into check.
-
-
class GameBoard
instance variables:
@positions: a 2D array containing where all the chess piece instances are located
@player1
@player2
@turn_counter: initially 1
methods:
initialize: instantiates @positions and places pieces
place_pieces: creates all piece instances and places them in their default starting state
play: starts the game by calling submethods
title: displays ASCII chess title
display: displays the board based on where pieces are
instructions: displays the game instructions
get_names: sets @player1 and @player2 to Player instances
turns: starts the turn loop, using @turn_counter to determine whose turn it is
turn: prompts the appropriate player for user input
convert: converts a move input to the correct position on the @positions 2D array
move: moves a chess piece, also calls find_possible_moves to update its moveset
promote?: checks if a pawn has reached the end of the board, if so, prompt the appropriate player to promote it to a queen/knight/rook/bishop
check?: checks if a check is in play, if so, mandates that the next turn has the player move their king by setting all other @possible_moves to nothing
checkmate?: checks if a checkmate is in play, if so, the game ends and a player wins
win: displays winning text for the appropriate player
lose: displays losing text for the appropriate player
save: serializes @positions, @player1, @player2, and @turn_counter
load: loads serialized data into the game and sets instance variables equal to it
class Player
instance variables:
@name
methods:
initialize: instantiates and prompts for names
get_name: gets user input for name
take_turn: gets user input for piece to move and where to move
class King/Rook/Bishop/Queen/Knight/Pawn
instance variables:
@moveset
@x_position
@y_position
@possible_moves
@icon
methods:
initialize(black/white): creates instance variables, takes in a parameter to determine icon color
find_possible_moves: when initialized, @possible_moves calls this to create an array for its value. Is passed in @positions to help calculate
Additionally:
class King/Rook/Pawn
instance variable:
@has_moved: true or false for castling and pawn double-step move
class Pawn
instance variable:
@double_stepped: true immediately after using a double-step move
This project is currently incomplete.
-
Cloning: Behold, my greatest failure as of yet. This was the longest I have ever taken to solve a bug. So here's the story...
-
First Iteration: In order for me to calculate the King's possible moves, I needed to evaluate the possible moves of all other pieces to make sure the King didn't move into check. So I iterated through every existing piece and compared them to the King's possible moves to determine which were valid.
-
Second Iteration: Then, I realized that a given piece's possible moveset would not include a friendly piece adjacent to the King, because it can't capture friendly pieces. However, the King could capture that piece and put itself into check via the given piece's moveset. So I created a 'deep clone' of the board. I iterated through every position on the board and cloned the object into the clone board. I moved the King to the potential move-spot and then calculated the new possible movesets accordingly. This didn't work, because it would calculate the possible moveset of the enemy King, which would create another clone board, which would calculate the moveset of the enemy King... stack overflow.
-
Third Iteration: When iterating through the pieces on the clone board, I created an if/else to isolate the King, and simply added all adjacent squares into that King's possible movesets. That seemed to work until I found out that the Black King somehow could capture his friendly adjacent pieces.
-
Fourth Iteration: This took me two days to realize, but the 'deep clone' I had created earlier still wasn't deep enough! The objects that were cloned still had instance variables, and in the case of the instance variable @possible_moves, it wasn't creating a copy, it was simply pointing to the same object! I made the clone deeper and that fixed the problem.
-
-
Pry: @105ron linked me to pry when I sought help for the above problem. I learnt some of the basics including 'binding.pry' and, although it didn't help me debug the cloning problem, I can see it is very useful and will add it to my debugging toolbelt.
-
Safe Navigation Operator: I learned about the "&" operator. It checks for nil before calling a method.
-
Example: variable&.method
-
If variable is nil, it will return nil.
-
If variable is not nil, it will call the method.
-
-
-
I have tried changing GitHub's tab size to 2, even modifying the file itself by ending a comment or something, but it always reverts to a displayed tab size of 8. Does anyone know how to fix this?
-
Thanks to some feedback from @hwchan91, I made several changes to make my code more modular and readable, including changing the name of some variables/methods and splitting a few large methods into multiple smaller methods.
-
After completing the project, I realized there are several areas I need to improve upon:
-
Making methods more modular. Right now, my methods do too much. I should have been better able to abstract concepts and make more methods doing less things.
-
Debugging. Going into this project, I mostly did output debugging. With this project, I've used a lot more testing and pry as additional tools to help me narrow down the problem. This process was slow for me because I had to think about what I was doing. With more practice, I hope my debugging skills improve.
-
More coverage with unit tests. Sometimes in a rush, I would simply fully write the method without writing any tests. Sometimes these methods would perform complicated procedures and it would bite me in the butt later when the method had undesired behavior. I'd have to go back and write tests for specific scenarios and then hunt down where exactly it broke.
-
-
Overall, I am very happy that I completed this project. It was a worthy venture.
-
After the fact, I realized I had forgotten that one of the project requirements was the ability to save and load files. I am rather burnt out on chess at the moment, so I will come back later and implement it. Onwards to Rails!