Another Game!!
The final project for the Ruby portion of The Odin Project is to create a command line version of Chess with Ruby. We were given very little structure or guidance for this assignment. The architecture of the project was left to our own discretion. The one piece of advice that was given to us was to use RSPEC testing for the project. This made since given the insane number of possible permutations of a Chess game.
A Plan for Success!
I was initially overwhelmed by the scope of this project and i couldnt bare to think of the number of lines of code it would take for me to fulfill the requirements. Earlier in quarantine, i followed a tutorial for building a Netflix reccomendation algorithm. One thing that was stressed in the tutorial, was to break the project into smaller parts and have benchmarks along the way. At these “benchmarks” i would utilize RSPEC testing to make sure that each feature worked properly before moving on to the next part of the project. I made an initial ouline describing my goals:
- Print the game board
- Print the UNIX code for each chess piece
- Write a generic game_piece class
- Write a Move class
- Write a class for each of the pieces that inherited the game_piece class
- create rules for each of the game pieces
- Allow the ability to capture other pieces
- Add a Game_runner class to manage the game loop
- Create method to determine “check”
- Add the ability to save and load games
Creating the board!
Here is what my vision was for the board:
8 │ ♜ │ ♞ │ ♝ │ ♛ │ ♚ │ ♝ │ ♞ │ ♜ │
────────────────────────────────────
7 │ ♟ │ ♟ │ ♟ │ ♟ │ ♟ │ ♟ │ ♟ │ ♟ │
────────────────────────────────────
6 │ │ │ │ │ │ │ │ │
────────────────────────────────────
5 │ │ │ │ │ │ │ │ │
────────────────────────────────────
4 │ │ │ │ │ │ │ │ │
────────────────────────────────────
3 │ │ │ │ │ │ │ │ │
────────────────────────────────────
2 │ ♙ │ ♙ │ ♙ │ ♙ │ ♙ │ ♙ │ ♙ │ ♙ │
────────────────────────────────────
1 │ ♖ │ ♘ │ ♗ │ ♕ │ ♔ │ ♗ │ ♘ │ ♖ │
────────────────────────────────────
│ a │ b │ c │ d │ e │ f │ g │ h │
────────────────────────────────────
For the purposes of testing, i made sure to make a text-only version of the board as well:
[["Rb","Nb","Bb","Qb","Kb","Bb","Nb","Rb"],
["Pb","Pb","Pb","Pb","Pb","Pb","Pb","Pb"],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
["Pw","Pw","Pw","Pw","Pw","Pw","Pw","Pw"],
["Rw","Nw","Bw","Qw","Kw","Bw","Nw","Rw"]]
Because this project is mainly about using OOP, the display version is not too fancy. All i did was use a two-dimensional array to represent the board.
This is the point where i used my first RSPEC tests
describe "#disp_board" do
it "Turns the 'board' into a 'display board" do
board =[["Rb","Nb","Bb","Qb","Kb","Bb","Nb","Rb"],
["Pb","Pb","Pb","Pb","Pb","Pb","Pb","Pb"],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
["Pw","Pw","Pw","Pw","Pw","Pw","Pw","Pw"],
["Rw","Nw","Bw","Qw","Kw","Bw","Nw","Rw"]]
display = [["8","♜","♞","♝","♛","♚","♝","♞","♜"],
["7","♟","♟","♟","♟","♟","♟","♟","♟"],
["6"," "," "," "," "," "," "," "," "],
["5"," "," "," "," "," "," "," "," "],
["4"," "," "," "," "," "," "," "," "],
["3"," "," "," "," "," "," "," "," "],
["2","♙","♙","♙","♙","♙","♙","♙","♙"],
["1","♖","♘","♗","♕","♔","♗","♘","♖"],
[" ","a","b","c","d","e","f","g","h"]]
# @test_game.curr_board=board
@test_game.display_board
expect(@test_game.disp_array).to eql display
end
end
This test makes sure that the the “test” board can be properly turned into a “display” board with the appropriate unicode characters.
Creating the pieces
The first piece i would make would be the rook
class Rook < Game_Piece
attr_accessor :x,:y
attr_reader :team
def initialize(x,y,team=nil)
@x=x
@y=y
@team=team
end
def is_move_allowed(to_x,to_y) #Checks to see if the move is allowed based on the pieces 'rule'
allowed=false
if @x==to_x || @y==to_y
allowed=true
end
if @x==to_x && @y==to_y
allowed=false
end
return allowed
end
Each of the pieces will have a set of rules. The rules for the rook are by far the simplest, two conditional statements are all that is needed to make sure the move is valid
def check_path(to_x,to_y,curr_board) #Checks to make sure if the path is clear for the requested move
x_diff=to_x-@x
y_diff=to_y-@y
# puts "@x: #{@x} @y: #{@y} to_x: #{to_x} to_y: #{to_y}"
x_dist = (x_diff).abs
y_dist = (y_diff).abs
path = []
x_diff>0 ? x_dir=1:x_dir=-1
y_diff>0 ? y_dir=1:y_dir=-1
for i in 0..x_dist
for j in 0..y_dist
#puts "@x+(i*x_dir): #{@x+(i*x_dir)} @y+(i*y_dir): #{@y+(i*y_dir)} "
path.push(curr_board[@x+(i*x_dir)][@y+(j*y_dir)])
end
end
# puts "Path: #{path}"
path.shift
path.pop
# puts "Path: #{path}"
return path.all? {|s| s==" "}
end
Aside from the Knight all of the pieces will require a “check_path” method to make sure that there is not another piece between it’s starting square and it’s ending square.
def to_s
if @team=='w'
return "♖"
else
return "♜"
end
end
end
Every piece will have a to_string method tgat returns it’s unicode equivalence.
The tricky part about a project like this is the sheer number of possibilites. This is why extensive testing is required for each step of the project. Since i just wrote the Rook class, i will need to run through as many scenarios as possible to make sure that the movement behavior is as it should be.
Here are all the RSPEC tests for the Rook class:
describe Rook do
before(:each) do
@test_rook=Rook.new(1,3)
end
describe "#is_move_allowed" do
it "Allows horizontal moves" do
expect(@test_rook.is_move_allowed(1,7)).to eql true
end
it "Allows vertical moves" do
expect(@test_rook.is_move_allowed(4,3)).to eql true
end
it "Does not allow diagonal moves" do
expect(@test_rook.is_move_allowed(3,5)).to eql false
end
it "Does not allow stupid moves" do
expect(@test_rook.is_move_allowed(4,5)).to eql false
end
end
describe "#check_path" do
it "returns false if path is blocked" do
board= [[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," ","♝"," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "]]
expect(@test_rook.check_path(5,3,board)).to eql false
end
it "returns true if path is clear" do
board= [[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," ","♝"," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "]]
expect(@test_rook.check_path(2,3,board)).to eql true
end
it "returns true if path is clear horizontally one spot" do
board= [[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," ","♝","♝"," "," "," "],
[" "," ","♝"," ","♝"," "," "," "],
[" "," "," "," ","♝"," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "]]
@rook_boi=Rook.new(4,2)
expect(@rook_boi.check_path(4,3,board)).to eql true
end
it "returns false if path is full horizontally three spot" do
board= [[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," ","♝","♝"," "," "," "],
[" "," ","♝"," ","♝"," "," "," "],
[" "," "," "," ","♝"," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "]]
@rook_boi=Rook.new(4,2)
expect(@rook_boi.check_path(4,5,board)).to eql false
end
it "returns true if path is clear horizontally four spot" do
board= [[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," ","♝","♝"," "," "," "],
[" "," ","♝"," ","♝"," "," "," "],
[" "," "," "," ","♝"," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "]]
@rook_boi=Rook.new(6,2)
expect(@rook_boi.check_path(6,6,board)).to eql true
end
it "returns false if verical path is full" do
board= [[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," ","♝","♝"," "," "," "],
[" "," ","♝"," ","♝"," "," "," "],
[" "," "," "," ","♝"," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "]]
@rook_boi=Rook.new(0,3)
expect(@rook_boi.check_path(6,3,board)).to eql false
end
it "returns true if left path is empty" do
board= [[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," ","♝","♝"," "," "," "],
[" "," ","♝"," ","♝"," "," "," "],
[" "," "," "," ","♝"," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "]]
@rook_boi=Rook.new(6,6)
expect(@rook_boi.check_path(0,6,board)).to eql true
end
it "returns false if left path is obstructed" do
board= [[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," ","♝","♝"," "," "," "],
[" "," ","♝"," ","♝"," "," "," "],
[" "," "," "," ","♝"," "," "," "],
[" "," "," "," "," "," "," "," "],
[" "," "," "," "," "," "," "," "]]
@rook_boi=Rook.new(5,6)
expect(@rook_boi.check_path(5,2,board)).to eql false
end
end
end
The Night is a very interesting piece and it was fun trying to figure out the algorithms for it
def is_move_allowed(to_x,to_y) #Checks to see if the move is allowed based on the pieces 'rule'
allowed=false
if (to_x==x+2 || to_x == x-2) && (to_y==y+1 || to_y==y-1)
allowed=true
end
if (to_x==x+1 || to_x==x-1) && (to_y==y+2 || to_y==y-2)
allowed=true
end
return allowed
end
I found it very interesting that there is almost an inverse relationship between the difficulties in explaining a pieces move to a human compared to explaining it to a computer.
I won’t bother copying every single piece. But the one piece that was by far the hardest was the pawn. Every time you want to move a pawn you have to:
- Check if this is the pawns first move
- Check and see if the pawn is attacking
While this does not sound too terribly complicated, we have to alter the architecture of the program and add an instance variable to the pawn class: the boolean first_move.
def is_move_allowed(to_x,to_y,attack=false) #Checks to see if the move is allowed based on the pieces 'rule'
allowed=false
x_diff=(to_x-@x).abs
y_diff=(to_y-@y).abs
if @first_move
if x_diff==2 && y_diff==0
allowed=true
end
end
if attack
if x_diff==1 && y_diff==1
allowed= true
end
else
if x_diff==1 && y_diff==0
allowed=true
end
end
return allowed
end
Note how there is an addition “attack” boolean that is sent to the is_move_allowed method.
Saving and Loading the Game
Loading and saving the game was a relatively easy step:
def save_game()
data={
"board" => @my_board,
"move_list" => @move_list,
"turn" => @turn
}
yaml = YAML.dump(data)
game_file = File.open("save.txt","w")
game_file.write(yaml)
end
def load_game
yaml = YAML.load(File.open("save.txt"))
@my_board = yaml["board"]
@move_list = yaml["move_list"]
@turn = yaml["turn"]
end
Conclusion
This project was 7 days and each of those days were at least 8 hours. Actually getting the game to run error free and work as intended was a MASSIVE confidence boost. A few of the lessons i learned were:
- Do not be afraid to erase code and start fresh with a new branch and start a segment of the code from scratch
- Learn to take breaks