keyboard, fix clue bugs, length slider

This commit is contained in:
Lynn
2022-01-01 03:04:48 +01:00
parent 15d46d3587
commit 1219991921
10 changed files with 267564 additions and 1242 deletions

View File

@@ -7,14 +7,14 @@ body {
background-color: #eeeeee; background-color: #eeeeee;
} }
div.Row { .Row {
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
div.Row-letter { .Row-letter {
margin: 2px; margin: 2px;
border: 2px solid rgba(0,0,0,0.4); border: 2px solid rgba(0, 0, 0, 0.4);
width: 40px; width: 40px;
height: 40px; height: 40px;
font-size: 28px; font-size: 28px;
@@ -25,19 +25,47 @@ div.Row-letter {
font-weight: bold; font-weight: bold;
} }
div.Row-letter-green { .Game-keyboard {
display: flex;
flex-direction: column;
}
.Game-keyboard-row {
display: flex;
flex-direction: row;
justify-content: center;
}
.Game-keyboard-button {
margin: 2px;
background-color: #cdcdcd;
padding: 4px;
text-transform: capitalize;
border-radius: 4px;
min-width: 25px;
color: inherit;
text-decoration: inherit;
border: inherit;
cursor: pointer;
}
.Game-keyboard-button:focus {
outline: none;
}
.letter-correct {
border: none; border: none;
background-color: rgb(87, 172, 87); background-color: rgb(87, 172, 87);
color: white; color: white;
} }
div.Row-letter-yellow { .letter-elsewhere {
border: none; border: none;
background-color: #e9c601; background-color: #e9c601;
color: white; color: white;
} }
div.Row-letter-gray { .letter-absent {
border: none; border: none;
background-color: rgb(162, 162, 162); background-color: rgb(162, 162, 162);
color: white; color: white;

View File

@@ -1,17 +1,49 @@
import React from "react";
import logo from "./logo.svg";
import "./App.css"; import "./App.css";
import common from "./common.json"; import common from "./common.json";
import { pick } from "./util"; import { dictionarySet, pick } from "./util";
import Game from "./Game"; import Game from "./Game";
import { names } from "./names";
import { useEffect, useState } from "react";
const targets = common
.slice(0, 20000) // adjust for max target freakiness
.filter((word) => dictionarySet.has(word) && !names.has(word));
function randomTarget(wordLength: number) {
const eligible = targets.filter((word) => word.length === wordLength);
console.log(eligible);
return pick(eligible);
}
function App() { function App() {
return <> const [wordLength, setWordLength] = useState(5);
<h1>Wordl!</h1> const [target, setTarget] = useState(randomTarget(wordLength));
if (target.length !== wordLength) {
throw new Error("length mismatch");
}
return (
<>
<h1>hello wordl</h1>
<input
type="range"
min="3"
max="15"
value={wordLength}
onChange={(e) => {
setTarget(randomTarget(Number(e.target.value)));
setWordLength(Number(e.target.value));
}}
></input>
<div className="App"> <div className="App">
<Game target={pick(common)} /> <Game
key={wordLength}
wordLength={wordLength}
target={target}
maxGuesses={6}
/>
</div> </div>
</>; </>
);
} }
export default App; export default App;

View File

@@ -1,7 +1,8 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Row, RowState } from "./Row"; import { Row, RowState } from "./Row";
import { pick, wordLength } from "./util";
import dictionary from "./dictionary.json"; import dictionary from "./dictionary.json";
import { Clue, clue, clueClass } from "./clue";
import { Keyboard } from "./Keyboard";
enum GameState { enum GameState {
Playing, Playing,
@@ -10,25 +11,25 @@ enum GameState {
interface GameProps { interface GameProps {
target: string; target: string;
wordLength: number;
maxGuesses: number;
} }
function Game(props: GameProps) { function Game(props: GameProps) {
const [gameState, setGameState] = useState(GameState.Playing); const [gameState, setGameState] = useState(GameState.Playing);
const [guesses, setGuesses] = useState<string[]>([]); const [guesses, setGuesses] = useState<string[]>([]);
const [currentGuess, setCurrentGuess] = useState<string>(""); const [currentGuess, setCurrentGuess] = useState<string>("");
const maxGuesses = 6;
useEffect(() => { const onKey = (key: string) => {
const onKeyDown = (e: KeyboardEvent) => { console.log(key);
console.log(e.key)
if (gameState !== GameState.Playing) return; if (gameState !== GameState.Playing) return;
if (guesses.length === maxGuesses) return; if (guesses.length === props.maxGuesses) return;
if (/^[a-z]$/.test(e.key)) { if (/^[a-z]$/.test(key)) {
setCurrentGuess((guess) => (guess + e.key).slice(0, wordLength)); setCurrentGuess((guess) => (guess + key).slice(0, props.wordLength));
} else if (e.key === "Backspace") { } else if (key === "Backspace") {
setCurrentGuess((guess) => guess.slice(0, -1)); setCurrentGuess((guess) => guess.slice(0, -1));
} else if (e.key === "Enter") { } else if (key === "Enter") {
if (currentGuess.length !== wordLength) { if (currentGuess.length !== props.wordLength) {
// TODO show a helpful message // TODO show a helpful message
return; return;
} }
@@ -41,6 +42,11 @@ function Game(props: GameProps) {
} }
}; };
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
onKey(e.key);
};
document.addEventListener("keydown", onKeyDown); document.addEventListener("keydown", onKeyDown);
return () => { return () => {
@@ -49,40 +55,37 @@ function Game(props: GameProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentGuess]); }, [currentGuess]);
let rowDivs = []; let letterInfo = new Map<string, Clue>();
let i = 0; const rowDivs = Array(props.maxGuesses)
for (const guess of guesses) { .fill(undefined)
rowDivs.push( .map((_, i) => {
<Row const guess = [...guesses, currentGuess][i] ?? "";
key={i++} const cluedLetters = clue(guess, props.target);
rowState={RowState.LockedIn} if (i < guesses.length) {
letters={guess} for (const { clue, letter } of cluedLetters) {
target={props.target} if (clue === undefined) break;
/> const old = letterInfo.get(letter);
); if (old === undefined || clue > old) {
} letterInfo.set(letter, clue);
if (rowDivs.length < maxGuesses) {
rowDivs.push(
<Row
key={i++}
rowState={RowState.Pending}
letters={currentGuess}
target={props.target}
/>
);
while (rowDivs.length < maxGuesses) {
rowDivs.push(
<Row
key={i++}
rowState={RowState.Pending}
letters=""
target={props.target}
/>
);
} }
} }
}
return (
<Row
key={i}
wordLength={props.wordLength}
rowState={i < guesses.length ? RowState.LockedIn : RowState.Pending}
cluedLetters={cluedLetters}
/>
);
});
return <div className="Game">{rowDivs}</div>; return (
<div className="Game">
{rowDivs}
<Keyboard letterInfo={letterInfo} onKey={onKey} />
</div>
);
} }
export default Game; export default Game;

42
src/Keyboard.tsx Normal file
View File

@@ -0,0 +1,42 @@
import { Clue, clueClass } from "./clue";
interface KeyboardProps {
letterInfo: Map<string, Clue>;
onKey: (key: string) => void;
}
export function Keyboard(props: KeyboardProps) {
const keyboard = [
"q w e r t y u i o p".split(" "),
"a s d f g h j k l".split(" "),
"Backspace z x c v b n m Enter".split(" "),
];
return (
<div className="Game-keyboard">
{keyboard.map((row, i) => (
<div key={i} className="Game-keyboard-row">
{row.map((label, j) => {
let className = "Game-keyboard-button";
const clue = props.letterInfo.get(label);
if (clue !== undefined) {
className += " " + clueClass(clue);
}
return (
<div
tabIndex={-1}
key={j}
className={className}
onClick={() => {
props.onKey(label);
}}
>
{label}
</div>
);
})}
</div>
))}
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { wordLength } from "./util"; import { Clue, clueClass, CluedLetter } from "./clue";
export enum RowState { export enum RowState {
LockedIn, LockedIn,
@@ -7,26 +7,19 @@ export enum RowState {
interface RowProps { interface RowProps {
rowState: RowState; rowState: RowState;
letters: string; wordLength: number;
target: string; cluedLetters: CluedLetter[];
} }
export function Row(props: RowProps) { export function Row(props: RowProps) {
const isLockedIn = props.rowState === RowState.LockedIn; const isLockedIn = props.rowState === RowState.LockedIn;
const letterDivs = props.letters const letterDivs = props.cluedLetters
.padEnd(wordLength) .concat(Array(props.wordLength).fill({ clue: Clue.Absent, letter: "" }))
.split("") .slice(0, props.wordLength)
.map((letter, i) => { .map(({ clue, letter }, i) => {
let letterClass = "Row-letter"; let letterClass = "Row-letter";
if (isLockedIn) { if (isLockedIn && clue !== undefined) {
if (props.target[i] === letter) { letterClass += " " + clueClass(clue);
letterClass += " Row-letter-green";
} else if (props.target.includes(letter)) {
// TODO don't color letters accounted for by a green clue
letterClass += " Row-letter-yellow";
} else {
letterClass += " Row-letter-gray";
}
} }
return ( return (
<div key={i} className={letterClass}> <div key={i} className={letterClass}>

49
src/clue.ts Normal file
View File

@@ -0,0 +1,49 @@
export enum Clue {
Absent,
Elsewhere,
Correct,
}
export interface CluedLetter {
clue?: Clue;
letter: string;
}
// clue("perks", "rebus")
// [
// { letter: "p", clue: Absent },
// { letter: "e", clue: Correct },
// { letter: "r", clue: Elsewhere },
// { letter: "k", clue: Absent },
// { letter: "s", clue: Correct },
// ]
export function clue(word: string, target: string): CluedLetter[] {
let notFound: string[] = [];
target.split("").map((letter, i) => {
if (word[i] !== letter) {
notFound.push(letter);
}
});
return word.split("").map((letter, i) => {
let j: number;
if (target[i] === letter) {
return { clue: Clue.Correct, letter };
} else if ((j = notFound.indexOf(letter)) > -1) {
notFound[j] = "";
return { clue: Clue.Elsewhere, letter };
} else {
return { clue: Clue.Absent, letter };
}
});
}
export function clueClass(clue: Clue): string {
if (clue === Clue.Absent) {
return "letter-absent";
} else if (clue === Clue.Elsewhere) {
return "letter-elsewhere";
} else {
return "letter-correct";
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

17
src/names.ts Normal file
View File

@@ -0,0 +1,17 @@
export const names: Set<string> = new Set([
"anglo",
"bible",
"carol",
"costa",
"dutch",
"harry",
"jimmy",
"jones",
"lewis",
"maria",
"paris",
"pedro",
"roger",
"sally",
"texas",
]);

View File

@@ -1,4 +1,6 @@
export const wordLength = 5; import dictionary from "./dictionary.json";
export const dictionarySet: Set<string> = new Set(dictionary);
export function pick<T>(array: Array<T>): T { export function pick<T>(array: Array<T>): T {
return array[Math.floor(array.length * Math.random())]; return array[Math.floor(array.length * Math.random())];