I'm wondering if anybody can help me out with this...I'm sure it's one of those forehead-slapping things...but I can't figure out how to DRY this bit up.
I'm making a Tic-Tac-Toe program as a learning exercise. I'm actually finished, but there's a bit that bothers me because it seems obviously clumsy. The part comes when I'm doing the AI for the computer's move. The way this works is, you test each of eight conditions looking for a possible move. You look at the first condition; if it results in a move, return that move and skip over the rest of the conditions. If none of the eight conditions elicits a move, then the game is over (the board is full).
Well, the conditions are complex enough that they each require a separate method. So, to execute the logic, I say simply 1.times
(sigh) and then in a single once-only block, go through these methods one by one. After checking to see if the method returns a move, I say break if move
, i.e., break out of the block. It seems obvious I shouldn't have to use break if move
after each item. Violates DRY. I thought of using if...elsif...
or a switch
statement, but that wouldn't work because all of the methods have to be gone through, until one of them returns a move, and the methods don't just return true or false; they actually return either the index number of the move or else false
.
(The unless skip_rule == n
bits are a way to dumb down the AI; a different method selects a rule to skip, if the player wants the AI to be beatable.)
If you want to dive in, check the code at line 96+ here: https://github.com/globewalldesk/tictactoe2/blob/master/lib/board.rb
# Test each set of conditions, until a move is found
move = false
1.times do
# Win: If you have two in a row, play the third to get three in a row.
# puts "Trying 1"
move, length = are_there_two_tokens_in_a_row(@ctoken) unless skip_rule == 0
break if move # skip to end if move is found
# Block: If the opponent has two in a row, play the third to block them.
# puts "Trying 2"
move, length = are_there_two_tokens_in_a_row(@ptoken) unless skip_rule == 1
break if move
# Fork: Create an opportunity where you can win in two ways (a fork).
# puts "Trying 3"
move = discover_fork(@ctoken) unless skip_rule == 2
break if move
# Block Opponent's Fork: If opponent can create fork, block that fork.
# puts "Trying 4"
move = discover_fork(@ptoken) unless skip_rule == 3
break if move
# Center: Play the center.
# puts "Trying 5"
unless skip_rule == 4
move = 4 if @spaces[4].c == " " # if the center is open, move there
end
break if move
# Opposite Corner: If the opponent is in the corner, play the opposite corner.
# puts "Trying 6"
move = try_opposite_corner unless skip_rule == 5
break if move
# Empty Corner: Play an empty corner.
# puts "Trying 7"
move = try_empty_corner
break if move
# Empty Side: Play an empty side.
# puts "Trying 8"
move = play_empty_side
# If move is still false, game is over!
end # of "do" block
# Make the change to @spaces; this edits the individual space and hence also
# the triads and the board, which use it.
@spaces[move].c = @ctoken if move
end # of computer_moves
UPDATE (12/3/2016): I wasn't quite able to understand the advice I got below. But a friend offline looked at the problem and suggested that I make an array of the methods and iterate over that...and that's the sound of my hand hitting my forehead. I knew there was a (relatively) simple way to do it. Anyway, an array of methods might not be possible in Ruby, but it is possible to make an array of procs that contains methods. So that's what I did. Here's the corrected/improved code:
# Initialize array of procs for each step of algorithm
rules = [
# Win: If you have two in a row, play the third to get three in a row.
Proc.new { are_there_two_tokens_in_a_row(@ctoken) },
# Block: If the opponent has two in a row, play the third to block them.
Proc.new { are_there_two_tokens_in_a_row(@ptoken) },
# Fork: Create an opportunity where you can win in two ways (a fork).
Proc.new { discover_fork(@ctoken) },
# Block Opponent's Fork: If opponent can create fork, block that fork.
Proc.new { discover_fork(@ctoken) },
# Center: Play the center.
Proc.new {
unless skip_rule == 4
move = 4 if @spaces[4].c == " " # if the center is open, move there
end
},
# Opposite Corner: If the opponent is in the corner, play the opposite corner.
Proc.new { try_opposite_corner },
# Empty Corner: Play an empty corner.
Proc.new { try_empty_corner },
# Empty Side: Play an empty side.
Proc.new { play_empty_side }
]
# Iterates over rule procs, and breaks out of iteration when move != false
(0..7).each do |rule_index|
move, length = rules[rule_index].call unless skip_rule == rule_index
break if move
end
Pretty, n'est-ce pas? This bit of code DRYed up, as far as I can tell. Here are the program's updated source files. BTW the program can be set to be unbeatable, but the user can have it "forget" random rules which makes it beatable.
Thanks to all who replied. Next time I'll try on Code Review.SE.