hello-wordl/src/Game.tsx

209 lines
6.1 KiB
TypeScript
Raw Normal View History

2021-12-31 03:43:09 +02:00
import { useEffect, useState } from "react";
import { Row, RowState } from "./Row";
import dictionary from "./dictionary.json";
2022-01-18 00:25:44 +02:00
import { Clue, clue, describeClue, violation } from "./clue";
2022-01-01 04:04:48 +02:00
import { Keyboard } from "./Keyboard";
2022-01-09 19:39:19 +02:00
import targetList from "./targets.json";
2022-01-14 01:16:27 +02:00
import { dictionarySet, pick, resetRng, seed, speak } from "./util";
2021-12-31 03:43:09 +02:00
enum GameState {
Playing,
2022-01-01 20:14:50 +02:00
Won,
Lost,
2021-12-31 03:43:09 +02:00
}
interface GameProps {
2022-01-01 04:04:48 +02:00
maxGuesses: number;
2022-01-03 17:17:36 +02:00
hidden: boolean;
2022-01-17 20:42:56 +02:00
hard: boolean;
2022-01-03 01:08:23 +02:00
}
2022-01-12 18:40:23 +02:00
const targets = targetList.slice(0, targetList.indexOf("murky") + 1); // Words no rarer than this one
2022-01-03 01:08:23 +02:00
function randomTarget(wordLength: number) {
const eligible = targets.filter((word) => word.length === wordLength);
return pick(eligible);
2021-12-31 03:43:09 +02:00
}
function Game(props: GameProps) {
const [gameState, setGameState] = useState(GameState.Playing);
const [guesses, setGuesses] = useState<string[]>([]);
const [currentGuess, setCurrentGuess] = useState<string>("");
2022-01-03 01:08:23 +02:00
const [wordLength, setWordLength] = useState(5);
const [hint, setHint] = useState<string>(`Make your first guess!`);
2022-01-08 01:01:19 +02:00
const [target, setTarget] = useState(() => {
resetRng();
return randomTarget(wordLength);
});
const [gameNumber, setGameNumber] = useState(1);
2022-01-03 01:08:23 +02:00
2022-01-08 01:01:19 +02:00
const startNextGame = () => {
2022-01-03 01:08:23 +02:00
setTarget(randomTarget(wordLength));
setGuesses([]);
setCurrentGuess("");
setHint("");
setGameState(GameState.Playing);
2022-01-08 01:01:19 +02:00
setGameNumber((x) => x + 1);
2022-01-03 01:08:23 +02:00
};
2022-01-01 04:04:48 +02:00
const onKey = (key: string) => {
2022-01-01 20:14:50 +02:00
if (gameState !== GameState.Playing) {
if (key === "Enter") {
2022-01-08 01:01:19 +02:00
startNextGame();
2022-01-01 20:14:50 +02:00
}
return;
}
2022-01-01 04:04:48 +02:00
if (guesses.length === props.maxGuesses) return;
2022-01-15 21:38:32 +02:00
if (/^[a-z]$/i.test(key)) {
setCurrentGuess((guess) =>
(guess + key.toLowerCase()).slice(0, wordLength)
);
2022-01-18 00:25:44 +02:00
// When typing a guess, make sure a later "Enter" press won't activate a link or button.
const active = document.activeElement as HTMLElement;
if (active && ["A", "BUTTON"].includes(active.tagName)) {
active.blur();
}
2022-01-02 02:52:58 +02:00
setHint("");
2022-01-01 04:04:48 +02:00
} else if (key === "Backspace") {
setCurrentGuess((guess) => guess.slice(0, -1));
2022-01-02 02:52:58 +02:00
setHint("");
2022-01-01 04:04:48 +02:00
} else if (key === "Enter") {
2022-01-03 01:08:23 +02:00
if (currentGuess.length !== wordLength) {
2022-01-01 20:14:50 +02:00
setHint("Too short");
2022-01-01 04:04:48 +02:00
return;
}
if (!dictionary.includes(currentGuess)) {
2022-01-01 20:14:50 +02:00
setHint("Not a valid word");
2022-01-01 04:04:48 +02:00
return;
}
2022-01-18 00:25:44 +02:00
if (props.hard) {
for (const g of guesses) {
const feedback = violation(clue(g, target), currentGuess);
if (feedback) {
setHint(feedback);
return;
}
}
}
2022-01-01 04:04:48 +02:00
setGuesses((guesses) => guesses.concat([currentGuess]));
setCurrentGuess((guess) => "");
2022-01-03 01:08:23 +02:00
if (currentGuess === target) {
setHint(
`You won! The answer was ${target.toUpperCase()}. (Enter to play again)`
);
2022-01-01 20:14:50 +02:00
setGameState(GameState.Won);
} else if (guesses.length + 1 === props.maxGuesses) {
setHint(
2022-01-03 01:08:23 +02:00
`You lost! The answer was ${target.toUpperCase()}. (Enter to play again)`
2022-01-01 20:14:50 +02:00
);
setGameState(GameState.Lost);
} else {
setHint("");
2022-01-14 01:16:27 +02:00
speak(describeClue(clue(currentGuess, target)));
2022-01-01 20:14:50 +02:00
}
2022-01-01 04:04:48 +02:00
}
};
2021-12-31 03:43:09 +02:00
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
2022-01-02 02:52:58 +02:00
if (!e.ctrlKey && !e.metaKey) {
onKey(e.key);
}
2022-01-17 00:06:29 +02:00
if (e.key === "Backspace") {
e.preventDefault();
}
2021-12-31 03:43:09 +02:00
};
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
};
2022-01-01 20:14:50 +02:00
}, [currentGuess, gameState]);
2021-12-31 03:43:09 +02:00
2022-01-01 04:04:48 +02:00
let letterInfo = new Map<string, Clue>();
2022-01-14 01:16:27 +02:00
const tableRows = Array(props.maxGuesses)
2022-01-01 04:04:48 +02:00
.fill(undefined)
.map((_, i) => {
const guess = [...guesses, currentGuess][i] ?? "";
2022-01-03 01:08:23 +02:00
const cluedLetters = clue(guess, target);
2022-01-01 20:14:50 +02:00
const lockedIn = i < guesses.length;
if (lockedIn) {
2022-01-01 04:04:48 +02:00
for (const { clue, letter } of cluedLetters) {
if (clue === undefined) break;
const old = letterInfo.get(letter);
if (old === undefined || clue > old) {
letterInfo.set(letter, clue);
}
}
}
return (
2021-12-31 03:43:09 +02:00
<Row
2022-01-01 04:04:48 +02:00
key={i}
2022-01-03 01:08:23 +02:00
wordLength={wordLength}
2022-01-12 18:40:23 +02:00
rowState={
lockedIn
? RowState.LockedIn
: i === guesses.length
? RowState.Editing
: RowState.Pending
}
2022-01-01 04:04:48 +02:00
cluedLetters={cluedLetters}
2021-12-31 03:43:09 +02:00
/>
);
2022-01-01 04:04:48 +02:00
});
2021-12-31 03:43:09 +02:00
2022-01-01 04:04:48 +02:00
return (
2022-01-03 17:17:36 +02:00
<div className="Game" style={{ display: props.hidden ? "none" : "block" }}>
2022-01-03 01:08:23 +02:00
<div className="Game-options">
<label htmlFor="wordLength">Letters:</label>
<input
type="range"
min="4"
max="11"
id="wordLength"
2022-01-07 18:13:39 +02:00
disabled={
gameState === GameState.Playing &&
(guesses.length > 0 || currentGuess !== "")
}
2022-01-03 01:08:23 +02:00
value={wordLength}
onChange={(e) => {
const length = Number(e.target.value);
2022-01-07 18:13:39 +02:00
resetRng();
2022-01-08 01:01:19 +02:00
setGameNumber(1);
setGameState(GameState.Playing);
2022-01-07 18:13:39 +02:00
setGuesses([]);
setCurrentGuess("");
2022-01-03 01:08:23 +02:00
setTarget(randomTarget(length));
setWordLength(length);
setHint(`${length} letters`);
}}
></input>
<button
2022-01-14 01:16:27 +02:00
style={{ flex: "0 0 auto" }}
2022-01-03 01:08:23 +02:00
disabled={gameState !== GameState.Playing || guesses.length === 0}
onClick={() => {
setHint(
`The answer was ${target.toUpperCase()}. (Enter to play again)`
);
setGameState(GameState.Lost);
(document.activeElement as HTMLElement)?.blur();
}}
>
Give up
</button>
</div>
2022-01-14 18:33:00 +02:00
<table className="Game-rows" tabIndex={0} aria-label="Table of guesses">
<tbody>{tableRows}</tbody>
</table>
2022-01-14 01:16:27 +02:00
<p role="alert">{hint || `\u00a0`}</p>
2022-01-01 04:04:48 +02:00
<Keyboard letterInfo={letterInfo} onKey={onKey} />
2022-01-08 01:01:19 +02:00
{seed ? (
<div className="Game-seed-info">
seed {seed}, length {wordLength}, game {gameNumber}
</div>
) : undefined}
2022-01-01 04:04:48 +02:00
</div>
);
2021-12-31 03:43:09 +02:00
}
export default Game;