extract settings stuff to localStorage, store daily game stats & preserve try count
This commit is contained in:
parent
65f20beaf7
commit
e0e2f3b2ef
3 changed files with 92 additions and 35 deletions
|
@ -9,6 +9,8 @@ import { COMMON_WORDS, UNCOMMON_WORDS } from './dictionary';
|
|||
import * as PUZZLES from './puzzles.json';
|
||||
import { Row, RowState } from './Row';
|
||||
import { Keyboard } from './Keyboard';
|
||||
import { useLocalStorage } from './localstorage';
|
||||
|
||||
const MAX_GUESSES = 2;
|
||||
const WORD_LENGTH = 5;
|
||||
const EPOCH = new Date(2022, 1, 27);
|
||||
|
@ -27,18 +29,25 @@ interface GameProps {
|
|||
|
||||
type History = GuessResult[];
|
||||
|
||||
type DailyStats = {
|
||||
tries: number,
|
||||
won: boolean,
|
||||
winningGuesses?: GuessResult[],
|
||||
};
|
||||
|
||||
export function Game(props: GameProps) {
|
||||
const puzzle = useMemo(() => {
|
||||
const day = useMemo(() => {
|
||||
let date = new Date();
|
||||
date.setHours(date.getHours() - 6); // treat 6 AM as the start of the day
|
||||
date.setHours(0); // now set it to midnight of the day
|
||||
const day = Math.round((date.getTime() - EPOCH.getTime()) / (24 * 60 * 60 * 1000));
|
||||
return PUZZLES[day];
|
||||
return Math.round((date.getTime() - EPOCH.getTime()) / (24 * 60 * 60 * 1000));
|
||||
}, []);
|
||||
const puzzle = PUZZLES[day];
|
||||
const [gameState, setGameState] = useState(GameState.Playing);
|
||||
const [guesses, setGuesses] = useState<GuessResult[]>([...puzzle.history]);
|
||||
const [currentGuess, setCurrentGuess] = useState<string>("");
|
||||
const [tryNumber, setTryNumber] = useState(1);
|
||||
const [dailyStats, setDailyStats] = useLocalStorage<{ [key: number]: DailyStats }>("dailyTries", {});
|
||||
const [todayStats, setTodayStats] = useState<DailyStats>({ tries: 1, won: false });
|
||||
const [hint, setHint] = useState<JSX.Element | string>(
|
||||
<p>
|
||||
Try to guarantee a win in <strong>2 guesses</strong>!
|
||||
|
@ -51,12 +60,38 @@ export function Game(props: GameProps) {
|
|||
setGuesses([...puzzle.history]);
|
||||
setCurrentGuess("");
|
||||
setGameState(GameState.Playing);
|
||||
setTryNumber((x) => x + 1);
|
||||
setTodayStats((oldStats) => ({
|
||||
...oldStats,
|
||||
tries: oldStats.tries + 1,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const storedTodayStats = dailyStats[day];
|
||||
if (storedTodayStats !== undefined) {
|
||||
setTodayStats(storedTodayStats);
|
||||
if (storedTodayStats.won) {
|
||||
setGameState(GameState.Won);
|
||||
setHint("You already won today's puzzle!");
|
||||
if (storedTodayStats.winningGuesses) {
|
||||
setGuesses([
|
||||
...puzzle.history,
|
||||
...storedTodayStats.winningGuesses,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
setDailyStats({
|
||||
...dailyStats,
|
||||
[day]: todayStats,
|
||||
});
|
||||
}, [todayStats]);
|
||||
|
||||
async function share(copiedHint: string, text?: string) {
|
||||
const url = window.location.origin + window.location.pathname;
|
||||
const body = `${text}\n\n${url}`;
|
||||
const body = `${text}\n${url}`;
|
||||
if (
|
||||
/android|iphone|ipad|ipod|webos/i.test(navigator.userAgent) &&
|
||||
!/firefox/i.test(navigator.userAgent)
|
||||
|
@ -123,12 +158,20 @@ export function Game(props: GameProps) {
|
|||
setCurrentGuess("");
|
||||
|
||||
if (matches.every((m) => m === 2)) {
|
||||
if (tryNumber === 1) {
|
||||
if (todayStats.tries === 1) {
|
||||
setHint("You won on your first try! Amazing job.");
|
||||
} else {
|
||||
setHint(`You won in ${tryNumber} tries! Great job.`);
|
||||
setHint(`You won in ${todayStats.tries} tries! Great job.`);
|
||||
}
|
||||
setGameState(GameState.Won);
|
||||
setTodayStats((oldStats) => ({
|
||||
...oldStats,
|
||||
won: true,
|
||||
winningGuesses: [
|
||||
...guesses.slice(puzzle.history.length),
|
||||
result,
|
||||
],
|
||||
}))
|
||||
} else if (guesses.length + 1 === puzzle.history.length + MAX_GUESSES) {
|
||||
setHint(<p>Not quite! The answer could've been <strong>{game.possible_word().toUpperCase()}</strong>. (Enter to try again)</p>);
|
||||
setGameState(GameState.Lost);
|
||||
|
@ -216,10 +259,21 @@ export function Game(props: GameProps) {
|
|||
{gameState === GameState.Won && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const score = tryNumber === 1 ? "my first try!" : `try #${tryNumber}.`;
|
||||
const emoji = props.colorBlind
|
||||
? ["⬛", "🟦", "🟧"]
|
||||
: ["⬛", "🟨", "🟩"];
|
||||
const score = todayStats.tries === 1 ? "my first try!" : `try #${todayStats.tries}!`;
|
||||
share(
|
||||
"Result copied to clipboard!",
|
||||
`I solved ${gameName} #${puzzle.seed} on ${score}`
|
||||
`I solved ${gameName} #${puzzle.seed} on ${score}\n` +
|
||||
guesses
|
||||
.slice(puzzle.history.length)
|
||||
.map((result) =>
|
||||
clue(result)
|
||||
.map((c) => emoji[c.clue ?? 0])
|
||||
.join("")
|
||||
)
|
||||
.join("\n")
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -4,28 +4,8 @@ import { Game } from './Game';
|
|||
import { gameName, urlParam } from "./util";
|
||||
import "./App.css";
|
||||
import { About } from './About';
|
||||
import { useLocalStorage } from './localstorage';
|
||||
|
||||
function useSetting<T>(
|
||||
key: string,
|
||||
initial: T
|
||||
): [T, (value: T | ((t: T) => T)) => void] {
|
||||
const [current, setCurrent] = useState<T>(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : initial;
|
||||
} catch (e) {
|
||||
return initial;
|
||||
}
|
||||
});
|
||||
const setSetting = (value: T | ((t: T) => T)) => {
|
||||
try {
|
||||
const v = value instanceof Function ? value(current) : value;
|
||||
setCurrent(v);
|
||||
window.localStorage.setItem(key, JSON.stringify(v));
|
||||
} catch (e) { }
|
||||
};
|
||||
return [current, setSetting];
|
||||
}
|
||||
|
||||
export function App() {
|
||||
type Page = "game" | "about" | "settings";
|
||||
|
@ -33,13 +13,13 @@ export function App() {
|
|||
const prefersDark =
|
||||
window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
const [dark, setDark] = useSetting<boolean>("dark", prefersDark);
|
||||
const [colorBlind, setColorBlind] = useSetting<boolean>("colorblind", false);
|
||||
const [keyboard, setKeyboard] = useSetting<string>(
|
||||
const [dark, setDark] = useLocalStorage<boolean>("dark", prefersDark);
|
||||
const [colorBlind, setColorBlind] = useLocalStorage<boolean>("colorblind", false);
|
||||
const [keyboard, setKeyboard] = useLocalStorage<string>(
|
||||
"keyboard",
|
||||
"qwertyuiop-asdfghjkl-BzxcvbnmE"
|
||||
);
|
||||
const [enterLeft, setEnterLeft] = useSetting<boolean>("enter-left", false);
|
||||
const [enterLeft, setEnterLeft] = useLocalStorage<boolean>("enter-left", false);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.className = dark ? "dark" : "";
|
||||
|
|
23
web-optimle/www/src/localstorage.ts
Normal file
23
web-optimle/www/src/localstorage.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { useState } from "preact/hooks";
|
||||
|
||||
export function useLocalStorage<T>(
|
||||
key: string,
|
||||
initial: T
|
||||
): [T, (value: T | ((t: T) => T)) => void] {
|
||||
const [current, setCurrent] = useState<T>(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : initial;
|
||||
} catch (e) {
|
||||
return initial;
|
||||
}
|
||||
});
|
||||
const setSetting = (value: T | ((t: T) => T)) => {
|
||||
try {
|
||||
const v = value instanceof Function ? value(current) : value;
|
||||
setCurrent(v);
|
||||
window.localStorage.setItem(key, JSON.stringify(v));
|
||||
} catch (e) { }
|
||||
};
|
||||
return [current, setSetting];
|
||||
}
|
Loading…
Reference in a new issue