replace the nth character of a string (RegEx or not)

I'm trying to program a vocabulary game. Let me say at the outset that I'm a completely rookie programmer!

I am using a regular expression to hide a word, which I have to guess.

Take for example the chosen_word:

'TO CRANK (STH) UP'

With RegExs I manage to hide the keywords and I have hidden_word as follows:

TO _ _ _ _ _ (STH) _ _

With the RegEx findall() and the filter() methods I get a list of hidden_letters:

['C', 'R', 'A', 'N', 'K', 'U', 'P']

The idea is that we now choose one letter from this list randomly to reveal to the user:

chosen_letter = random.choice(hidden_letters)

n = hidden_letters.index(chosen_letter) + 1

Now the chosen letter's n maps beautifully onto its corresponding underscores in the hidden_word. For example, if the game has randomly chosen the letter 'R', then n will be '2', and in the hidden_word this letter's position is at the 2nd underscore.

I am stuck at this stage now. I want to replace the nth underscore of hidden_word with the chosen_letter. I'm thinking of using either the .replace() method, or the RegEx re.sub() method, but I don't know how to start off.

Could someone please give me hints?

Many thanks in advance!!

Upvotes: 2

Views: 575

Answers (2)

chsws
chsws

Reputation: 443

Given you're a rookie, I'd thought throw my hat in and do it without regex and attempt to explain. Maybe only read this once you've had your go. Your question title can be answered with:

def replace_nth_char(string, n, replacement):
    n -= 1
    l_string = list(string)
    l_string[n] = replacement
    return ''.join(l_string)

It converts the string to a list of letters which you can replace by index n and then joins it back up.

I've also given the rest a go to show you more python options.

As you're managing the state, you might want to think about using a class. It helps you wrap all the functions and attributes together into one object with a purpose - playing your Vocabulary Game. I recommend looking into them. Here is one for the game:

import random


class VocabularyGame():
    def __init__(self, chosen_words, hidden_words):
        self.chosen_words = chosen_words
        self.hidden_words = hidden_words
        self.hidden_letters = list(set(''.join(words)))
        self.masked_sentence = self.mask_words(chosen_words, hidden_words)
        print(f"Game start: {self.masked_sentence}")
        
    def mask_words(self, sentence, masks):
        return ' '.join(['_'*len(w) if w in masks else w for w in sentence.split(' ')])
    
    def try_letter(self, letter=None):
        if letter is None:
            letter = random.choice(self.hidden_letters)
        self.masked_sentence = ''.join(
            [c if c== letter else m for m, c in zip(self.masked_sentence, self.chosen_words)]
        )
        self.hidden_letters = [l for l in self.hidden_letters if l != letter]
        print(f"Trying letter {letter}...\nRemaining letters: {self.masked_sentence}")

The __init__ section runs whenever we make new game instances and takes three arguments, (self, chosen_words, hidden_words). The use of self references the current class instance (or game) and we can use it to set and retrieve attributes to the class, in this case, to remember our words and sentences.

    def __init__(self, chosen_words, hidden_words):
        self.chosen_words = chosen_words
        self.hidden_words = hidden_words
        self.hidden_letters = list(set(''.join(hidden_words)))
        self.masked_sentence = self.mask_words(chosen_words, hidden_words)
        print(f"Game start: {self.masked_sentence}")

list(set(''.join(words))) gets all unique letters in the hidden words by joining them into one string and using sets to convert them into the unique letters. I convert this back into a list for ease of use later.

We then apply a function to mask the words with '_'.

    def mask_words(self, sentence, masks):
        return ' '.join(['_'*len(w) if w in masks else w for w in sentence.split(' ')])

This goes through each word in the sentence and replaces it with '_' for the length of the word, if it exists in hidden words. Then it joins it back up. Now we have our start state.

The last thing to do is to try a letter. We do this by defining a method (function on a class) def try_letter(self, letter=None):. If no letter is provided, we pick a random one from the unique missing letters we defined earlier. The we go through each letter in the original sentence and masked sentence together using zip and when the original letter is our chosen letter, we replace the one in our masked sentence.

        self.masked_sentence = ''.join(
            [c if c==letter else m for m, c in zip(self.masked_sentence, self.chosen_words)]
        )

Then remove the letter form our hidden letters list:

self.hidden_letters = [l for l in self.hidden_letters if l != letter]

Finally, we print result out using f-strings. Now, we can play the game!

chosen_word = "TO CRANK (STH) UP"
words = ['CRANK', 'UP']

game = VocabularyGame(chosen_word, words)

Outputs: Game start: TO _____ (STH) __

Trying a letter 7 times for i in range(7): game.try_letter() Outputs:

Trying letter N...
Remaining letters: TO ___N_ (STH) __
Trying letter K...
Remaining letters: TO ___NK (STH) __
Trying letter R...
Remaining letters: TO _R_NK (STH) __
Trying letter P...
Remaining letters: TO _R_NK (STH) _P
Trying letter C...
Remaining letters: TO CR_NK (STH) _P
Trying letter U...
Remaining letters: TO CR_NK (STH) UP
Trying letter A...
Remaining letters: TO CRANK (STH) UP

Upvotes: 1

Pierre D
Pierre D

Reputation: 26211

I would approach this a bit differently:

The state at each point in time is defined by:

  • the full phrase (to be guessed),
  • a set of hidden words,
  • letters guessed so far (or to be revealed to the user).

The you can define a show() function with these three quantities:

def show(phrase, hidden_words, letters_guessed):
    parts = [
        ''.join([
            c if c in letters_guessed else '-' for c in w
        ]) if w in hidden_words else w
        for w in phrase.split()
    ]
    return ' '.join(parts)

With this, you can write tests, including doctests. That will make your code much easier to document, debug, test and use.

Some examples (which could be included as doctests in the docstring of show):

phrase = 'TO PIPE (STH) UP AND PIPE DOWN'
hidden_words = {'PIPE', 'UP'}

>>> show(phrase, hidden_words, {})
'TO ---- (STH) -- AND ---- DOWN'

>>> show(phrase, hidden_words, set('I'))
'TO -I-- (STH) -- AND -I-- DOWN'

>>> show(phrase, hidden_words, set('PI'))
'TO PIP- (STH) -P AND PIP- DOWN'

Upvotes: 2

Related Questions