Tuesday, February 22, 2022

Guess6 in React - source

The game itself is published here. Here's the full source code. It's surprisingly consise, feel free to study it on your own.
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom';

import App from '@/app';

/**
 * Entry point
 */
class Program {
    
    Main() {

        var app = (
                <App />
        );

        ReactDOM.render(app, document.getElementById('guess6'));
    }
}

new Program().Main();

// App.tsx
import React, { useEffect, useState } from 'react';
import Dictionary from './dictionary';
import Keyboard from './keyboard';
import WordMatch from './wordMatch';

const App = () => {

  const EXPECTEDLENGTH = 6;

  const [words, setWords] = useState<Array<string>>([]);
  const [secretWord, setSecretWord] = useState<string>('');

  function getRandomWord(Dictionary: string[]): string {
    const randomIndex = Math.floor(Math.random() * (Dictionary.length));
    return Dictionary[randomIndex].toUpperCase();
  }

  function restartGame() {
      setWords([]);
      setSecretWord(getRandomWord(Dictionary));
  }
  
  function onWordTyped( newWord: string ) {
    setWords( words => words.concat([newWord]) );   
  }

  function giveUp() {
    if ( secretWord.length == EXPECTEDLENGTH ) {
      setWords( words => words.concat( secretWord ) );
    }
  }

  useEffect( () => {
    restartGame();
  }, []);

  return <>  
    <div>
      <button className='flatButton' onClick={() => restartGame()}>NEW GAME</button>
      <button className='flatButton' onClick={() => giveUp()}>GIVE UP</button>
    </div>
    <h1>Enter {EXPECTEDLENGTH}-letter word</h1>
    <Keyboard dictionary={Dictionary} expectedLength={EXPECTEDLENGTH} onWordTyped={onWordTyped} />
    {words.map( (word, index) => <ordMatch candidate={word} secret={secretWord} key={index} />)}
  </>
};

export default App;

// Keyboard.tsx
import React, { KeyboardEvent, useEffect, useState } from 'react';

const Keyboard = ({dictionary, expectedLength, onWordTyped} : 
    {dictionary: string[] | undefined, expectedLength: number, onWordTyped: (word: string) => void}) => {

    const [message, setMessage] = useState<string>('');
    const [word, setWord]       = useState<string>('');

    const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    const QWERTY  = "QWERTYUIOP";
    const ASDF    = "ASDFGHJKL";
    const ZXCV    = "ZXCVBNM";

    function appendLetter(letter: string) {
        if ( word.length < expectedLength ) {
            setWord( w => w + letter );
            setMessage('');
        }
    }

    function clearWord() {
        setWord('');
        setMessage('');
    }

    function tryAcceptWord() {
        if ( word.length != expectedLength ) {
            setMessage(`Expected ${expectedLength} characters, got ${word.length} so far`);
            return;
        } 
        
        if ( dictionary !== undefined && dictionary.map( w => w.toUpperCase() ).indexOf( word ) < 0 ) {
            setMessage(`Word ${word} not in dictionary`);
            return;
        }
      
        onWordTyped(word);
        setWord('');
    }

    return <div>
        <div>
            <input className='keyboardInput' value={word} readOnly />
        </div>
        <div className='firstRow'>
            {QWERTY.split('').map( (letter) => <button className='letterButton flatButton'  
              onClick={() => appendLetter(letter)} key={letter}>{letter}</button> )}
        </div>
        <div className='secondRow'>
            {ASDF.split('').map( (letter) => <button className='letterButton flatButton'  
              onClick={() => appendLetter(letter)} key={letter}>{letter}</button> )}
            <button className='flatButton' onClick={() => clearWord()}>DEL</button>
        </div>
        <div className='thirdRow'>
            {ZXCV.split('').map( (letter) => <button className='letterButton flatButton'  
              onClick={() => appendLetter(letter)} key={letter}>{letter}</button> )}
            <button className='flatButton' onClick={() => tryAcceptWord()}>ENTER</button>
        </div>
        <div>{message}</div>
    </div>;
}

export default Keyboard;

// WordMatch.tsx
import React from 'react';

const WordMatch = ({candidate, secret} : {candidate: string, secret: string}) => {

    if ( candidate.length != secret.length ) {
        throw new Error('candidate and secret word must have same length');
    }

    type  letterState = 'USED' | undefined;
    const letterStates: Array<letterState> = Array<letterState>(candidate.length);

    function getLetterClass( index: number ) : string {        
        // match
        if ( secret[index] == candidate[index] ) {
            letterStates[index] = 'USED';
            return 'letter letterMatch';
        }
        // possible
        for ( let i=0; i<secret.length; i++ ) {
            if ( secret[i] == candidate[index] &&
                 letterStates[i] == undefined 
                ) {
                letterStates[i] = 'USED';
                return 'letter letterPossible';
            }
        }
        // none
        return 'letter letterWrong';
    }

    return <div>
        {candidate.split('').map( (letter, index) => 
            <span className={getLetterClass(index)} key={index}>{letter}</span>
        )}
    </div>;
}

export default WordMatch;

// Dictionary.ts
// https://eslforums.com/6-letter-words/

const Dictionary: Array<string> = [
"abacus",
// the rest of the dictionary here
"zoning"
];

export default Dictionary;

Guess6 in React