Matt Miller

I am a Senior Computer Science Major at The University of Missouri Kansas City. I love using the power of programming to solve modern-day problems.

Chess

01 May 2020 »

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:

  1. Print the game board
  2. Print the UNIX code for each chess piece
  3. Write a generic game_piece class
  4. Write a Move class
  5. Write a class for each of the pieces that inherited the game_piece class
  6. create rules for each of the game pieces
  7. Allow the ability to capture other pieces
  8. Add a Game_runner class to manage the game loop
  9. Create method to determine “check”
  10. 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