KillerDesigner
10/24/2013 - 2:10 AM

tic-tac-toe.rb

require "Rumoji" # See; https://github.com/mwunsch/rumoji
                          # Customize grid space and player icons as emoji charcters from: http://www.emoji-cheat-sheet.com/
require "Rainbow"

class Grid

  attr_accessor :production_run, :player_x_moves, :player_o_moves

  def initialize(production_run, player_x_moves, player_o_moves)
    @is_debug_mode = false                # set to true to invoke debugging statements if needed...
    @production_run = production_run  #

    @player_x_moves = player_x_moves  # the set of grid positions filled by player x
    @player_o_moves = player_o_moves  # the set of grid positions filled by player o

    @player_x = Rumoji.decode(":negative_squared_cross_mark:")  # "X"
    @player_o = Rumoji.decode(":large_blue_circle:")                      # "O"

    @box1 = Rumoji.decode(":one:")    # "1"
    @box2 = Rumoji.decode(":two:")    # "2"
    @box3 = Rumoji.decode(":three:")  # "3"
    @box4 = Rumoji.decode(":four:")   # "4"
    @box5 = Rumoji.decode(":five:")    # "5"
    @box6 = Rumoji.decode(":six:")     # "6"
    @box7 = Rumoji.decode(":seven:") # "7"
    @box8 = Rumoji.decode(":eight:")  # "8"
    @box9 = Rumoji.decode(":nine:")   # "9"

    # Needed to keep the base number grid value settings useable again despite player move updates (x and o insertions) to the game grid boxes...
    @box1_base = @box1.dup
    @box2_base = @box2.dup
    @box3_base = @box3.dup
    @box4_base = @box4.dup
    @box5_base = @box5.dup
    @box6_base = @box6.dup
    @box7_base = @box7.dup
    @box8_base = @box8.dup
    @box9_base = @box9.dup

    # Create start grid state...
    update_grid()
  end # method initialize


  def print_grid()
      print "\n    .    . "
      print "\n #{@box1}  | #{@box2}  | #{@box3} "
      print "\n----+----+----"
      print "\n #{@box4}  | #{@box5}  | #{@box6} "
      print "\n----+----+----"
      print "\n #{@box7}  | #{@box8}  | #{@box9} "
      print "\n    '    ' "
  end # method print_grid


  def reset_grid()
    initialize(@production_run, [], [])
  end # method reset_grid


  def update_grid()
    debug("\nlocation   : at start of 'upgrade_grid' method.") if @is_debug_mode

    #update visual grid boxes for player_x_moves..
    @player_x_moves.each do |x_move|
      print "\nDEBUG: player_x_moves.each instance: #{get_icon(x_move)} \n" if @is_debug_mode
      case x_move
        when "1"
          @box1 = @player_x.dup  #"X"
          print "\nDEBUG: setting #{@box1_base}  to #{@player_x}\n" if @is_debug_mode
        when "2"
          @box2 = @player_x.dup  #"X"
          print "\nDEBUG: setting #{@box2_base}  to #{@player_x}\n" if @is_debug_mode
        when "3"
          @box3 = @player_x.dup  #"X"
          print "\nDEBUG: setting #{@box3_base}  to #{@player_x}\n" if @is_debug_mode
        when "4"
          @box4 = @player_x.dup  #"X"
          print "\nDEBUG: setting #{@box4_base}  to #{@player_x}\n" if @is_debug_mode
        when "5"
          @box5 = @player_x.dup  #"X"
          print "\nDEBUG: setting #{@box5_base}  to #{@player_x}\n" if @is_debug_mode
        when "6"
          @box6 = @player_x.dup  #"X"
          print "\nDEBUG: setting #{@box6_base}  to #{@player_x}\n" if @is_debug_mode
        when "7"
          @box7 = @player_x.dup  #"X"
          print "\nDEBUG: setting #{@box7_base}  to #{@player_x}\n" if @is_debug_mode
        when "8"
          @box8 = @player_x.dup #"X"
          print "\nDEBUG: setting #{@box8_base}  to #{@player_x}\n" if @is_debug_mode
        when "9"
          @box9 = @player_x.dup  #"X"
          print "\nDEBUG: setting #{@box9_base}  to #{@player_x}\n" if @is_debug_mode
       end # case
    end # player_x_moves.each

    #update visual grid boxes for player_o_moves
    @player_o_moves.each do |o_move|
      print "\nDEBUG: player_o_moves.each instance: #{get_icon(o_move)} \n" if @is_debug_mode
      case o_move
        when "1"
          @box1 = @player_o.dup  #"O"
          print "\nDEBUG: setting #{@box1_base}  to #{@player_o}\n" if @is_debug_mode
        when "2"
          @box2 =  @player_o.dup  #"O"
          print "\nDEBUG: setting #{@box2_base}  to #{@player_o}\n" if @is_debug_mode
        when "3"
          @box3 = @player_o.dup #"O"
          print "\nDEBUG: setting #{@box3_base}  to #{@player_o}\n" if @is_debug_mode
        when "4"
          @box4 = @player_o.dup  #"O"
          print "\nDEBUG: setting #{@box4_base}  to #{@player_o}\n" if @is_debug_mode
        when "5"
          @box5 = @player_o.dup  #"O"
          print "\nDEBUG: setting #{@box5_base}  to #{@player_o}\n" if @is_debug_mode
        when "6"
          @box6 = @player_o.dup  #"O"
          print "\nDEBUG: setting #{@box6_base}  to #{@player_o}\n" if @is_debug_mode
        when "7"
          @box7 = @player_o.dup  #"O"
          print "\nDEBUG: setting #{@box7_base}  to #{@player_o}\n" if @is_debug_mode
        when "8"
          @box8 = @player_o.dup  #"O"
          print "\nDEBUG: setting #{@box8_base}  to #{@player_o}\n" if @is_debug_mode
        when "9"
          @box9 = @player_o.dup  #"O"
          print "\nDEBUG: setting #{@box9_base}  to #{@player_o}\n" if @is_debug_mode
       end # case
    end # player_o_moves.each

    # Print an updated display grid...
    print "\n updated grid....\n" if @is_debug_mode
    print_grid()

    # Check if we have a winner yet...
    if is_winner?("X")
        print "\n\nPlayer #{@player_x}  wins!\n\n"
        if @production_run  # continue to new game if in prodution_run mode...
          print "\n--------------\n** New game **\n".color("#00CCCC")
          reset_grid()
          process_move()  # start new game
        end
    elsif is_winner?("O")
        print "\n\nPlayer #{@player_o}  wins!\n\n"
        if @production_run  # continue to new gaae if in production_run mode...
          print "\n--------------\n** New game **\n".color("#00CCCC")
          reset_grid()
          process_move()  # start new game
        end
    elsif ( (@player_x_moves + @player_o_moves).length == 9) # no more moves left and no winner...
        print "\n\nGame ended in a tie.\n\n"
        if @production_run # contnue to new game if in production_run mode...
          print "\n--------------\n** New game **\n".color("#00CCCC")
          reset_grid()
          process_move()  # start new game
        end
    else
        print "\n\nDEBUG: no winner yet...." if @is_debug_mode
    end
  end # method update_grid()


  def is_winner?(player)
    #check if each single array of player_moves contains any of the subsets the finite single array winning states
    debug("\nlocation   : at start of 'is_winner?' method.") if @is_debug_mode

    winner = false
    winning_grids = [ ["1","2","3"], ["4","5","6"], ["7","8","9"], ["1","4","7"], ["2","5","8"], ["3","6","9"], ["1","5","9"], ["3","5","7"] ]

    (player == "X") ? (player_moves = @player_x_moves; player_icon = @player_x) : (player_moves = @player_o_moves; player_icon = @player_o)
    print "\n\tDEBUG: player_moves passed into 'is_winner?': #{player_moves}" if @is_debug_mode

    winning_grids.each do |win_pattern|
      print "\n\tDEBUG: Checking against win_pattern #{win_pattern}" if @is_debug_mode
      win_pattern_temp = win_pattern.dup #needed becauyse 'contains_all?' called in the next line destroys the second array passed-in
      if ( contains_all?(player_moves, win_pattern_temp) )
        winner = true
      end
    end # winning_grids.each

    print "\n\nDEBUG: winner state check: #{player_icon}\n" if @is_debug_mode

    winner
  end #  method is_winner?


  def is_odd?(number)
    ( (number % 2) == 0 ) ? false : true
  end # method is_odd?


  def contains_all? (first_array, second_array)
    #method to return true if all the elements of second_array are contained within first_array
    debug("\nlocation   : at start of 'contains_all?' method.") if @is_debug_mode

    #first_array.each { |e| if i = second_array.index(e) then second_array.delete_at(i) end}
    first_array.each do |e|
      print "\n\t\tDEBUG: first_array[e] = #{e}" if @is_debug_mode
      if i = second_array.index(e)
        second_array.delete_at(i)
        print "\n\t\t\tDEBUG: second_array[#{e}] is at index #{i} and is being deleted." if @is_debug_mode
      end
    end

    print "\n\t\tDEBUG: second_array.empty? = #{second_array.empty?}" if @is_debug_mode
    second_array.empty? #if true then
  end #  method contains_all?


  def is_valid_move?(player_move)
    #puts "Player move = #{player_move}"
    all_moves = @player_x_moves + @player_o_moves
    player_move_temp = player_move.dup #needed becauyse 'contains_all?' called in the next line destroys the second array passed-in
    not contains_all?(all_moves, player_move_temp.chars.to_a)  #need the preceeding .chars here to get the .to_a to work
  end # method is_valid_move?


  def debug(message)
    print "\n---------------------"
    print "\nDEBUG:\n#{message}"
    print "\n\tplayer_x_moves = #{@player_x_moves}"
    print "\n\tplayer_o_moves = #{@player_o_moves}\n"
    print_grid()
    print "\n---------------------"
  end # method debug(message)


  def process_move()
    all_moves = @player_x_moves + @player_o_moves
    move_count = all_moves.length + 1

    debug("move count : #{move_count}\nlocation   : at start of 'process_move' method.") if @is_debug_mode

    if ( move_count <= 9 ) # there are only nine boxes on the grid...
      is_odd?(move_count) ? ( player=@player_x ) : ( player=@player_o )
      print "\n\n#{move_count}. Player #{player}  make your move [ #{@box1_base} - #{@box9_base} , q ]: "
      player_move = gets.chomp  #todo: validate input

      if (player_move.upcase != "Q")
        if ( ("1".."9").to_a.include? (player_move) )

          begin # begin..end section needed around the following exception code because there is a trailing 'else' from the above 'if' that needs to be clearly excluded from the ensure section below
          raise DuplicateMoveError if ( not is_valid_move?(player_move) )
            (player == @player_x) ? ( @player_x_moves.push(player_move) ) : ( @player_o_moves.push(player_move) )
          rescue Exception => e
            print "\n\tInvalid move: #{get_icon("base", player_move)}  is already filled by player #{get_icon("move", player_move)} \n"
            ( print "\tDEBUG: " ; p e.backtrace.inspect if ( e.class == DuplicateMoveError ) ) if is_debug_mode
          ensure
            update_grid()
            process_move() #continue the game
          end  # begin

          # Original if-else-end logic replaced by begin..end section above.
          # if ( is_valid_move?(player_move) )
          #   (player == @player_x) ? ( @player_x_moves.push(player_move) ) : ( @player_o_moves.push(player_move) )
          #   update_grid()
          #   process_move() #continue the game
          # else
          #   print "\n\tInvalid move: #{get_icon("base", player_move)}  is already filled by player #{get_icon("move", player_move)} \n"
          #    ( print "\tDEBUG: " ; p e.backtrace.inspect if ( e.class == DuplicateMoveError ) ) if is_debug_mode
          #   update_grid()
          #   process_move() #continue the game
          # end

        else
          print "\n\tInvalid selection. Pick one of [ #{@box1_base} - #{@box9_base} , q ]\n"
          update_grid()
          process_move() #continue the game
        end # if ( ("1".."9").to_a.include? (player_move) )

      else
        print "\nQuiting game.\n\n"
        exit
      end # if (player_move.upcase != "Q")

    else
      #print "\n\nGame ended in a tie.\n\n"
      update_grid()
    end # if ( move_count <= 9 )

  end # method process_move()


  def get_icon(type, box_selection)
    case box_selection
    when "1"
        (type == "base") ? (return @box1_base) : (return @box1)
    when "2"
        (type == "base") ? (return @box2_base) : (return @box2)
    when "3"
        (type == "base") ? (return @box3_base) : (return @box3)
    when "4"
        (type == "base") ? (return @box4_base) : (return @box4)
    when "5"
        (type == "base") ? (return @box5_base) : (return @box5)
    when "6"
        (type == "base") ? (return @box6_base) : (return @box6)
    when "7"
        (type == "base") ? (return @box7_base) : (return @box7)
    when "8"
        (type == "base") ? (return @box8_base) : (return @box8)
    when "9"
        (type == "base") ? (return @box9_base) : (return @box9)
    end
  end # method get_icon

end # class Grid


class DuplicateMoveError < StandardError
  # Creating my own exception...
end # class DuplicateMoveError


# RUN
production_run = true   # set to true to have the main tic-tac-toe game run invoked... set to false to have the test runs (below) invoked...

# Main two player game...
if production_run
  # Setup start empty game grid state..
  #grid = Grid.new(production_run, [], []) # so you can start the game in any state and continue...

  # Test in-progress game state...
  print "\n\nTest game in progress..."
  player_x_moves = ["2", "4", "6", "7"]
  player_o_moves = ["1", "3", "5"]
  grid = Grid.new(production_run, player_x_moves, player_o_moves)  # so you can start the game in any state and continue...

  # Start or continue game...
  grid.process_move()
end


#Test game grid states...
if ( not production_run )

  # Test player X wins...
  print "\n\nTest player X wins..."
  player_x_moves = ["1", "2", "3"]
  player_o_moves = ["4", "5"]
  grid = Grid.new(production_run, player_x_moves, player_o_moves)  # so you can start the game in any state and continue...
  print "--------------\n"

  # Test player O wins...
  print "\n\nTest player O wins..."
  player_x_moves = ["2", "4", "6"]
  player_o_moves = ["1", "5", "9"]
  grid = Grid.new(production_run, player_x_moves, player_o_moves)  # so you can start the game in any state and continue...
  print "--------------\n"

  # Test game tie...
  print "\n\nTest game tie..."
  player_x_moves = ["2", "4", "6", "7", "9"]
  player_o_moves = ["1", "3", "5", "8"]
  grid = Grid.new(production_run, player_x_moves, player_o_moves)  # so you can start the game in any state and continue...
  print "--------------\n"

end