dim letters that are part of the puzzle; improve uncommon word handling; add example game to about

This commit is contained in:
ashastral 2022-02-28 19:30:23 -08:00
parent 6fced58fb7
commit 8032a835c9
5 changed files with 136 additions and 21 deletions

View file

@ -1,4 +1,6 @@
import { h } from 'preact'; import { h } from 'preact';
import { Clue } from './clue';
import { Row, RowState } from './Row';
import { gameName } from "./util"; import { gameName } from "./util";
export function About() { export function About() {
@ -25,9 +27,11 @@ export function About() {
<br /> <br />
<br /> <br />
Like Wordle &amp; hello wordl, the game has two separate word lists: a Like Wordle &amp; hello wordl, the game has two separate word lists: a
"common" list for secret words, and a broader list including "uncommon" "secret list" containing only reasonably common words, and a larger list of
words for your guesses. The game will stop you from guessing an uncommon words you're allowed to guess. The game will let you know when you've typed
word for your second guess, since it has no chance of being the secret word. a word that doesn't appear in the secret list; you can still guess it if you
want, but it can't be the secret word, so try something else if you're on your
second guess!
<br /> <br />
<br /> <br />
There's a new puzzle every day, starting at 6 AM in all timezones. There's a new puzzle every day, starting at 6 AM in all timezones.
@ -37,6 +41,99 @@ export function About() {
forced to guess an uncommon word to win! forced to guess an uncommon word to win!
</p> </p>
<hr /> <hr />
<Row
rowState={RowState.LockedIn}
wordLength={5}
isPartOfPuzzle={true}
cluedLetters={[
{ clue: Clue.Absent, letter: "b" },
{ clue: Clue.Absent, letter: "a" },
{ clue: Clue.Elsewhere, letter: "t" },
{ clue: Clue.Absent, letter: "c" },
{ clue: Clue.Correct, letter: "h" },
]}
/>
<Row
rowState={RowState.LockedIn}
wordLength={5}
isPartOfPuzzle={true}
cluedLetters={[
{ clue: Clue.Absent, letter: "y" },
{ clue: Clue.Correct, letter: "o" },
{ clue: Clue.Absent, letter: "u" },
{ clue: Clue.Correct, letter: "t" },
{ clue: Clue.Correct, letter: "h" },
]}
/>
<Row
rowState={RowState.LockedIn}
wordLength={5}
isPartOfPuzzle={true}
cluedLetters={[
{ clue: Clue.Absent, letter: "t" },
{ clue: Clue.Correct, letter: "o" },
{ clue: Clue.Absent, letter: "o" },
{ clue: Clue.Correct, letter: "t" },
{ clue: Clue.Correct, letter: "h" },
]}
/>
<p>
We know the word has an <b class="green-bg">O</b> in the second position
and ends with <b class="green-bg">TH</b>. What words could it be?
</p>
<Row
rowState={RowState.LockedIn}
wordLength={5}
isPartOfPuzzle={false}
cluedLetters={[
{ clue: Clue.Absent, letter: "n" },
{ clue: Clue.Correct, letter: "o" },
{ clue: Clue.Correct, letter: "r" },
{ clue: Clue.Correct, letter: "t" },
{ clue: Clue.Correct, letter: "h" },
]}
/>
<Row
rowState={RowState.LockedIn}
wordLength={5}
isPartOfPuzzle={false}
cluedLetters={[
{ clue: Clue.Absent, letter: "f" },
{ clue: Clue.Correct, letter: "o" },
{ clue: Clue.Correct, letter: "r" },
{ clue: Clue.Correct, letter: "t" },
{ clue: Clue.Correct, letter: "h" },
]}
/>
<p style={{ fontSize: "80%" }}>Not quite! The answer could've been <strong>WORTH</strong>.</p>
<p>Looks like we didn't cover all our bases. Let's see if we can account for that <b>W</b>:</p>
<Row
rowState={RowState.LockedIn}
wordLength={5}
isPartOfPuzzle={false}
cluedLetters={[
{ clue: Clue.Absent, letter: "s" },
{ clue: Clue.Absent, letter: "w" },
{ clue: Clue.Elsewhere, letter: "o" },
{ clue: Clue.Elsewhere, letter: "r" },
{ clue: Clue.Absent, letter: "n" },
]}
/>
<Row
rowState={RowState.LockedIn}
wordLength={5}
isPartOfPuzzle={false}
cluedLetters={[
{ clue: Clue.Correct, letter: "f" },
{ clue: Clue.Correct, letter: "o" },
{ clue: Clue.Correct, letter: "r" },
{ clue: Clue.Correct, letter: "t" },
{ clue: Clue.Correct, letter: "h" },
]}
/>
<p style={{ fontSize: "80%" }}>You won in 2 tries! Great job.</p>
<p>There! The game had no choice but to narrow it down to one word.</p>
<hr />
<p> <p>
Like hello wordl, this game will be free and ad-free for as long as Like hello wordl, this game will be free and ad-free for as long as
it's online. it's online.

View file

@ -142,18 +142,27 @@ table.Game-rows > tbody {
background-color: rgb(87, 172, 120); background-color: rgb(87, 172, 120);
color: white !important; color: white !important;
} }
.letter-correct.dim {
background-color: rgba(87, 172, 120, 0.75);
}
.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: rgb(233, 198, 1);
color: white !important; color: white !important;
} }
.letter-elsewhere.dim {
background-color: rgba(233, 198, 1, 0.75);
}
.letter-absent { .letter-absent {
border: 2px solid transparent; border: 2px solid transparent;
background-color: hsla(190, 12.5%, 50%, 0.6); background-color: hsla(190, 12.5%, 40%, 0.6);
color: white !important; color: white !important;
} }
.letter-absent.dim {
opacity: .75;
}
body.dark { body.dark {
background-color: #0b3644; background-color: #0b3644;

View file

@ -122,11 +122,21 @@ export function Game(props: GameProps) {
} }
if (guesses.length === MAX_GUESSES) return; if (guesses.length === MAX_GUESSES) return;
if (/^[a-z]$/i.test(key)) { if (/^[a-z]$/i.test(key)) {
// We're about to update the current guess, but
// we can't rely on the value updating in this render
const newGuess = currentGuess + key.toLowerCase();
setCurrentGuess((guess) => setCurrentGuess((guess) =>
(guess + key.toLowerCase()).slice(0, WORD_LENGTH) (guess + key.toLowerCase()).slice(0, WORD_LENGTH)
); );
tableRef.current?.focus(); tableRef.current?.focus();
setHint(""); if (newGuess.length === WORD_LENGTH
&& !COMMON_WORDS.includes(newGuess)
&& UNCOMMON_WORDS.includes(newGuess)
) {
setHint(`(The word ${newGuess.toUpperCase()} isn't in the secret list)`);
} else {
setHint("");
}
} else if (key === "Backspace") { } else if (key === "Backspace") {
setCurrentGuess((guess) => guess.slice(0, -1)); setCurrentGuess((guess) => guess.slice(0, -1));
setHint(""); setHint("");
@ -135,16 +145,11 @@ export function Game(props: GameProps) {
setHint("Too short"); setHint("Too short");
return; return;
} }
if (!COMMON_WORDS.includes(currentGuess)) { if (!COMMON_WORDS.includes(currentGuess)
if (!UNCOMMON_WORDS.includes(currentGuess)) { && !UNCOMMON_WORDS.includes(currentGuess)
setHint("Not a valid word"); ) {
return; setHint("Not a valid word");
} else if (guesses.length === puzzle.history.length) { return;
setHint("(Uncommon word allowed for first guess)");
} else {
setHint("(Uncommon word not allowed for second guess)");
return;
}
} }
for (const g of guesses) { for (const g of guesses) {
const c = clue(g); const c = clue(g);
@ -200,6 +205,7 @@ export function Game(props: GameProps) {
const tableRows = Array(puzzle.history.length + MAX_GUESSES) const tableRows = Array(puzzle.history.length + MAX_GUESSES)
.fill(undefined) .fill(undefined)
.map((_, i) => { .map((_, i) => {
const isPartOfPuzzle = i < puzzle.history.length;
const result = [ const result = [
...guesses, ...guesses,
{ word: currentGuess, matches: [0, 0, 0, 0, 0] }, { word: currentGuess, matches: [0, 0, 0, 0, 0] },
@ -227,6 +233,7 @@ export function Game(props: GameProps) {
: RowState.Pending : RowState.Pending
} }
cluedLetters={cluedLetters} cluedLetters={cluedLetters}
isPartOfPuzzle={isPartOfPuzzle}
/> />
); );
}); });

View file

@ -1,4 +1,4 @@
import { h } from "preact"; import { h, JSX } from "preact";
import { Clue, clueClass, CluedLetter, clueWord } from "./clue"; import { Clue, clueClass, CluedLetter, clueWord } from "./clue";
export enum RowState { export enum RowState {
@ -11,7 +11,8 @@ interface RowProps {
rowState: RowState; rowState: RowState;
wordLength: number; wordLength: number;
cluedLetters: CluedLetter[]; cluedLetters: CluedLetter[];
annotation?: string; annotation?: string | JSX.Element;
isPartOfPuzzle: boolean;
} }
export function Row(props: RowProps) { export function Row(props: RowProps) {
@ -28,7 +29,7 @@ export function Row(props: RowProps) {
return ( return (
<td <td
key={i} key={i}
className={letterClass} className={`${letterClass} ${props.isPartOfPuzzle ? "dim" : ""}`}
aria-live={isEditing ? "assertive" : "off"} aria-live={isEditing ? "assertive" : "off"}
aria-label={ aria-label={
isLockedIn isLockedIn

View file

@ -1,6 +1,7 @@
[ [
{"history":[{"word":"batch","matches":[0,0,1,0,2]},{"word":"youth","matches":[0,2,0,2,2]},{"word":"tooth","matches":[0,2,0,2,2]}],"seed":0,"solutions":40}, {"history":[{"word":"batch","matches":[0,0,1,0,2]},{"word":"youth","matches":[0,2,0,2,2]},{"word":"tooth","matches":[0,2,0,2,2]}],"seed":1,"solutions":40},
{"history":[{"word":"grope","matches":[0,1,0,0,1]},{"word":"teary","matches":[0,1,1,1,0]},{"word":"maker","matches":[0,2,0,2,2]},{"word":"waver","matches":[0,2,0,2,2]},{"word":"baler","matches":[0,2,0,2,2]}],"seed":1,"solutions":29}, {"history":[{"word":"batch","matches":[0,0,1,0,2]},{"word":"youth","matches":[0,2,0,2,2]},{"word":"tooth","matches":[0,2,0,2,2]}],"seed":1,"solutions":40},
{"history":[{"word":"decal","matches":[0,1,0,0,0]},{"word":"inner","matches":[1,1,0,1,0]},{"word":"ovine","matches":[0,0,2,2,2]}],"seed":2,"solutions":32}, {"history":[{"word":"decal","matches":[0,1,0,0,0]},{"word":"inner","matches":[1,1,0,1,0]},{"word":"ovine","matches":[0,0,2,2,2]}],"seed":2,"solutions":32},
{"history":[{"word":"glean","matches":[2,0,1,1,0]},{"word":"grade","matches":[2,2,2,0,2]},{"word":"grace","matches":[2,2,2,0,2]}],"seed":3,"solutions":2}, {"history":[{"word":"glean","matches":[2,0,1,1,0]},{"word":"grade","matches":[2,2,2,0,2]},{"word":"grace","matches":[2,2,2,0,2]}],"seed":3,"solutions":2},
{"history":[{"word":"sassy","matches":[0,2,0,0,0]},{"word":"rabbi","matches":[0,2,0,0,0]},{"word":"naval","matches":[1,2,0,0,0]},{"word":"oaken","matches":[0,2,0,0,1]},{"word":"haunt","matches":[0,2,2,2,2]}],"seed":4,"solutions":1}, {"history":[{"word":"sassy","matches":[0,2,0,0,0]},{"word":"rabbi","matches":[0,2,0,0,0]},{"word":"naval","matches":[1,2,0,0,0]},{"word":"oaken","matches":[0,2,0,0,1]},{"word":"haunt","matches":[0,2,2,2,2]}],"seed":4,"solutions":1},