I have implemented Tic-Tac-Toe so that human can play with the computer, where the computer should never lose. I did a simple analysis before implementing, and I found out that there are certain cells you want to occupy (I named it BEST_CELLS).
Here is the link to the implementation: https://github.com/yangtheman/tictactoe
I have five classes:
Player
: human or CPUTicTacToe
: holds board instance variableTicTacToeGame
: gets user input and make CPU moveTicTacToePrint
: prints out the boardTicTacToeScan
: scan the board for any two in a rows and three in a row (column/row/diagonal)
I don't feel that my class design is optimal and scanning algorithms can be improved. I also used hashes for the board for faster look-up, but perhaps using arrays would be easier? I am looking for feedback on my design and algorithm.
tic_tac_toe.rb
class TicTacToe
X_COORDS = ["A", "B", "C"]
Y_COORDS = ["1", "2", "3"]
attr_reader :board
def initialize
@best_cells = ["B2", "A1", "A3", "C1", "C3"]
@board = {}
X_COORDS.each do |x|
@board[x] = {}
end
end
def x_coords
X_COORDS
end
def y_coords
Y_COORDS
end
def place_marker(coord_string, player)
x, y = coord_string.upcase.split("")
return nil unless coord_valid?(x, y)
deduct_from_best_cells(x, y)
@board[x][y] = player
end
def best_cells_left
@best_cells
end
def board_full?
sum = X_COORDS.inject(0) {|sum, x| sum += @board[x].size}
sum == 9
end
def player_cell?(x, y, player)
@board[x][y] == player
end
def empty_cell?(x, y)
@board[x][y].nil?
end
def cell_marker(x, y)
@board[x][y].nil? ? "." : @board[x][y].marker
end
private
def coord_valid?(x, y)
within_range?(x, y) && @board[x][y].nil?
end
def within_range?(x, y)
X_COORDS.include?(x) && Y_COORDS.include?(y)
end
def deduct_from_best_cells(x, y)
@best_cells -= ["#{x}#{y}"]
end
end
tic_tac_toe_game.rb
require_relative './player'
require_relative './tic_tac_toe'
require_relative './tic_tac_toe_scan'
require_relative './tic_tac_toe_print'
class TicTacToeGame
def initialize
@board = TicTacToe.new
@cpu = Player.new
@human = Player.new(false)
end
def play
print_initial_instruction
interact_with_human
play if continue_to_play?
end
def print_board
TicTacToePrint.print_board(@board)
end
def scan_board(player)
TicTacToeScan.new(@board, player)
end
private
def print_initial_instruction
puts "Welcome to a Tic-Tac-Toe Game!\nYou are playing against the computer. Try to win."
puts "CPU marker is #{@cpu.marker}\nYour marker is #{@human.marker}"
print_board
end
def interact_with_human
loop do
if human_turn
break if game_over?
else
puts "Invalid Move. Please try again."
end
end
end
def game_over?
human_scan = scan_board(@human)
return true if game_finished?(human_scan)
cpu_scan = scan_board(@cpu)
cpu_turn(cpu_scan, human_scan)
print_board
return true if game_finished?(scan_board(@cpu))
end
def human_turn
print "Your Next Move (for example A1 or C3): "
input = STDIN.gets.chomp().upcase
@board.place_marker(input, @human)
end
def cpu_turn(cpu_scan, human_scan)
cpu_cell = calculate_cpu_cell(cpu_scan, human_scan)
@board.place_marker(cpu_cell, @cpu)
puts "CPU put his/her marker on #{cpu_cell}"
end
def calculate_cpu_cell(cpu_scan, human_scan)
playable_cells = cpu_scan.get_playable_cells
to_block = human_scan.get_playable_cells
if playable_cells[2] && playable_cells[2].length > 0
cpu_cell = playable_cells[2].first
elsif to_block[2] && to_block[2].length > 0
cpu_cell = to_block[2].first
elsif @board.best_cells_left.length > 0
cpu_cell = @board.best_cells_left.first
elsif playable_cells[1] && playable_cells[1].length > 0
cpu_cell = playable_cells[1].first
else
cpu_cell = playable_cells[0].first
end
cpu_cell
end
def game_finished?(scan)
if scan.winner?
if scan.player == @human
puts "Congratulations, You Won!"
else
puts "Sorry. You Lost!"
end
return true
elsif @board.board_full?
puts "Awwwww. No one won! Game is tied!"
return true
end
false
end
def continue_to_play?
print "Would you like to play again? (Y or N): "
if STDIN.gets.chomp() =~ /Y|y/
@board = TicTacToe.new
return true
end
false
end
end
tic_tac_toe_scan.rb
class TicTacToeScan
attr_reader :player, :playable_cells
def initialize(game, player)
@game = game
@player = player
@playable_cells = {}
end
def get_playable_cells
calculate_playable_cells
@playable_cells
end
def winner?
calculate_playable_cells if @playable_cells == {}
@playable_cells[3] && @playable_cells[3] == []
end
private
def calculate_playable_cells
scan_rows
scan_cols
scan_diag_w2e
scan_diag_e2w
end
def add_to_playable_cells(num, array)
@playable_cells[num] ||= []
@playable_cells[num] += array
@playable_cells[num].uniq!
end
def scan_rows
@game.y_coords.each do |y|
player_cell_num = 0
empty_cells = []
@game.x_coords.each do |x|
empty_cells << "#{x}#{y}" if @game.empty_cell?(x, y)
player_cell_num += 1 if @game.player_cell?(x, y, @player)
end
add_to_playable_cells(player_cell_num, empty_cells)
end
end
def scan_cols
@game.x_coords.each do |x|
player_cell_num = 0
empty_cells = []
@game.y_coords.each do |y|
empty_cells << "#{x}#{y}" if @game.empty_cell?(x, y)
player_cell_num += 1 if @game.player_cell?(x, y, @player)
end
add_to_playable_cells(player_cell_num, empty_cells)
end
end
def scan_diag_w2e
player_cell_num = 0
empty_cells = []
@game.x_coords.each_with_index do |x, index|
y = @game.y_coords[index]
empty_cells << "#{x}#{y}" if @game.empty_cell?(x, y)
player_cell_num += 1 if @game.player_cell?(x, y, @player)
end
add_to_playable_cells(player_cell_num, empty_cells)
end
def scan_diag_e2w
player_cell_num = 0
empty_cells = []
@game.x_coords.each_with_index do |x, index|
y = @game.y_coords.reverse[index]
empty_cells << "#{x}#{y}" if @game.empty_cell?(x, y)
player_cell_num += 1 if @game.player_cell?(x, y, @player)
end
add_to_playable_cells(player_cell_num, empty_cells)
end
end