Tic-tac-toe: Difference between revisions
Content added Content deleted
(→{{header|Ruby}}: improve AI’s defense; refactor a lot; make more idiomatic; make board printout look nicer) |
|||
Line 5,245: | Line 5,245: | ||
=={{header|Ruby}}== |
=={{header|Ruby}}== |
||
<lang ruby> |
<lang ruby>require 'set' |
||
ROWS = [[1,2,3],[4,5,6],[7,8,9],[1,4,7],[2,5,8],[3,6,9],[1,5,9],[3,5,7]] |
|||
module TicTacToe |
|||
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]] |
|||
class Game |
class Game |
||
def initialize( |
def initialize(player_1_class, player_2_class) |
||
@board = Array.new(10) |
@board = Array.new(10) # we ignore index 0 for convenience |
||
@free_positions = (1..9) |
@free_positions = Set.new(1..9) |
||
@players = [player1Class.new(self), player2Class.new(self)] |
|||
@players = [player_1_class.new(self), player_2_class.new(self)] |
|||
@turn = rand(2) |
|||
@current_player_id = 0 |
|||
puts "#{@players[@turn]} goes first." |
|||
@players[@ |
@players[@current_player_id].marker = "X" |
||
@players[ |
@players[other_player_id].marker = "O" |
||
puts "#{@players[@current_player_id]} goes first." |
|||
end |
end |
||
attr_reader : |
attr_reader :board, :free_positions, :current_player_id |
||
def play |
def play |
||
loop do |
loop do |
||
player = @players[@ |
player = @players[@current_player_id] |
||
idx = player.select |
|||
puts "#{player} selects #{player.marker} position #{idx}" |
|||
@board[idx] = player.marker |
|||
@free_positions.delete(idx) |
|||
place_player_marker(player) |
|||
ROWS.each do |row| |
|||
if row.all? {|idx| @board[idx] == player.marker} |
|||
puts "#{player} wins!" |
|||
print_board |
|||
return |
|||
end |
|||
end |
|||
if player_has_won?(player) |
|||
puts "#{player} wins!" |
|||
print_board |
|||
return |
|||
elsif board_full? |
|||
puts "It's a draw." |
puts "It's a draw." |
||
print_board |
print_board |
||
Line 5,284: | Line 5,279: | ||
end |
end |
||
switch_players! |
|||
end |
end |
||
end |
end |
||
def |
def place_player_marker(player) |
||
position = player.select_position |
|||
puts "#{player} selects #{player.marker} position #{position}" |
|||
@board[position] = player.marker |
|||
@free_positions.delete(position) |
|||
end |
|||
def player_has_won?(player) |
|||
LINES.any? do |line| |
|||
line.all? {|position| @board[position] == player.marker} |
|||
end |
|||
end |
|||
def board_full? |
|||
@free_positions.empty? |
|||
end |
|||
def other_player_id |
|||
1 - @current_player_id |
|||
end |
end |
||
def |
def switch_players! |
||
@ |
@current_player_id = other_player_id |
||
end |
end |
||
def opponent |
def opponent |
||
@players[ |
@players[other_player_id] |
||
end |
|||
def turn_num |
|||
10 - @free_positions.size |
|||
end |
end |
||
def print_board |
def print_board |
||
col_separator, row_separator = " | ", "--+---+--" |
|||
display =lambda{|row| row.map {|i| @board[i] ? @board[i] : i}.join("|")} |
|||
label_for_position = lambda{|position| @board[position] ? @board[position] : position} |
|||
puts display[[1,2,3]], "-+-+-", display[[4,5,6]], "-+-+-", display[[7,8,9]] |
|||
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") |
|||
end |
end |
||
end |
end |
||
Line 5,315: | Line 5,337: | ||
class HumanPlayer < Player |
class HumanPlayer < Player |
||
def |
def select_position |
||
@game.print_board |
@game.print_board |
||
loop do |
loop do |
||
Line 5,331: | Line 5,353: | ||
class ComputerPlayer < Player |
class ComputerPlayer < Player |
||
DEBUG = false # edit this line if necessary |
|||
def group_row(row) |
|||
markers = row.group_by {|idx| @game.board[idx]} |
|||
def group_positions_by_markers(line) |
|||
markers = line.group_by {|position| @game.board[position]} |
|||
markers.default = [] |
markers.default = [] |
||
markers |
markers |
||
end |
end |
||
def |
def select_position |
||
opponent_marker = @game.opponent.marker |
opponent_marker = @game.opponent.marker |
||
winning_or_blocking_position = look_for_winning_or_blocking_position(opponent_marker) |
|||
# look for winning rows |
|||
return winning_or_blocking_position if winning_or_blocking_position |
|||
for row in ROWS |
|||
markers = group_row(row) |
|||
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) |
|||
next if markers[nil].length != 1 |
next if markers[nil].length != 1 |
||
if markers[self.marker].length == 2 |
if markers[self.marker].length == 2 |
||
log_debug "winning on line #{line.join}" |
|||
return markers[nil].first |
return markers[nil].first |
||
elsif markers[opponent_marker].length == 2 |
elsif markers[opponent_marker].length == 2 |
||
log_debug "could block on line #{line.join}" |
|||
blocking_position = markers[nil].first |
|||
end |
end |
||
end |
end |
||
if blocking_position |
|||
log_debug "blocking at #{blocking_position}" |
|||
# look for opponent's winning rows to block |
|||
return blocking_position |
|||
end |
|||
end |
|||
# need some logic here to get the computer to pick a smarter position |
|||
def corner_trap_defense_needed? |
|||
# simply pick a position in order of preference |
|||
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 |
|||
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 |
|||
def random_prioritized_position |
|||
log_debug "picking random position, favoring center and then corners" |
|||
([5] + [1,3,7,9].shuffle + [2,4,6,8].shuffle).find do |pos| |
([5] + [1,3,7,9].shuffle + [2,4,6,8].shuffle).find do |pos| |
||
@game.free_positions.include?(pos) |
@game.free_positions.include?(pos) |
||
end |
end |
||
end |
|||
def log_debug(message) |
|||
puts "#{self}: #{message}" if DEBUG |
|||
end |
end |
||
def to_s |
def to_s |
||
"Computer#{@game. |
"Computer#{@game.current_player_id}" |
||
end |
end |
||
end |
end |
||
Line 5,372: | Line 5,430: | ||
Game.new(ComputerPlayer, ComputerPlayer).play |
Game.new(ComputerPlayer, ComputerPlayer).play |
||
puts |
puts |
||
players_with_human = [HumanPlayer, ComputerPlayer].shuffle |
|||
Game.new(*players_with_human).play</lang> |
|||
sample output |
sample output |
||
<pre> |
<pre> |
||
Computer0 goes first. |
|||
Computer0 selects X position 5 |
|||
Computer1 selects O position 9 |
|||
Computer0 selects X position 3 |
|||
Computer1 selects O position 7 |
|||
Computer0 selects X position 8 |
|||
Computer1 selects O position 2 |
|||
Computer0 selects X position 1 |
|||
Computer1 selects O position 4 |
|||
Computer0 selects X position 6 |
|||
It's a draw. |
It's a draw. |
||
X | O | X |
|||
-+-+- |
--+---+-- |
||
O | X | X |
|||
-+-+- |
--+---+-- |
||
O | X | O |
|||
Human goes first. |
Human goes first. |
||
1|2|3 |
1 | 2 | 3 |
||
-+-+- |
--+---+-- |
||
4|5|6 |
4 | 5 | 6 |
||
-+-+- |
--+---+-- |
||
7|8|9 |
7 | 8 | 9 |
||
Select your X position: |
Select your X position: 3 |
||
Human selects X position |
Human selects X position 3 |
||
Computer1 selects O position 6 |
|||
1 | 2 | X |
|||
--+---+-- |
|||
4 | 5 | O |
|||
--+---+-- |
|||
7 | 8 | 9 |
|||
Select your X position: 7 |
|||
Human selects X position 7 |
|||
Computer1 selects O position 5 |
Computer1 selects O position 5 |
||
1 | 2 | X |
|||
-+-+- |
--+---+-- |
||
4|O| |
4 | O | O |
||
-+-+- |
--+---+-- |
||
X | 8 | 9 |
|||
Select your X position: 4 |
|||
Human selects X position 4 |
|||
Computer1 selects O position 1 |
|||
O | 2 | X |
|||
--+---+-- |
|||
X | O | O |
|||
--+---+-- |
|||
X | 8 | 9 |
|||
Select your X position: 9 |
Select your X position: 9 |
||
Human selects X position 9 |
Human selects X position 9 |
||
Computer1 selects O position |
Computer1 selects O position 8 |
||
O | 2 | X |
|||
-+-+- |
--+---+-- |
||
X | O | O |
|||
-+-+- |
--+---+-- |
||
X | O | X |
|||
Select your X position: |
Select your X position: 2 |
||
Human selects X position |
Human selects X position 2 |
||
It's a draw. |
|||
Computer1 selects O position 4 |
|||
O | X | X |
|||
X|2|O |
|||
-+-+- |
--+---+-- |
||
X | O | O |
|||
-+-+- |
--+---+-- |
||
X| |
X | O | X</pre> |
||
Select your X position: 8 |
|||
Human selects X position 8 |
|||
Human wins! |
|||
X|2|O |
|||
-+-+- |
|||
O|O|6 |
|||
-+-+- |
|||
X|X|X |
|||
</pre> |
|||
=={{header|Run BASIC}}== |
=={{header|Run BASIC}}== |