#!/usr/bin/env ruby #--- # Visit http://www.pragmaticprogrammer.com/titles/fr_quiz #--- module TicTacToe module SquaresContainer def []( index ) @squares[index] end def blanks() @squares.find_all { |s| s == " " }.size end def os() @squares.find_all { |s| s == "O" }.size end def xs() @squares.find_all { |s| s == "X" }.size end end class Board class Row def initialize( squares, names ) @squares = squares @names = names end include SquaresContainer def to_board_name( index ) Board.index_to_name(@names[index]) end end MOVES = %w{a1 a2 a3 b1 b2 b3 c1 c2 c3} # Define constant INDICES INDICES = Hash.new { |h, k| h[k] = MOVES.find_index(k) } def self.name_to_index( name )# Receives "b2" and returns 4 INDICES[name] end def self.index_to_name( index ) # Receives the index, like 4 and returns "b2" MOVES[index] end def initialize( squares ) @squares = squares # An array of Strings: [ " ", " ", " ", " ", "X", " ", " ", " ", "O"] end include SquaresContainer def []( *indices ) if indices.size == 2 # board[1,2] is @squares[7] super indices[0] + indices[1] * 3 # calls SquaresContainer [] method elsif indices[0].is_a? Fixnum # board[7] super indices[0] else # board["b2"] super Board.name_to_index(indices[0].to_s) end end def []=(indice, value) # board["b2"] = "X" m = Board.name_to_index(indice) @squares[m] = value end HORIZONTALS = [ [0, 1, 2], [3, 4, 5], [6, 7, 8] ] COLUMNS = [ [0, 3, 6], [1, 4, 7], [2, 5, 8] ] DIAGONALS = [ [0, 4, 8], [2, 4, 6] ] ROWS = HORIZONTALS + COLUMNS + DIAGONALS def each_row ROWS.each do |e| yield Row.new(@squares.values_at(*e), e) end end def moves moves = [ ] @squares.each_with_index do |s, i| moves << Board.index_to_name(i) if s == " " end moves # returns the set of feasible moves [ "b3", "c2", ... ] end def won? each_row do |row| return "X" if row.xs == 3 # "X" wins return "O" if row.os == 3 # "O" wins end return " " if blanks == 0 # tie false end BOARD =<<EOS +---+---+---+ a | 0 | 1 | 2 | +---+---+---+ b | 3 | 4 | 5 | +---+---+---+ c | 6 | 7 | 8 | +---+---+---+ 1 2 3 EOS def to_s BOARD.gsub(/(\d)(?= \|)/) { |i| @squares[i.to_i] } end end end module TicTacToe class Player def initialize( mark ) @mark = mark # "X" or "O" or " " end attr_reader :mark def move( board ) raise NotImplementedError, "Player subclasses must define move()." end def finish( final_board ) end end end module TicTacToe class HumanPlayer < Player def move( board ) print board moves = board.moves print "Your move? (format: b3) " move = $stdin.gets until moves.include?(move.chomp.downcase) print "Invalid move. Try again. " move = $stdin.gets end move.chomp end def finish( final_board ) print final_board if final_board.won? == @mark print "Congratulations, you win.\n\n" elsif final_board.won? == " " print "Tie game.\n\n" else print "You lost tic-tac-toe?!\n\n" end end end end module TicTacToe class DumbPlayer < Player def move( board ) moves = board.moves moves[rand(moves.size)] end end class SmartPlayer < Player def move( board ) moves = board.moves # If I have a win, take it. If he is threatening to win, stop it. board.each_row do |row| if row.blanks == 1 and (row.xs == 2 or row.os == 2) (0..2).each do |e| return row.to_board_name(e) if row[e] == " " end end end # Take the center if open. return "b2" if moves.include? "b2" # Defend opposite corners. if board[0] != @mark and board[0] != " " and board[8] == " " return "c3" elsif board[8] != @mark and board[8] != " " and board[0] == " " return "a1" elsif board[2] != @mark and board[2] != " " and board[6] == " " return "c1" elsif board[6] != @mark and board[6] != " " and board[2] == " " return "a3" end # Defend against the special case XOX on a diagonal. if board.xs == 2 and board.os == 1 and board[4] == "O" and (board[0] == "X" and board[8] == "X") or (board[2] == "X" and board[6] == "X") return %w{a2 b1 b3 c2}[rand(4)] end # Or make a random move. moves[rand(moves.size)] end end end module TicTacToe class Game def initialize( player1, player2, random = true ) if random and rand(2) == 1 @x_player = player2.new("X") @o_player = player1.new("O") else @x_player = player1.new("X") @o_player = player2.new("O") end @board = Board.new([" "] * 9) end attr_reader :x_player, :o_player def play until @board.won? @board[@x_player.move(@board)] = @x_player.mark break if @board.won? @board[@o_player.move(@board)] = @o_player.mark end @o_player.finish @board @x_player.finish @board end end end if __FILE__ == $0 if ARGV.size > 0 and ARGV[0] == "-d" game = TicTacToe::Game.new TicTacToe::HumanPlayer, TicTacToe::DumbPlayer else game = TicTacToe::Game.new TicTacToe::HumanPlayer, TicTacToe::SmartPlayer end game.play end