1

i have a relatively simple guess the word by each letter game.

word = "hello"
puts word
guess = ""

chars = word.chars
check = ""

blanks = "-" * word.length

while check != chars
  if chars.include? guess
    reveal_letter = chars.index(guess)
    blanks[reveal_letter] = guess
    puts blanks
  end  
  print "guess the word: "
  puts blanks
  print "enter a letter: "
  guess = gets.chomp
end

puts "You win!"

i am satisfied with how the blanks get filled after correct guesses, but the problem is the index only returns the 1st occurence and thus will never complete if the word has more than 1 occurence of a character.

6
  • Do try and clean up your code a little before posting, remove extraneous blank lines, and make the indentation consistent so we can see clearly what you're trying to do. Commented Oct 5, 2020 at 23:47
  • 1
    Index takes an offset parameter, which indicates which character to start searching from. You might try using that with the last-known index to find the next subsequent index until you've found all the occurrences in the string. Another approach would be to simply iterate chars.each.with_index and test each character. Commented Oct 6, 2020 at 0:07
  • 1
    Try something like chars = ['b', 'a', 'n', 'a', 'n', 'a']; guess = 'a'; chars.each_index.select { |i| chars[i] == guess } #=> [1, 3, 5]. Commented Oct 6, 2020 at 0:08
  • @CarySwoveland i'm new to ruby and I do keep seeing that syntax inside the curly braces. what is it called? Commented Oct 6, 2020 at 0:31
  • 2
    It's called a block, and it's used so frequently it's probably worth understanding before you proceed further Commented Oct 6, 2020 at 0:38

4 Answers 4

2

The issue is here:

reveal_letter = chars.index(guess)

Array#index returns the first index corresponding to that value.

What you want to do, is find the first index that has not already been seen.

For this you can use Array#each_index with Enumerable#find:

reveal_letter = chars.each_index.find do |idx|
  chars[idx] == guess && slots[idx] != "-"
end

each_index returns an Enumerable, which is not exactly an array but behaves like one. If you call each_index.to_a you will get an array of all indexes. Then we can chain on find to find the first index that the block returns truthy for.

Sign up to request clarification or add additional context in comments.

Comments

1

I answered a similar question over on codereview a while back. Here is a simple class that manages the guesses, works for multiple occurrences of the same letter, and provides a string that can be used for display.

class Hangword
  attr_reader :guesses
  def initialize(word)
    @word = word.downcase.chars
    @display = @word.map { '_' }
    @charmap = Hash.new { |h, k| h[k] = [] }
    @word.each_with_index { |char, index| @charmap[char].append index }
    @guesses = 7
  end

  def guess(char)
    raise 'Hanged' if hanged?

    if @charmap.include? char
      @charmap[char].each { |i| @display[i] = char }
    else
      @guesses -= 1
    end
  end

  def secret
    @word.join
  end

  def display
    @display.join
  end

  def hanged?
    @guesses.zero?
  end

  def reprieved?
    !hanged? && [email protected]?('_')
  end
end

It manages the game state, you just need to write some presentation code to interact with the player.

a = Hangword.new 'pneumonoultramicroscopicsilicovolcanoconiosis'
a.guess('o')
puts a.display
# => _____o_o_________o__o________o_o____o_o__o___

Comments

1

Working with indices is error prone. In this case it can be avoided. Suppose there is a string secret_word and a string guessed_chars. You want to transpose every char in secret_word which is NOT(^) in guessed_chars with "-". This is the corresponding code:

secret_word.tr("^" + guessed_chars, "-")

doc for the tr method

1 Comment

Didn't know that about tr. Very handy.
1

@max has idenfied your problem. I would like to give a solution that works with strings directly; that is, it does not convert strings to arrays of characters.

Let's first create two helper methods that we can test separately. The first obtains a guess of a letter from the player.

def request_guess(guesses)
  loop do
    print "enter a letter: "
    guess = gets.chomp.downcase
    if !guess.match?(/\A[a-z]\z/)
      puts "You must enter a single letter. Try again."
    elsif guesses.include?(guess)
      puts "Er, you already guessed that letter. Try again."
    else
      break guess
    end
  end
end

The regular expression /\A[a-z]\z/ used by String#match reads, "match the beginning of the string (\A) followed by any one lowercase letter followed by the end of the string (\z). Suppose letters in the string "erluz" had already been guessed. Here is a possible dialogue.

request_guess("erluz")
enter a letter: 4
You must enter a single letter. Try again.
enter a letter: ab
You must enter a single letter. Try again.
enter a letter: l
Er, you already guessed that letter. Try again.
enter a letter: U
Er, you already guessed that letter. Try again.
enter a letter: i
  #=> "i"

The second helper method creates a string that displays the letters in the word that have been identified by previous guesses and displays hyphens in place of the letters that have not yet been identified.

def reveal(word, guesses)
  word.gsub(/./) { |c| guesses.include?(c) ? c : '-' }
end

The regular expression used by String#gsub (/./) matches any single character so it matches each character in the string. The character is held by the block variable c.

If, for example, the word were "blizzard" and the letters in the string "erluoz" had been guessed, the "reveal" would be:

reveal("blizzard", "erluz")
  #=> "-l-zz-r-"

Notice that this is not especially efficient. Firstly, for this example, had guesses contained only successful guesses we wouldn't have to check if 'e' or 'u' were in the string. However, if want to inform the player that they have repeated an unsuccessful guess we need to keep the unsuccessful guesses somewhere. We could also improve efficiency by making guesses a set of guessed characters, rather that a string, but let's be realistic, efficiency is irrelevant here when one considers that English words contain no more than 45 characters1.

We can operate the game as follows.

word = "hello"
guesses = ''
    
loop do
  break if word.delete(guesses).empty?
  puts "guess the word: #{reveal(word, guesses)}"
  guess = request_guess(guesses)
  puts "The word contains #{word.count(guess)} #{guess}'s."
  guesses << guess
end
puts word
puts "Congratulations! You won!"

We may have the following dialogue.

guess the word: -----
enter a letter: ab
You must enter a single letter. Try again.
enter a letter: a
The word contains 0 a's.
guess the word: -----
enter a letter: e
The word contains 1 e's.
guess the word: -e---
enter a letter: l
The word contains 2 l's.
guess the word: -ell-
enter a letter: u
The word contains 0 u's.
guess the word: -ell-
enter a letter: o
The word contains 1 o's.
guess the word: -ello
enter a letter: h
The word contains 1 h's.
hello
Congratulations! You won!

1. The number of letters in "pneumonoultramicroscopicsilicovolcanoconiosis".

1 Comment

And there was me thinking that "antidisestablishmentarianism" was the longest. You learn something every day...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.