Add hard mode

This commit is contained in:
Lynn 2022-01-17 23:25:44 +01:00
parent 83dc909cc4
commit d7e2f52f11
6 changed files with 97 additions and 44 deletions

View file

@ -5,6 +5,7 @@
body { body {
text-align: center; text-align: center;
background-color: #eeeeee; background-color: #eeeeee;
transition: 0.3s background-color ease-out;
} }
.Row { .Row {
@ -14,7 +15,7 @@ body {
.Row-letter { .Row-letter {
margin: 2px; margin: 2px;
border: 2px solid rgba(0, 0, 0, 0.4); border: 2px solid rgba(128, 128, 128, 0.8);
flex: 1; flex: 1;
max-width: 40px; max-width: 40px;
height: 40px; height: 40px;
@ -39,10 +40,14 @@ body {
margin-top: 0; margin-top: 0;
} }
.Game,
h1 {
user-select: none;
}
.Game { .Game {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
user-select: none;
} }
table.Game-rows { table.Game-rows {
@ -99,51 +104,29 @@ table.Game-rows > tbody {
.letter-correct { .letter-correct {
border: 2px solid rgba(0, 0, 0, 0.3); border: 2px solid rgba(0, 0, 0, 0.3);
background-color: rgb(87, 172, 87); background-color: rgb(87, 172, 120);
color: white; color: white !important;
} }
.letter-elsewhere { .letter-elsewhere {
border: 2px dotted rgba(0, 0, 0, 0.3); border: 2px dotted rgba(0, 0, 0, 0.3);
background-color: #e9c601; background-color: #e9c601;
color: white; color: white !important;
} }
.letter-absent { .letter-absent {
border: 2px solid transparent; border: 2px solid transparent;
background-color: rgb(162, 162, 162); background-color: rgb(162, 162, 162);
color: white; color: white !important;
} }
@media (prefers-color-scheme: dark) { body.dark {
body { background-color: #404040;
background-color: #404040; color: #e0e0e0;
color: #e0e0e0; }
}
.Game-keyboard-button { body.dark .Game-keyboard-button {
color: #404040; color: #404040;
}
.Row-letter {
border: 2px solid rgba(255, 255, 255, 0.4);
}
.letter-correct {
border: 2px solid rgba(0, 0, 0, 0.3);
color: white;
}
.letter-elsewhere {
border: 2px dotted rgba(0, 0, 0, 0.3);
color: white;
}
.letter-absent {
border: 2px solid transparent;
background-color: rgb(142, 142, 142);
color: white;
}
} }
a, a,
@ -196,3 +179,23 @@ a:active {
height: 1px; height: 1px;
overflow: hidden; overflow: hidden;
} }
.Settings {
text-align: left;
font-size: 18px;
}
.Settings-setting {
margin: 8px;
display: flex;
align-items: center;
}
.Settings-setting input {
width: 18px;
height: 18px;
}
.Settings-setting label {
margin-inline-start: 8px;
}

View file

@ -1,10 +1,13 @@
import "./App.css"; import "./App.css";
import { maxGuesses, seed } from "./util"; import { maxGuesses, seed } from "./util";
import Game from "./Game"; import Game from "./Game";
import { useState } from "react"; import { useEffect, useState } from "react";
import { About } from "./About"; import { About } from "./About";
function useSetting<T>(key: string, initial: T): [T, (value: T | ((t: T) => T)) => void] { function useSetting<T>(
key: string,
initial: T
): [T, (value: T | ((t: T) => T)) => void] {
const [current, setCurrent] = useState<T>(() => { const [current, setCurrent] = useState<T>(() => {
try { try {
const item = window.localStorage.getItem(key); const item = window.localStorage.getItem(key);
@ -26,13 +29,20 @@ function useSetting<T>(key: string, initial: T): [T, (value: T | ((t: T) => T))
function App() { function App() {
const [page, setPage] = useState<"game" | "about" | "settings">("game"); const [page, setPage] = useState<"game" | "about" | "settings">("game");
const prefersDark = const prefersDark =
window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches; window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches;
const [dark, setDark] = useSetting<boolean>("dark", prefersDark); const [dark, setDark] = useSetting<boolean>("dark", prefersDark);
const [hard, setHard] = useSetting<boolean>("hard", false); const [hard, setHard] = useSetting<boolean>("hard", false);
useEffect(() => {
document.body.className = dark ? "dark" : "";
}, [dark]);
return ( return (
<div className="App-container"> <div className="App-container">
<h1>hello wordl</h1> <h1>
<span style={hard ? { color: "#e66" } : {}}>hell</span>o wordl
</h1>
<div style={{ position: "absolute", right: 5, top: 5 }}> <div style={{ position: "absolute", right: 5, top: 5 }}>
{page !== "game" ? ( {page !== "game" ? (
<a href="#" onClick={() => setPage("game")}> <a href="#" onClick={() => setPage("game")}>
@ -63,7 +73,8 @@ function App() {
onClick={() => onClick={() =>
(document.location = seed (document.location = seed
? "?" ? "?"
: "?seed=" + new Date().toISOString().replace(/-/g, "").slice(0, 8)) : "?seed=" +
new Date().toISOString().replace(/-/g, "").slice(0, 8))
} }
> >
{seed ? "Random" : "Today's"} {seed ? "Random" : "Today's"}
@ -88,7 +99,7 @@ function App() {
checked={hard} checked={hard}
onChange={() => setHard((x: boolean) => !x)} onChange={() => setHard((x: boolean) => !x)}
/> />
<label htmlFor="hard-setting">Hard mode (must use clues)</label> <label htmlFor="hard-setting">Hard mode (must use all clues)</label>
</div> </div>
</div> </div>
)} )}

View file

@ -1,7 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Row, RowState } from "./Row"; import { Row, RowState } from "./Row";
import dictionary from "./dictionary.json"; import dictionary from "./dictionary.json";
import { Clue, clue, describeClue } from "./clue"; import { Clue, clue, describeClue, violation } from "./clue";
import { Keyboard } from "./Keyboard"; import { Keyboard } from "./Keyboard";
import targetList from "./targets.json"; import targetList from "./targets.json";
import { dictionarySet, pick, resetRng, seed, speak } from "./util"; import { dictionarySet, pick, resetRng, seed, speak } from "./util";
@ -58,6 +58,11 @@ function Game(props: GameProps) {
setCurrentGuess((guess) => setCurrentGuess((guess) =>
(guess + key.toLowerCase()).slice(0, wordLength) (guess + key.toLowerCase()).slice(0, wordLength)
); );
// 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();
}
setHint(""); setHint("");
} else if (key === "Backspace") { } else if (key === "Backspace") {
setCurrentGuess((guess) => guess.slice(0, -1)); setCurrentGuess((guess) => guess.slice(0, -1));
@ -71,6 +76,15 @@ function Game(props: GameProps) {
setHint("Not a valid word"); setHint("Not a valid word");
return; return;
} }
if (props.hard) {
for (const g of guesses) {
const feedback = violation(clue(g, target), currentGuess);
if (feedback) {
setHint(feedback);
return;
}
}
}
setGuesses((guesses) => guesses.concat([currentGuess])); setGuesses((guesses) => guesses.concat([currentGuess]));
setCurrentGuess((guess) => ""); setCurrentGuess((guess) => "");
if (currentGuess === target) { if (currentGuess === target) {

View file

@ -1,3 +1,5 @@
import { ordinal } from "./util";
export enum Clue { export enum Clue {
Absent, Absent,
Elsewhere, Elsewhere,
@ -55,3 +57,25 @@ export function describeClue(clue: CluedLetter[]): string {
.map(({ letter, clue }) => letter.toUpperCase() + " " + clueWord(clue!)) .map(({ letter, clue }) => letter.toUpperCase() + " " + clueWord(clue!))
.join(", "); .join(", ");
} }
export function violation(
clues: CluedLetter[],
guess: string
): string | undefined {
let i = 0;
for (const { letter, clue } of clues) {
if (clue === Clue.Absent) {
// Apparently Wordle doesn't enforce this?
// if (guess.includes(letter))
// return "Guess can't contain " + letter.toUpperCase();
} else if (clue === Clue.Correct) {
if (guess[i] !== letter)
return ordinal(i + 1) + " letter must be " + letter.toUpperCase();
} else if (clue === Clue.Elsewhere) {
if (!guess.includes(letter))
return "Guess must contain " + letter.toUpperCase();
}
++i;
}
return undefined;
}

View file

@ -6714,8 +6714,6 @@
"reminder", "reminder",
"exodus", "exodus",
"clash", "clash",
"bobby",
"syphilis",
"demon", "demon",
"authorization", "authorization",
"richness", "richness",
@ -20795,7 +20793,6 @@
"nicety", "nicety",
"intrepidity", "intrepidity",
"shopper", "shopper",
"empyema",
"whitewash", "whitewash",
"uncontrollably", "uncontrollably",
"atelectasis", "atelectasis",

View file

@ -47,3 +47,7 @@ export function speak(
document.body.removeChild(document.getElementById(id)!); document.body.removeChild(document.getElementById(id)!);
}, 1000); }, 1000);
} }
export function ordinal(n: number): string {
return n + ([, "st", "nd", "rd"][(n % 100 >> 3) ^ 1 && n % 10] || "th");
}