Ultra Hard mode
This commit is contained in:
@@ -209,7 +209,7 @@ a:active {
|
|||||||
.Settings-setting {
|
.Settings-setting {
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Settings-setting input {
|
.Settings-setting input {
|
||||||
@@ -217,6 +217,11 @@ a:active {
|
|||||||
height: 18px;
|
height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Settings-setting input[type=range] {
|
||||||
|
width: 50px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.Settings-setting label {
|
.Settings-setting label {
|
||||||
margin-inline-start: 8px;
|
margin-inline-start: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
99
src/App.tsx
99
src/App.tsx
@@ -27,12 +27,13 @@ function useSetting<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [page, setPage] = useState<"game" | "about" | "settings">("game");
|
type Page = "game" | "about" | "settings";
|
||||||
|
const [page, setPage] = useState<Page>("game");
|
||||||
const prefersDark =
|
const prefersDark =
|
||||||
window.matchMedia &&
|
window.matchMedia &&
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
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 [difficulty, setDifficulty] = useSetting<number>("difficulty", 0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.className = dark ? "dark" : "";
|
document.body.className = dark ? "dark" : "";
|
||||||
@@ -41,42 +42,42 @@ function App() {
|
|||||||
}, 1);
|
}, 1);
|
||||||
}, [dark]);
|
}, [dark]);
|
||||||
|
|
||||||
|
const link = (emoji: string, label: string, page: Page) => (
|
||||||
|
<a
|
||||||
|
className="emoji-link"
|
||||||
|
href="#"
|
||||||
|
onClick={() => setPage(page)}
|
||||||
|
title={label}
|
||||||
|
aria-label={label}
|
||||||
|
>
|
||||||
|
{emoji}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App-container">
|
<div className="App-container">
|
||||||
<h1>
|
<h1>
|
||||||
<span style={hard ? { color: "#e66" } : {}}>hell</span>o wordl
|
<span
|
||||||
|
style={
|
||||||
|
difficulty > 0
|
||||||
|
? {
|
||||||
|
color: "#e66",
|
||||||
|
textShadow: difficulty > 1 ? "0px 0px 5px #e66" : "none",
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
hell
|
||||||
|
</span>
|
||||||
|
o wordl
|
||||||
</h1>
|
</h1>
|
||||||
<div className="top-right">
|
<div className="top-right">
|
||||||
{page !== "game" ? (
|
{page !== "game" ? (
|
||||||
<a
|
link("❌", "Close", "game")
|
||||||
className="emoji-link"
|
|
||||||
href="#"
|
|
||||||
onClick={() => setPage("game")}
|
|
||||||
title="Close"
|
|
||||||
aria-label="Close"
|
|
||||||
>
|
|
||||||
❌
|
|
||||||
</a>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<a
|
{link("❓", "About", "about")}
|
||||||
className="emoji-link"
|
{link("⚙️", "Settings", "settings")}
|
||||||
href="#"
|
|
||||||
onClick={() => setPage("about")}
|
|
||||||
title="About"
|
|
||||||
aria-label="About"
|
|
||||||
>
|
|
||||||
❓
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="emoji-link"
|
|
||||||
href="#"
|
|
||||||
onClick={() => setPage("settings")}
|
|
||||||
title="Settings"
|
|
||||||
aria-label="Settings"
|
|
||||||
>
|
|
||||||
⚙️
|
|
||||||
</a>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -114,16 +115,42 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="Settings-setting">
|
<div className="Settings-setting">
|
||||||
<input
|
<input
|
||||||
id="hard-setting"
|
id="difficulty-setting"
|
||||||
type="checkbox"
|
type="range"
|
||||||
checked={hard}
|
min="0"
|
||||||
onChange={() => setHard((x: boolean) => !x)}
|
max="2"
|
||||||
|
value={difficulty}
|
||||||
|
onChange={(e) => setDifficulty(+e.target.value)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="hard-setting">Hard mode (must use all clues)</label>
|
<div>
|
||||||
|
<label htmlFor="difficulty-setting">Difficulty:</label>
|
||||||
|
|
||||||
|
<strong>{["Normal", "Hard", "Ultra Hard"][difficulty]}</strong>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 14,
|
||||||
|
height: 40,
|
||||||
|
marginLeft: 8,
|
||||||
|
marginTop: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
[
|
||||||
|
`No restrictions on guesses.`,
|
||||||
|
`Wordle's "Hard Mode". Green letters must stay fixed, and yellow letters must be reused.`,
|
||||||
|
`An even stricter Hard Mode. Yellow letters must move away, and gray letters can't be reused.`,
|
||||||
|
][difficulty]
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Game maxGuesses={maxGuesses} hidden={page !== "game"} hard={hard} />
|
<Game
|
||||||
|
maxGuesses={maxGuesses}
|
||||||
|
hidden={page !== "game"}
|
||||||
|
difficulty={difficulty}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/Game.tsx
25
src/Game.tsx
@@ -4,7 +4,15 @@ import dictionary from "./dictionary.json";
|
|||||||
import { Clue, clue, describeClue, violation } 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, urlParam } from "./util";
|
import {
|
||||||
|
dictionarySet,
|
||||||
|
Difficulty,
|
||||||
|
pick,
|
||||||
|
resetRng,
|
||||||
|
seed,
|
||||||
|
speak,
|
||||||
|
urlParam,
|
||||||
|
} from "./util";
|
||||||
import { decode, encode } from "./base64";
|
import { decode, encode } from "./base64";
|
||||||
|
|
||||||
enum GameState {
|
enum GameState {
|
||||||
@@ -16,7 +24,7 @@ enum GameState {
|
|||||||
interface GameProps {
|
interface GameProps {
|
||||||
maxGuesses: number;
|
maxGuesses: number;
|
||||||
hidden: boolean;
|
hidden: boolean;
|
||||||
hard: boolean;
|
difficulty: Difficulty;
|
||||||
}
|
}
|
||||||
|
|
||||||
const targets = targetList.slice(0, targetList.indexOf("murky") + 1); // Words no rarer than this one
|
const targets = targetList.slice(0, targetList.indexOf("murky") + 1); // Words no rarer than this one
|
||||||
@@ -111,13 +119,12 @@ function Game(props: GameProps) {
|
|||||||
setHint("Not a valid word");
|
setHint("Not a valid word");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (props.hard) {
|
for (const g of guesses) {
|
||||||
for (const g of guesses) {
|
const c = clue(g, target);
|
||||||
const feedback = violation(clue(g, target), currentGuess);
|
const feedback = violation(props.difficulty, c, currentGuess);
|
||||||
if (feedback) {
|
if (feedback) {
|
||||||
setHint(feedback);
|
setHint(feedback);
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setGuesses((guesses) => guesses.concat([currentGuess]));
|
setGuesses((guesses) => guesses.concat([currentGuess]));
|
||||||
|
|||||||
26
src/clue.ts
26
src/clue.ts
@@ -1,4 +1,4 @@
|
|||||||
import { ordinal } from "./util";
|
import { Difficulty, ordinal } from "./util";
|
||||||
|
|
||||||
export enum Clue {
|
export enum Clue {
|
||||||
Absent,
|
Absent,
|
||||||
@@ -59,21 +59,31 @@ export function describeClue(clue: CluedLetter[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function violation(
|
export function violation(
|
||||||
|
difficulty: Difficulty,
|
||||||
clues: CluedLetter[],
|
clues: CluedLetter[],
|
||||||
guess: string
|
guess: string
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
|
if (difficulty === Difficulty.Normal) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (const { letter, clue } of clues) {
|
for (const { letter, clue } of clues) {
|
||||||
|
const upper = letter.toUpperCase();
|
||||||
|
const nth = ordinal(i + 1);
|
||||||
if (clue === Clue.Absent) {
|
if (clue === Clue.Absent) {
|
||||||
// Apparently Wordle doesn't enforce this?
|
if (difficulty === Difficulty.UltraHard && guess.includes(letter)) {
|
||||||
// if (guess.includes(letter))
|
return "Guess can't contain " + upper;
|
||||||
// return "Guess can't contain " + letter.toUpperCase();
|
}
|
||||||
} else if (clue === Clue.Correct) {
|
} else if (clue === Clue.Correct) {
|
||||||
if (guess[i] !== letter)
|
if (guess[i] !== letter) {
|
||||||
return ordinal(i + 1) + " letter must be " + letter.toUpperCase();
|
return nth + " letter must be " + upper;
|
||||||
|
}
|
||||||
} else if (clue === Clue.Elsewhere) {
|
} else if (clue === Clue.Elsewhere) {
|
||||||
if (!guess.includes(letter))
|
if (!guess.includes(letter)) {
|
||||||
return "Guess must contain " + letter.toUpperCase();
|
return "Guess must contain " + upper;
|
||||||
|
} else if (difficulty === Difficulty.UltraHard && guess[i] === letter) {
|
||||||
|
return nth + " letter can't be " + upper;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import dictionary from "./dictionary.json";
|
import dictionary from "./dictionary.json";
|
||||||
|
|
||||||
|
export enum Difficulty {
|
||||||
|
Normal,
|
||||||
|
Hard,
|
||||||
|
UltraHard,
|
||||||
|
}
|
||||||
|
|
||||||
export const maxGuesses = 6;
|
export const maxGuesses = 6;
|
||||||
|
|
||||||
export const dictionarySet: Set<string> = new Set(dictionary);
|
export const dictionarySet: Set<string> = new Set(dictionary);
|
||||||
|
|||||||
Reference in New Issue
Block a user