I am stuck on implementing the minimax algorithm for a tic tac toe game. I keep getting the same error every time (undefined method `<' for nil:NilClass (NoMethodError)) but can't seem to fix it.
My implementation is as follows:
def minimax(current_board, current_player)
if user_won?(current_board)
return -10
elsif computer_won?(current_board)
return 10
elsif tie?(current_board)
return 0
end
available_squares = empty_squares(current_board)
moves = []
available_squares.each do |available_square|
move = {}
current_board[available_square] = COMPUTER_MARKER if current_player == 'computer'
current_board[available_square] = PLAYER_MARKER if current_player == 'player'
score = minimax(current_board, alternate_player(current_player))
current_board[available_square] = INITIAL_MARKER
move[available_square] = score
moves.push(move)
end
best_move = nil
best_score = current_player == 'computer' ? -Float::INFINITY : Float:: INFINITY
if current_player == 'computer'
for hsh in moves
hsh.each do |move, score|
if score > best_score
best_move = move
best_score = score
end
end
end
else
for hsh in moves
hsh.each do |move, score|
if score < best_score
best_move = move
best_score = score
end
end
end
end
best_move
end
I have been stuck on this for days so any help would be appreciated.
Here is the rest of my code: https://github.com/YBirader/launch_school/blob/master/RB101/lesson_6/ttt.rb
The variable score has to be defined outside of the loop. Then the line
if score > best_score
will work.
I've just been through and debugged this.
It turns out that the minimax method is occasionally returning nil, which is then pushed into the hash of moves on line 94, as it can't compare nil to any number with > or < this throws an error, and we can't proceed.
You just need to add a fallback to 0 when the result is nil, change line 91 to this:
minimax(current_board, alternate_player(current_player)) || 0
You also don't need to define score outside of the loop, as that variable is only used to add it to the hash on line 94. What I'd missed earlier is that score is defined again in the each loop.
Related
I can play the game. Switch the player all working fine but not getting result who won the game.
def initialize_board
#count = 9
#player = PLAYER_ONE #current_player
#board = Array.new(3){ Array.new(3, " ") }
end
def play
inputs = get_inputs
return false if !inputs
update_board(inputs)
print_board
end
def switch_player
if(#player == PLAYER_ONE)
#player = PLAYER_TWO
else
#player = PLAYER_ONE
end
end
def game_over?
# #count = #count - 1
# #count <= 0
if check_winner
puts "#{#player} won "
end
end
def check_winner
WIN_COMBINATIONS.find do |indices|
binding.pry
values = #board.values_at(*indices)
values.all?('X') || values.all?('O')
end
end
Here I am getting indices [0,1,2] in all cases while debugging.
The main reason why you're not getting the winner is because your 'values = #board.values_at(*indices)' statement returns an array of arrays. And values.all?('X') || values.all?('O') checks not an 'X' or 'O' pattern but an array object. So you need to flatten an array first.
values.flatten!
Stefan already answered similar question , but his board was one-dimensional because of %w expression, you can read about it here
I've been working on this for a few days, at least. Testing seems to show the correct value is being returned. My problem is being able to grab the best_move value and have it print out. I set up the suggested_move method and try to use return suggested_move(best_move) but it triggers the method for every level back up the tree. Also it returns the wrong value, which I'm guessing is because it's stopping before depth is back to 0.
In my minimax I had a similar setup the difference being the depth was incremented (not decremented) on successive calls. Point being I was able to say if depth == 0 p best_move. So I'm scratching my head because using that same conditional I get a nil class error in this code.
#board = ["X","O","O","X","X","","","O",""]
def available_spaces
#board.map.with_index {|a, i| a == "" ? i+1 : nil}.compact
end
def suggested_move(move)
p "Suggested move: #{move}"
end
def full?
#board.all?{|token| token == "X" || token == "O"}
end
def suggested_move(move)
p "Move: #{move}"
end
def bestmove(board, depth=0, best_score = {})
return 0 if full?
return 1 if won?
best = -Float::INFINITY
available_spaces.each do |space|
#board[space-1] = current_player
best_score[space] = -bestmove(#board, depth-1, {})
#board[space-1] = ""
end
p best_score
if best_score.max_by {|key,value| value }[1] > best
best = best_score.max_by {|key,value| value }[1]
best_move = best_score.max_by {|key,value| value }[0]
end
return best_move
end
bestmove(#board)
I'm trying to use the minmax algorithm for a game of tic-tac-toe.
This is the code I'm working with for getting the best AI move in the game.
def get_best_move(board, next_player, depth = 0, best_score = {})
return 0 if tie(board)
return -1 if game_is_over(board)
available_spaces = []
best_move = nil
board.each do |s|
if s != "X" && s != "O"
available_spaces << s
end
end
available_spaces.each do |as|
board[as.to_i] = #com
if game_is_over(board)
best_score[as.to_i] = -1 * get_best_move(board, #com, depth + 1, {})
board[as.to_i] = as
return best_score
else
board[as.to_i] = #hum
if game_is_over(board)
best_score[as.to_i] = -1 * get_best_move(board, #com, depth + 1, {})
board[as.to_i] = as
return best_score
else
board[as.to_i] = as
end
end
end
best_move = best_score.max_by { |key, value| value }[0]
highest_minimax_score = best_score.max_by { |key, value| value }[1]
if depth == 0
return best_move
elsif depth > 0
return highest_minimax_score
end
end
I get this error when I run the game in the terminal, and I would like to know how to fix it.
rb:332:in get_best_move': undefined method `[]' for nil:NilClass (NoMethodError)
rb:332 is referring to these 2 lines in the example below
best_move = best_score.max_by { |key, value| value }[0]
highest_minimax_score = best_score.max_by { |key, value| value }[1]
The error message says pretty clearly what the problem is: you are attempting to call [] on nil, but nil doesn't respond to []. The only place where you call [] is on the result of max_by. max_by returns nil when called on an empty Enumerable. Ergo, best_score must be empty. And indeed: you always pass an empty Hash as the argument to best_score, and you never modify best_score, so it is and always will be empty.
I am doing a command line of game of Rock Paper Scissors Lizard Spock in Ruby. I have the matchup method that takes the variables #shape (the hand shape randomly selected by the game) and #player_shape (the hand shape chosen by the player).
My matchup method compares the two shapes and sets the game result to #result:
class Game
SHAPES = [:rock, :paper, :scissors, :lizard, :spock]
attr_reader :shape, :player_shape, :status
# ...
def matchup
if #shape == #player_shape
#result = :draw
else
case #player_shape
when :rock
#result = (#shape == :scissors || #shape == :lizard) ? :player_wins : :game_wins
when :paper
#result = (#shape == :rock || #shape == :spock) ? :player_wins : :game_wins
when :scissors
#result = (#shape == :paper || #shape == :lizard) ? :player_wins : :game_wins
when :lizard
#result = (#shape == :paper || #shape == :spock) ? :player_wins : :game_wins
when :spock
#result = (#shape == :rock || #shape == :scissors) ? :player_wins : :game_wins
end
end
end
# ...
end
I ran the code many times and it works as expected, but in my spec file, the result I'm getting is not matching the code behavior. Here is the spec:
describe Game do
subject(:game) { Game.new }
# ...
describe "#matchup" do
context "game chooses rock" do
before do
allow(game).to receive(:shape).and_return(:rock)
end
it "sets result as :player_wins if the game chooses paper" do
allow(game).to receive(:player_shape).and_return(:paper)
game.matchup
expect(game.result).to eq(:player_wins)
end
end
end
end
and here is the result:
Failure/Error: expect(game.result).to eq(:player_wins)
expected: :player_wins
got: :draw
(compared using ==)
Diff:
## -1,2 +1,2 ##
-:player_wins
+:draw
What I am doing wrong here? I've tried and tried and still can't figure out how to solve this problem.
Here
allow(game).to receive(:shape).and_return(:rock)
and there
allow(game).to receive(:player_shape).and_return(:paper)
You're stubbing methods Game#shape and Game#player_shape for game object.
But you're using #shape == #player_shape as condition in if statement.
Ruby treats undefined instance variables (those which starts from #) as nil by default.
→ irb
irb(main):001:0> #shape
=> nil
Thus #shape == #player_shape is the same as nil == nil in your case. Which is actually a true. What's why program flow reach #result = :draw line.
To make it work you need to define shape and player_shape methods by using attr_reader, for example, and use them instead of instance variables.
upd
Now replace #shape with shape and #player_shape with player_shape in your code. Be sure you initialise values for them.
Take a look at that Ruby Monk tutorial (or any other articles) about instance vairables to understand how they work
Don't understand why #nums.pop won't work in the value method. It seems to tell me that it can't do that for nil, but if I just say #nums, it shows that there is indeed something in the array. So then why can't I pop it out?
class RPNCalculator
def initialize
#value = value
nums ||= []
#nums = nums
end
def push(num)
#nums << num
end
def plus
if #nums[-2] == nil || #nums[-1] == nil
raise "calculator is empty"
else
#value = #nums.pop + #nums.pop
#nums.push(#value)
end
end
def minus
if #nums[-2] == nil || #nums[-1] == nil
raise "calculator is empty"
else
#value = #nums[-2] - #nums[-1]
#nums.pop(2)
#nums.push(#value)
end
end
def divide
if #nums[-2] == nil || #nums[-1] == nil
raise "calculator is empty"
else
#value = #nums[-2].to_f / #nums[-1].to_f
#nums.pop(2)
#nums.push(#value)
end
end
def times
if #nums[-2] == nil || #nums[-1] == nil
raise "calculator is empty"
else
#value = #nums.pop.to_f * #nums.pop.to_f
#nums.push(#value)
end
end
def value
#nums #Don't understand why #nums.pop won't work here
end
def tokens(str)
str.split(" ").map { |char| (char.match(/\d/) ? char.to_i : char.to_sym)}
end
def evaluate(str)
tokens(str).each do |x|
if x == ":-"
minus
elsif x == ":+"
plus
elsif x == ":/"
divide
elsif x ==":*"
times
else
push(x)
end
end
value
end
end
Error relates to the following part of a spec:
it "adds two numbers" do
calculator.push(2)
calculator.push(3)
calculator.plus
calculator.value.should == 5
end
Error says either:
Failure/Error: calculator.value.should == 5
expected: 5
got: [5] <using ==>
OR if .pop is used
Failure/Error: #calculator = RPNCalculator.new
NoMethodError:
undefined method 'pop' for nil:NilClass
Your initialize method assigning #value = value calls the function at def value which returns #nums which has not yet been created in initialize since #nums is created afterwards with nums ||= []; #nums = nums therefore it's nil. This is why .pop won't work.
You've created #nums as an array with nums ||= [] and you're using it with push and pop so why are you checking for the value with value.should == 5 (Integer) when calling value returns an (Array). You would need to write it like value.first.should == 5 or value[0].should == 5 ... otherwise you should change value to return just the element you want
def value
#nums.pop # or #nums[0], or #nums.first or #nums.last however you plan on using it
end
The problem is #value = value in your initialize method. Fix that then you can add the .pop in value.
EDIT
Also your evaluation is calling methods before you've populated #nums with the values. Then the methods "raise" errors. You can't call minus after only one value has been pushed to #nums.
Here's how I would do the flow for splitting the string
# Multiplication and Division need to happen before addition and subtraction
mylist = "1+3*7".split(/([+|-])/)
=> ["1", "+", "3*7"]
# Compute multiplication and division
mylist = mylist.map {|x| !!(x =~ /[*|\/]/) ? eval(x) : x }
=> ["1", "+", 21]
# Do the rest of the addition
eval mylist.join
=> 22
I realize this isn't exactly how you're going about solving this... but I think splitting by order of mathematical sequence will be the right way to go. So first evaluate everything between (), then only multiplication and division, then all addition and subtraction.
EDIT I just looked into what a RPN Calculator is. So don't mind my splitting recommendation as it doesn't apply.