Reputation: 1807
I'm comparing a given word against user's guesses for a hangman game. word
is a string that the user wants to guess, guesses
is a string that's concatenated with letters the user guessed correctly.
def initialize(word)
@word = word
@guesses = ''
@wrong_guesses = ''
end
I'm using if word.delete(guesses).empty?
to determine the win condition. This code is run for every guess, so there's no way that I can find where the user guessed all the correct letters, and word.delete(guesses)
doesn't evaluate to empty.
def check_win_or_lose
if @word.delete(@guesses).empty?
#is this a better choice?
#if @word == self.word_with_guesses
return :win
elsif @wrong_guesses.length >= 7
return :lose
else
return :play
end
end
It works, but I feel this is a bit hacky, using a side effect of delete
. I would like to know if there's a better, concise, way to do this. Maybe using some regex?
There's a function that I'm already using for another part of the program; not sure if its faster/better than the .delete.empty?
method. It returns word
with '-'
in place of unguessed letters:
def word_with_guesses
displayed = @word
@guesses.length > 0 ? displayed.gsub(/[^#{guesses}]/i, '-') : displayed.gsub(/./, '-')
end
Upvotes: 2
Views: 943
Reputation: 106147
I guess I'd do it this way:
word.chars.sort == guesses.sort
Edit: Upon further consideration, I would probably do this:
def initialize(word)
@word = word
@letters_remaining = @word.chars
@correct_guesses = []
@wrong_guesses = []
end
def guess(letter)
if found_idx = @letters_remaining.index(letter)
@correct_guesses << @letters_remaining.delete_at(found_idx)
return :win if @letters_remaining.empty?
else
@wrong_guesses << letter
return :lose if @wrong_guesses.size >= MAX_WRONG_GUESSES
end
:play
end
What's happening here is we're keeping a @letters_remaining
array, which in initialize
is populated with the letters of the word. Upon each guess, we look for the guessed letter in@letters_remaining
. If it's found, we delete it and add it to @correct_guesses
, then check if @letters_remaining
is empty. If it is, the user has won.
If the guessed letter isn't in @letters_remaining
, we add it to @wrong_guesses
and check the latter's size to determine if the user has lost.
If none of the above is true, the user plays on.
You'll note that I'm storing guesses, etc. in arrays instead of strings. Since most of our logic concerns letters rather than entire strings, this makes more sense.
Edit 2: The above assumes each guess "fills in" only one letter at a time. If you want each guess to fill in all of the matching letters, it would look something like this:
def guess(letter)
if @letters_remaining.delete(letter)
@correct_guesses << letter
return :win if @letters_remaining.empty?
else
@wrong_guesses << letter
return :lose if @wrong_guesses.size >= MAX_WRONG_GUESSES
end
:play
end
Upvotes: 1
Reputation: 110755
Considering that a guess of a letter exposes all instances of that letter in the secret word, you can write:
(word.chars - guesses).empty?
Edit: On reflection, it seems you may want a method that takes an argument that is one guess and returns :win
, :lose
or :continue
, taking account of all previous guesses. If so, you could do the following.
Code
MAX_INCORRECT_GUESSES = 7
Execute at the beginning:
def init(word)
@word = word
@nbr_incorrect = 0
end
Execute after every guess until :win
or :lose
is returned:
def win_lose_or_continue(guess)
if @word.include?(guess)
@word.delete!(guess)
return @word.empty? ? :win : :continue
end
@nbr_incorrect += 1
@nbr_incorrect == MAX_INCORRECT_GUESSES ? :lose : :continue
end
Examples
First, a helper for displaying results:
def results(guesses)
puts "@word = #{@word}"
guesses.each_char.each do |c|
puts "win_lose_or_continue(#{c}) = #{win_lose_or_continue(c)}, @word now #{@word}"
end
end
init('cat')
guesses = "argptc"
results(guesses)
@word = cat
win_lose_or_continue(a) = continue, @word now ct
win_lose_or_continue(r) = continue, @word now ct
win_lose_or_continue(g) = continue, @word now ct
win_lose_or_continue(p) = continue, @word now ct
win_lose_or_continue(t) = continue, @word now c
win_lose_or_continue(c) = win, @word now
init('cat')
guesses = "argptbfjk"
results(guesses)
@word = cat
win_lose_or_continue(a) = continue, @word now ct
win_lose_or_continue(r) = continue, @word now ct
win_lose_or_continue(g) = continue, @word now ct
win_lose_or_continue(p) = continue, @word now ct
win_lose_or_continue(t) = continue, @word now c
win_lose_or_continue(b) = continue, @word now c
win_lose_or_continue(f) = continue, @word now c
win_lose_or_continue(j) = continue, @word now c
win_lose_or_continue(k) = lose, @word now c
Upvotes: 1
Reputation: 2867
I think your initial solution isn't as bad as it sounds. The problem is that "delete" and "empty?" aren't obviously related to the problem at hand. Anyone reading the code needs to make the connection of why they're relevant.
A simple fix is to introduce an explaining variable to make it clear why delete
and empty?
are relevant.
Conceptually, I think you're perfectly on-track with the core idea: comparing the set of "actual guesses" with the set of "necessary guesses".
The player wins when there's nothing left to guess -- that is, when the set of actual guesses includes all of the necessary guesses.
Your necessary_guesses.delete(actual_guesses).empty?
solution checks whether actual_guesses
is a subset of necessary_guesses
. You can write it a few different ways, depending on what you want to emphasise:
unguessed_characters = necessary_guesses.delete(actual_guesses)
win if unguessed_characters.empty?
or:
unguessed_characters = necessary_guesses.chars - actual_guesses.chars
win if unguessed_characters.empty?
If you're willing to use actual Set objects, you can even write something like:
win if set_of_necessary_guesses.subset?(set_of_actual_guesses)
In short, the computer is happy with your solution, but focusing on names could make it easier for a human.
Upvotes: 1
Reputation: 2051
Considering guesses
is a list of chars (at least that's what I would expect from a hangman game), this would do:
def won word, guess
(word.chars & guess) == word.chars.uniq
end
Testing some input:
word = 'banana'
guess1 = %w(b a m)
guess2 = %w(b a m n)
puts won(word, guess1)
puts won(word, guess2)
#$ ruby words.rb
#false
#true
Upvotes: 3
Reputation: 193
Another hack: you can do to make it more concise is by using bit manipulation. Don't know a lot about ruby, but this how it will looks like in C++
long int checker = 0;
for(int i=0;i<word.length();i++)
{
long int t = word[i]-'a';
t = 1<<t;
if((t&checker)==0)
{
checker |= t;
}
}
for(int i=0;i<guesses.length();i++)
{
long int t = guesses[i]-'a';
t = 1<<t;
if((t&checker)==0){
return false;
}
}
return true;
Perhaps built in function could solve elegantly within much shorter but if you like to define your own function you could try this. Hope this helps
Upvotes: 0