diff --git a/src/App.css b/src/App.css index 325af74..294a05a 100644 --- a/src/App.css +++ b/src/App.css @@ -199,7 +199,7 @@ a:active { } .Game-seed-info { - opacity: 0.5; + opacity: 0.7; margin-top: 1em; font-variant-numeric: tabular-nums; } diff --git a/src/Game.tsx b/src/Game.tsx index 89a63b8..05617b0 100644 --- a/src/Game.tsx +++ b/src/Game.tsx @@ -5,6 +5,7 @@ import { Clue, clue, describeClue, violation } from "./clue"; import { Keyboard } from "./Keyboard"; import targetList from "./targets.json"; import { + describeSeed, dictionarySet, Difficulty, pick, @@ -30,8 +31,8 @@ interface GameProps { } const targets = targetList.slice(0, targetList.indexOf("murky") + 1); // Words no rarer than this one -const minWordLength = 4; -const maxWordLength = 11; +const minLength = 4; +const maxLength = 11; function randomTarget(wordLength: number): string { const eligible = targets.filter((word) => word.length === wordLength); @@ -64,24 +65,51 @@ if (initChallenge && !dictionarySet.has(initChallenge)) { challengeError = true; } +function parseUrlLength(): number { + const lengthParam = urlParam("length"); + if (!lengthParam) return 5; + const length = Number(lengthParam); + return length >= minLength && length <= maxLength ? length : 5; +} + +function parseUrlGameNumber(): number { + const gameParam = urlParam("game"); + if (!gameParam) return 1; + const gameNumber = Number(gameParam); + return gameNumber >= 1 && gameNumber <= 1000 ? gameNumber : 1; +} + function Game(props: GameProps) { const [gameState, setGameState] = useState(GameState.Playing); const [guesses, setGuesses] = useState([]); const [currentGuess, setCurrentGuess] = useState(""); + const [challenge, setChallenge] = useState(initChallenge); + const [wordLength, setWordLength] = useState( + challenge ? challenge.length : parseUrlLength() + ); + const [gameNumber, setGameNumber] = useState(parseUrlGameNumber()); + const [target, setTarget] = useState(() => { + resetRng(); + // Skip RNG ahead to the parsed initial game number: + for (let i = 1; i < gameNumber; i++) randomTarget(wordLength); + return challenge || randomTarget(wordLength); + }); const [hint, setHint] = useState( challengeError ? `Invalid challenge string, playing random game.` : `Make your first guess!` ); - const [challenge, setChallenge] = useState(initChallenge); - const [wordLength, setWordLength] = useState( - challenge ? challenge.length : 5 - ); - const [target, setTarget] = useState(() => { - resetRng(); - return challenge || randomTarget(wordLength); - }); - const [gameNumber, setGameNumber] = useState(1); + const currentSeedParams = () => + `?seed=${seed}&length=${wordLength}&game=${gameNumber}`; + useEffect(() => { + if (seed) { + window.history.replaceState( + {}, + document.title, + window.location.pathname + currentSeedParams() + ); + } + }, [wordLength, gameNumber]); const tableRef = useRef(null); const startNextGame = () => { if (challenge) { @@ -90,17 +118,20 @@ function Game(props: GameProps) { } setChallenge(""); const newWordLength = - wordLength < minWordLength || wordLength > maxWordLength ? 5 : wordLength; + wordLength >= minLength && wordLength <= maxLength ? wordLength : 5; setWordLength(newWordLength); setTarget(randomTarget(newWordLength)); + setHint(""); setGuesses([]); setCurrentGuess(""); - setHint(""); setGameState(GameState.Playing); setGameNumber((x) => x + 1); }; - async function share(url: string, copiedHint: string, text?: string) { + async function share(copiedHint: string, text?: string) { + const url = seed + ? window.location.origin + window.location.pathname + currentSeedParams() + : getChallengeUrl(target); const body = url + (text ? "\n\n" + text : ""); if ( /android|iphone|ipad|ipod|webos/i.test(navigator.userAgent) && @@ -231,8 +262,8 @@ function Game(props: GameProps) { - {gameState !== GameState.Playing && ( -

- {" "} +

+ {challenge + ? "playing a challenge game" + : seed + ? `${describeSeed(seed)} — length ${wordLength}, game ${gameNumber}` + : "playing a random game"} +
+

+ {" "} + {gameState !== GameState.Playing && ( -

- )} - {challenge ? ( -
playing a challenge game
- ) : seed ? ( -
- seed {seed}, length {wordLength}, game {gameNumber} -
- ) : undefined} + )} +

); } diff --git a/src/util.ts b/src/util.ts index 289799b..cb02446 100644 --- a/src/util.ts +++ b/src/util.ts @@ -62,3 +62,28 @@ export function ordinal(n: number): string { export const englishNumbers = "zero one two three four five six seven eight nine ten eleven".split(" "); + +export function describeSeed(seed: number): string { + const year = Math.floor(seed / 10000); + const month = Math.floor(seed / 100) % 100; + const day = seed % 100; + const isLeap = year % (year % 25 ? 4 : 16) === 0; + const feb = isLeap ? 29 : 28; + const days = [0, 31, feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + if ( + year >= 2000 && + year <= 2100 && + month >= 1 && + month <= 12 && + day >= 1 && + day <= days[month] + ) { + return new Date(year, month - 1, day).toLocaleDateString("en-US", { + day: "numeric", + month: "long", + year: "numeric", + }); + } else { + return "seed " + seed; + } +}