keyboard, fix clue bugs, length slider

pull/1/head
Lynn 2022-01-01 03:04:48 +01:00
rodzic 15d46d3587
commit 1219991921
10 zmienionych plików z 267564 dodań i 1242 usunięć

Wyświetl plik

@ -7,14 +7,14 @@ body {
background-color: #eeeeee;
}
div.Row {
.Row {
display: flex;
justify-content: center;
}
div.Row-letter {
.Row-letter {
margin: 2px;
border: 2px solid rgba(0,0,0,0.4);
border: 2px solid rgba(0, 0, 0, 0.4);
width: 40px;
height: 40px;
font-size: 28px;
@ -25,19 +25,47 @@ div.Row-letter {
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;
background-color: rgb(87, 172, 87);
color: white;
}
div.Row-letter-yellow {
.letter-elsewhere {
border: none;
background-color: #e9c601;
color: white;
}
div.Row-letter-gray {
.letter-absent {
border: none;
background-color: rgb(162, 162, 162);
color: white;

Wyświetl plik

@ -1,17 +1,49 @@
import React from "react";
import logo from "./logo.svg";
import "./App.css";
import common from "./common.json";
import { pick } from "./util";
import { dictionarySet, pick } from "./util";
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() {
return <>
<h1>Wordl!</h1>
<div className="App">
<Game target={pick(common)} />
</div>
</>;
const [wordLength, setWordLength] = useState(5);
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">
<Game
key={wordLength}
wordLength={wordLength}
target={target}
maxGuesses={6}
/>
</div>
</>
);
}
export default App;

Wyświetl plik

@ -1,7 +1,8 @@
import { useEffect, useState } from "react";
import { Row, RowState } from "./Row";
import { pick, wordLength } from "./util";
import dictionary from "./dictionary.json";
import { Clue, clue, clueClass } from "./clue";
import { Keyboard } from "./Keyboard";
enum GameState {
Playing,
@ -10,35 +11,40 @@ enum GameState {
interface GameProps {
target: string;
wordLength: number;
maxGuesses: number;
}
function Game(props: GameProps) {
const [gameState, setGameState] = useState(GameState.Playing);
const [guesses, setGuesses] = 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(() => {
const onKeyDown = (e: KeyboardEvent) => {
console.log(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) => "");
}
onKey(e.key);
};
document.addEventListener("keydown", onKeyDown);
@ -49,40 +55,37 @@ function Game(props: GameProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentGuess]);
let rowDivs = [];
let i = 0;
for (const guess of guesses) {
rowDivs.push(
<Row
key={i++}
rowState={RowState.LockedIn}
letters={guess}
target={props.target}
/>
);
}
if (rowDivs.length < maxGuesses) {
rowDivs.push(
<Row
key={i++}
rowState={RowState.Pending}
letters={currentGuess}
target={props.target}
/>
);
while (rowDivs.length < maxGuesses) {
rowDivs.push(
let letterInfo = new Map<string, Clue>();
const rowDivs = Array(props.maxGuesses)
.fill(undefined)
.map((_, i) => {
const guess = [...guesses, currentGuess][i] ?? "";
const cluedLetters = clue(guess, props.target);
if (i < guesses.length) {
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 (
<Row
key={i++}
rowState={RowState.Pending}
letters=""
target={props.target}
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;

42
src/Keyboard.tsx 100644
Wyświetl plik

@ -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>
);
}

Wyświetl plik

@ -1,4 +1,4 @@
import { wordLength } from "./util";
import { Clue, clueClass, CluedLetter } from "./clue";
export enum RowState {
LockedIn,
@ -7,26 +7,19 @@ export enum RowState {
interface RowProps {
rowState: RowState;
letters: string;
target: string;
wordLength: number;
cluedLetters: CluedLetter[];
}
export function Row(props: RowProps) {
const isLockedIn = props.rowState === RowState.LockedIn;
const letterDivs = props.letters
.padEnd(wordLength)
.split("")
.map((letter, i) => {
const letterDivs = props.cluedLetters
.concat(Array(props.wordLength).fill({ clue: Clue.Absent, letter: "" }))
.slice(0, props.wordLength)
.map(({ clue, letter }, i) => {
let letterClass = "Row-letter";
if (isLockedIn) {
if (props.target[i] === letter) {
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";
}
if (isLockedIn && clue !== undefined) {
letterClass += " " + clueClass(clue);
}
return (
<div key={i} className={letterClass}>

49
src/clue.ts 100644
Wyświetl plik

@ -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";
}
}

Plik diff jest za duży Load Diff

Plik diff jest za duży Load Diff

17
src/names.ts 100644
Wyświetl plik

@ -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",
]);

Wyświetl plik

@ -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 {
return array[Math.floor(array.length * Math.random())];