RosettaCodeData/Task/Tic-tac-toe/Ruby/tic-tac-toe.rb

189 lines
5.1 KiB
Ruby
Raw Permalink Normal View History

2015-02-20 00:35:01 -05:00
require 'set'
2013-04-11 01:07:29 -07:00
module TicTacToe
2015-02-20 00:35:01 -05:00
LINES = [[1,2,3],[4,5,6],[7,8,9],[1,4,7],[2,5,8],[3,6,9],[1,5,9],[3,5,7]]
2013-04-11 01:07:29 -07:00
class Game
2015-02-20 00:35:01 -05:00
def initialize(player_1_class, player_2_class)
@board = Array.new(10) # we ignore index 0 for convenience
@current_player_id = 0
@players = [player_1_class.new(self, "X"), player_2_class.new(self, "O")]
puts "#{current_player} goes first."
2013-04-11 01:07:29 -07:00
end
2015-02-20 00:35:01 -05:00
attr_reader :board, :current_player_id
2013-04-11 01:07:29 -07:00
def play
loop do
2015-02-20 00:35:01 -05:00
place_player_marker(current_player)
2013-04-11 01:07:29 -07:00
2015-02-20 00:35:01 -05:00
if player_has_won?(current_player)
puts "#{current_player} wins!"
print_board
return
elsif board_full?
2013-04-11 01:07:29 -07:00
puts "It's a draw."
2014-01-17 05:32:22 +00:00
print_board
2013-04-11 01:07:29 -07:00
return
end
2015-02-20 00:35:01 -05:00
switch_players!
end
end
def free_positions
Set.new((1..9).select {|position| @board[position].nil?})
end
def place_player_marker(player)
position = player.select_position!
puts "#{player} selects #{player.marker} position #{position}"
@board[position] = player.marker
end
def player_has_won?(player)
LINES.any? do |line|
line.all? {|position| @board[position] == player.marker}
2013-04-11 01:07:29 -07:00
end
end
2015-02-20 00:35:01 -05:00
def board_full?
free_positions.empty?
end
def other_player_id
1 - @current_player_id
end
def switch_players!
@current_player_id = other_player_id
2013-04-11 01:07:29 -07:00
end
2015-02-20 00:35:01 -05:00
def current_player
@players[current_player_id]
2013-04-11 01:07:29 -07:00
end
def opponent
2015-02-20 00:35:01 -05:00
@players[other_player_id]
end
def turn_num
10 - free_positions.size
2013-04-11 01:07:29 -07:00
end
2014-01-17 05:32:22 +00:00
def print_board
2015-02-20 00:35:01 -05:00
col_separator, row_separator = " | ", "--+---+--"
label_for_position = lambda{|position| @board[position] ? @board[position] : position}
row_for_display = lambda{|row| row.map(&label_for_position).join(col_separator)}
row_positions = [[1,2,3], [4,5,6], [7,8,9]]
rows_for_display = row_positions.map(&row_for_display)
puts rows_for_display.join("\n" + row_separator + "\n")
2013-04-11 01:07:29 -07:00
end
end
class Player
2015-02-20 00:35:01 -05:00
def initialize(game, marker)
2013-04-11 01:07:29 -07:00
@game = game
2015-02-20 00:35:01 -05:00
@marker = marker
2013-04-11 01:07:29 -07:00
end
2015-02-20 00:35:01 -05:00
attr_reader :marker
2013-04-11 01:07:29 -07:00
end
class HumanPlayer < Player
2015-02-20 00:35:01 -05:00
def select_position!
2014-01-17 05:32:22 +00:00
@game.print_board
2013-04-11 01:07:29 -07:00
loop do
print "Select your #{marker} position: "
2014-01-17 05:32:22 +00:00
selection = gets.to_i
return selection if @game.free_positions.include?(selection)
puts "Position #{selection} is not available. Try again."
2013-04-11 01:07:29 -07:00
end
end
def to_s
"Human"
end
end
class ComputerPlayer < Player
2015-02-20 00:35:01 -05:00
DEBUG = false # edit this line if necessary
def group_positions_by_markers(line)
markers = line.group_by {|position| @game.board[position]}
2014-01-17 05:32:22 +00:00
markers.default = []
2013-04-11 01:07:29 -07:00
markers
end
2015-02-20 00:35:01 -05:00
def select_position!
2014-01-17 05:32:22 +00:00
opponent_marker = @game.opponent.marker
2013-04-11 01:07:29 -07:00
2015-02-20 00:35:01 -05:00
winning_or_blocking_position = look_for_winning_or_blocking_position(opponent_marker)
return winning_or_blocking_position if winning_or_blocking_position
if corner_trap_defense_needed?
return corner_trap_defense_position(opponent_marker)
end
# could make this smarter by sometimes doing corner trap offense
return random_prioritized_position
end
def look_for_winning_or_blocking_position(opponent_marker)
for line in LINES
markers = group_positions_by_markers(line)
2014-01-17 05:32:22 +00:00
next if markers[nil].length != 1
if markers[self.marker].length == 2
2015-02-20 00:35:01 -05:00
log_debug "winning on line #{line.join}"
2013-04-11 01:07:29 -07:00
return markers[nil].first
2014-01-17 05:32:22 +00:00
elsif markers[opponent_marker].length == 2
2015-02-20 00:35:01 -05:00
log_debug "could block on line #{line.join}"
blocking_position = markers[nil].first
2013-04-11 01:07:29 -07:00
end
end
2015-02-20 00:35:01 -05:00
if blocking_position
log_debug "blocking at #{blocking_position}"
return blocking_position
end
end
2013-04-11 01:07:29 -07:00
2015-02-20 00:35:01 -05:00
def corner_trap_defense_needed?
corner_positions = [1, 3, 7, 9]
opponent_chose_a_corner = corner_positions.any?{|pos| @game.board[pos] != nil}
return @game.turn_num == 2 && opponent_chose_a_corner
end
2013-04-11 01:07:29 -07:00
2015-02-20 00:35:01 -05:00
def corner_trap_defense_position(opponent_marker)
# if you respond in the center or the opposite corner, the opponent can force you to lose
log_debug "defending against corner start by playing adjacent"
# playing in an adjacent corner could also be safe, but would require more logic later on
opponent_position = @game.board.find_index {|marker| marker == opponent_marker}
safe_responses = {1=>[2,4], 3=>[2,6], 7=>[4,8], 9=>[6,8]}
return safe_responses[opponent_position].sample
end
2013-04-11 01:07:29 -07:00
2015-02-20 00:35:01 -05:00
def random_prioritized_position
log_debug "picking random position, favoring center and then corners"
2014-01-17 05:32:22 +00:00
([5] + [1,3,7,9].shuffle + [2,4,6,8].shuffle).find do |pos|
@game.free_positions.include?(pos)
2013-04-11 01:07:29 -07:00
end
end
2015-02-20 00:35:01 -05:00
def log_debug(message)
puts "#{self}: #{message}" if DEBUG
end
2013-04-11 01:07:29 -07:00
def to_s
2015-02-20 00:35:01 -05:00
"Computer#{@game.current_player_id}"
2013-04-11 01:07:29 -07:00
end
end
end
2014-01-17 05:32:22 +00:00
include TicTacToe
Game.new(ComputerPlayer, ComputerPlayer).play
puts
2015-02-20 00:35:01 -05:00
players_with_human = [HumanPlayer, ComputerPlayer].shuffle
Game.new(*players_with_human).play