keyboard, fix clue bugs, length slider
This commit is contained in:
40
src/App.css
40
src/App.css
@@ -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;
|
||||||
|
|||||||
50
src/App.tsx
50
src/App.tsx
@@ -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));
|
||||||
<div className="App">
|
if (target.length !== wordLength) {
|
||||||
<Game target={pick(common)} />
|
throw new Error("length mismatch");
|
||||||
</div>
|
}
|
||||||
</>;
|
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">
|
||||||
|
<Game
|
||||||
|
key={wordLength}
|
||||||
|
wordLength={wordLength}
|
||||||
|
target={target}
|
||||||
|
maxGuesses={6}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
105
src/Game.tsx
105
src/Game.tsx
@@ -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,35 +11,40 @@ 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;
|
|
||||||
|
const onKey = (key: string) => {
|
||||||
|
console.log(key);
|
||||||
|
if (gameState !== GameState.Playing) return;
|
||||||
|
if (guesses.length === props.maxGuesses) return;
|
||||||
|
if (/^[a-z]$/.test(key)) {
|
||||||
|
setCurrentGuess((guess) => (guess + key).slice(0, props.wordLength));
|
||||||
|
} else if (key === "Backspace") {
|
||||||
|
setCurrentGuess((guess) => guess.slice(0, -1));
|
||||||
|
} else if (key === "Enter") {
|
||||||
|
if (currentGuess.length !== props.wordLength) {
|
||||||
|
// TODO show a helpful message
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!dictionary.includes(currentGuess)) {
|
||||||
|
// TODO show a helpful message
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setGuesses((guesses) => guesses.concat([currentGuess]));
|
||||||
|
setCurrentGuess((guess) => "");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
console.log(e.key)
|
onKey(e.key);
|
||||||
if (gameState !== GameState.Playing) return;
|
|
||||||
if (guesses.length === maxGuesses) return;
|
|
||||||
if (/^[a-z]$/.test(e.key)) {
|
|
||||||
setCurrentGuess((guess) => (guess + e.key).slice(0, wordLength));
|
|
||||||
} else if (e.key === "Backspace") {
|
|
||||||
setCurrentGuess((guess) => guess.slice(0, -1));
|
|
||||||
} else if (e.key === "Enter") {
|
|
||||||
if (currentGuess.length !== wordLength) {
|
|
||||||
// TODO show a helpful message
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!dictionary.includes(currentGuess)) {
|
|
||||||
// TODO show a helpful message
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setGuesses((guesses) => guesses.concat([currentGuess]));
|
|
||||||
setCurrentGuess((guess) => "");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("keydown", onKeyDown);
|
document.addEventListener("keydown", onKeyDown);
|
||||||
@@ -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++}
|
return (
|
||||||
rowState={RowState.Pending}
|
|
||||||
letters={currentGuess}
|
|
||||||
target={props.target}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
while (rowDivs.length < maxGuesses) {
|
|
||||||
rowDivs.push(
|
|
||||||
<Row
|
<Row
|
||||||
key={i++}
|
key={i}
|
||||||
rowState={RowState.Pending}
|
wordLength={props.wordLength}
|
||||||
letters=""
|
rowState={i < guesses.length ? RowState.LockedIn : RowState.Pending}
|
||||||
target={props.target}
|
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
42
src/Keyboard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
src/Row.tsx
25
src/Row.tsx
@@ -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
49
src/clue.ts
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
98717
src/common.json
98717
src/common.json
File diff suppressed because it is too large
Load Diff
169757
src/dictionary.json
169757
src/dictionary.json
File diff suppressed because it is too large
Load Diff
17
src/names.ts
Normal file
17
src/names.ts
Normal 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",
|
||||||
|
]);
|
||||||
@@ -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())];
|
||||||
|
|||||||
Reference in New Issue
Block a user