1

I want to create a hangman game in React.js, with this code when the user click on a letter, it will search the letter in the word and display her. It work correctly when the word contain only one same letter. I would like this code work with word like 'PROGRAMMING' with 2 'M'.

handleChooseLetter = (index) => {

const usedWord = [...this.state.usedWord];

const chosenLetter = this.state.letters[index].letter;

var letterPosition = usedWord.indexOf(chosenLetter);

if (letterPosition >= 0) {
   hiddenWord.splice(letterPosition, 1, chosenLetter); 

   this.setState({hiddenWord: hiddenWord});
}

}

I already try a while loop but it not work in my case:

var indices = [];

while(letterPosition >= 0) {
  const hiddenWord = [...this.state.hiddenWord];
  indices.push(letterPosition);
  letterPosition = usedWord.indexOf(chosenLetter, letterPosition + 1);
  hiddenWord.splice(letterPosition, 1, chosenLetter); 
  this.setState({hiddenWord: hiddenWord});
}

For me, the result is that find the letter and display them always for the last letter of the word.

I think my problem is with the splice method who splice the wrong letterPosition

Here my chooseWord function:

state = {
wordList: [
  { id: 1, word: 'PROGRAMMING'},
],
usedWord: [],
hiddenWord: [],
}

chooseWord() {
const wordList = [...this.state.wordList];

const listLength = wordList.length;

const randomWord = this.state.wordList[Math.floor(Math.random() * listLength)].word;

const splittedWord = randomWord.split("");

const arr = new Array(randomWord.length + 1).join("_").split("");

this.setState({
  usedWord: splittedWord, 
  hiddenWord: arr
});

}

Flosrn
  • 85
  • 1
  • 2
  • 9

3 Answers3

4

The simplest way is replace, not using an array:

const usedWord = "programming";
const chosenLetter = "m";
const hiddenWord = usedWord.replace(new RegExp("[^" + chosenLetter + "]", "g"), "_");
console.log(hiddenWord);

As the user adds more letters, you can add them to the character class:

const usedWord = "programming";
const chosenLetters = "mp";
const hiddenWord = usedWord.replace(new RegExp("[^" + chosenLetters + "]", "g"), "_");
console.log(hiddenWord);

React Example:

class Hangman extends React.Component {
    constructor(...args) {
        super(...args);
        this.state = {
            availableLetters: "abcdefghijklmnopqrstuvwxyz",
            chosenLetters: "",
            word: this.props.word
        };
        this.chooseLetter = this.chooseLetter.bind(this);
    }

    chooseLetter({target: {tagName, type, value}}) {
        if (tagName === "INPUT" && type === "button") {
            this.setState(prevState => ({chosenLetters: prevState.chosenLetters + value}));
        }
    }

    render() {
        const {word, chosenLetters} = this.state;
        const hiddenWord = word.replace(new RegExp("[^" + chosenLetters + "]", "g"), "_");
        return <div>
            <div>Word:&nbsp;&nbsp;&nbsp;<span className="hiddenWord">{hiddenWord}</span></div>
            <div onClick={this.chooseLetter} style={{marginTop: "8px"}}>
              {[...this.state.availableLetters].map(
                letter => <input type="button" value={letter} disabled={chosenLetters.includes(letter)} />
              )}
            </div>
          </div>;
    }
}

ReactDOM.render(
    <Hangman word="programming" />,
    document.getElementById("root")
);
.hiddenWord {
  font-family: monospace;
  letter-spacing: 1em;
  font-size: 18px;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

For single English alphabet letters, you don't have to worry about using new RegExp(chosenLetter, "g") because none of the English alphabetic letters has special meaning in a regular expression. If you did have characters with special meaning (., $, etc.), you'd escape the character before passing it to the RegExp constructor; see this question's answers for ways to do that.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Are you sure this will fix the OP's problem as I see it's different scenario. – Ankit Agarwal Jun 15 '18 at 10:42
  • @AnkitAgarwal - I think so, but the question isn't very clear. How are you reading it? – T.J. Crowder Jun 15 '18 at 10:43
  • @AnkitAgarwal - I just realized I may be thinking backward on the blanks. :-) – T.J. Crowder Jun 15 '18 at 10:45
  • OP code is removing that character once the match is found so if the use enters M again (which is correct) it will not find that index and treat it as a incorrect character. But the question here is also this that if it's handman then why the user will provide M twice? Shouldn't the M be shown on the blanks for all occurrence of M, for the first time. That's how hangman works. – Ankit Agarwal Jun 15 '18 at 10:52
  • @AnkitAgarwal - I took "chosen letter" to be the letter the player chose, and the loop to be trying to replace un-chosen letters with `_`. See the update with a React example. :-) – T.J. Crowder Jun 15 '18 at 10:58
  • Thank you for your response but it not work in my case because my usedWord and my hiddenWord are an array and I find your solution more complexe than @Vikky but it's certainly because I'm a beginner in React, thank you a lot anyway – Flosrn Jun 15 '18 at 12:12
  • @Flosrn - Hey, use what works for you! Hopefully this was of some help. Happy coding! – T.J. Crowder Jun 15 '18 at 12:23
2

I've added letter input <input onChange={this.handleChooseLetter} value={letter} /> and changed your handleChooseLetter function to iterate through letters of used word if at least 1 letter is found (because your usedWord.indexOf(chosenLetter) always returns 1 index only), so I decided to iterate entire word and check for chosen letter, if letter on that index exists, I just insert that letter to the hidden word on the same index - because hidden and used words have the same length:

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      hiddenWord: "___________",
      usedWord: "PROGRAMMING",
      letter: ""
    };
  }

  handleChooseLetter = e => {
    const usedWord = [...this.state.usedWord];
    const chosenLetter = e.target.value.toLocaleUpperCase();

    let letterPosition = usedWord.indexOf(chosenLetter);

    if (letterPosition > -1) {
      this.setState(prevState => {
        const hiddenWord = [...prevState.hiddenWord];
        for (let i = 0; i < usedWord.length; i++) {
          if (usedWord[i] === chosenLetter) {
            hiddenWord[i] = chosenLetter;
          }
        }
        return { hiddenWord, letter: "" };
      });
      return;
    }
    this.setState({ letter: "" });
  };

  render() {
    const { hiddenWord, letter } = this.state;
    return (
      <div className="App">
        {[...hiddenWord].map((letter, index) => (
          <span key={index}>{letter}&nbsp;</span>
        ))}
        <input onChange={this.handleChooseLetter} value={letter} />
      </div>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.4.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Vikky
  • 750
  • 8
  • 16
  • Thank you, your solution is the best in my case ! I understand the first part with the for loop but not the setState, why use this.setState({ hiddenWord, letter: "" }); and this.setState({ letter: "" }); ? – Flosrn Jun 15 '18 at 12:07
  • No problem! I'm glad I could help! Since (this.state.)letter is value of user input - I'm reseting it after checking is there chosen letter inside of the used word. You could remove that, but you have to reset that input somewhere. You can also write it like this: `this.setState({ hiddenWord }); } this.setState({ letter: "" }); ` - this.setState will be merged, and both hiddenWord and letter:"" will be applied to state. I just wanted to do separation - if letter is found update both hiddenWord and letter:"" (and return). If it isn't, just reset (input to) letter: "" – Vikky Jun 15 '18 at 12:13
  • Yeah @T.J Crowder your are right for the callback setState but in your example you call prevState but never use in the setState, is it normal ? – Flosrn Jun 15 '18 at 12:51
  • And thank you @Vikky for your explanation ! :) just a question: what is the best practice ? => const [...hiddenWord] = this.state.hiddenWord; or const hiddenWord = [...this.state.hiddenWord] ? – Flosrn Jun 15 '18 at 12:53
  • 1
    Flosrn - I'm not sure what is the best practice for ? => const [...hiddenWord] = this.state.hiddenWord; or const hiddenWord = [...this.state.hiddenWord] ?, maybe @T.J.Crowder can hop in? P.S. T.J.Crowder - Thx for the advice, I've added code snippet :) – Vikky Jun 15 '18 at 13:26
  • 1
    @Flosrn & Vikky - FWIW, I think it's a matter of style, destructuring rest notation (`const [...hiddenWord] = this.state.hiddenWord;`) or spread notation (`const hiddenWord = [...this.state.hiddenWord];`). Personally I would lean toward the latter because **I** find it that tiny bit clearer, but really a matter of style. If this were in a tight loop where performance mattered, if the question came up, I'd profile it (in which case `const hiddenWord = this.state.hiddenWord.slice()` would probably win), but... – T.J. Crowder Jun 15 '18 at 13:29
  • Ok thank you @T.J Crowder, I prefer also the spread notation, just a tiny bit clear lol. Just a last question for you : I have difficulties to understand when use prevState or not ? Should we use in any case ? After I stop bothering you ^^ – Flosrn Jun 15 '18 at 13:45
  • 1
    @Flosrn - No bother. Use the callback version any time you're setting state based on existing props or state; [more here](https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous). But if you're just *setting* something based on an external input or logic that *doesn't* involve reading existing state, you can use the object version instead. Vikky's `this.setState({letter: ""})` is fine, but the `this.setState({hiddenWord, letter: ""})` wasn't because `hiddenWord` came from state (before being modified). Happy coding! – T.J. Crowder Jun 15 '18 at 13:49
  • @Flosrn Worth noting that it doesn't have to be the same property. Consider: `if (this.state.flag) { this.setState({answer: 42}); } else { this.setState({answer: 24}); }` is also incorrect, because although we're not setting `flag`, we're using information from state (`flag`) to determine new state (`answer`), so we'd need the callback there too: `this.setState(prevState => ({answer: prevState.flag ? 42 : 24 }))` – T.J. Crowder Jun 15 '18 at 13:51
  • Ok, it more clear now thank you! In this case I must use callback no ? I edit my first question, it's more clear – Flosrn Jun 15 '18 at 14:01
-1

Aha! I had to do a similar project for my class once. Heres the github repo for your reference: https://github.com/LordKriegan/WraithFood

Essentially, what I did was I created an object with several strings and a few functions. When the game loads, it picks a random word from my list and sets 2 of the strings. One is the full word (lets call this property fullWord), the other is basically a string of the same length with all letters converted to underscores (let's call this one guessedWord, also see setWord() in the game.js file).

But you are interested in checking the letters! Thats easy enough. Javascript strings have a built-in method called .includes(), which takes a string as a parameter. Since I had my word saved in the object, I simply ran a .includes() on that word with the letter I wanted to check, then if it passed all my validation (letter already guessed, whether it is or isnt in the word, etc), I ran a for loop on guessedWord with another String method called .charAt(i). This method simply returns the character at position i in the string its called on. Since I have to strings in my object, which I KNOW to be the same length, I can execute a for loop on fullWord, check every letter at position i, and then reset the guessedWord property with .substr(). This method returns partial strings based on what parameters you pass it. Essentially I set guessedWord to + + . This was the checkLet() function in the game object.

I would reccomend reading up on string methods at https://www.w3schools.com/js/js_string_methods.asp