Seeded game links, allow share before end (fix #64)

This commit is contained in:
Lynn
2022-02-01 15:30:28 +01:00
parent 1c1ddbcc6e
commit 60111b6547
3 changed files with 91 additions and 39 deletions

View File

@@ -199,7 +199,7 @@ a:active {
} }
.Game-seed-info { .Game-seed-info {
opacity: 0.5; opacity: 0.7;
margin-top: 1em; margin-top: 1em;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }

View File

@@ -5,6 +5,7 @@ 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 { import {
describeSeed,
dictionarySet, dictionarySet,
Difficulty, Difficulty,
pick, pick,
@@ -30,8 +31,8 @@ interface GameProps {
} }
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
const minWordLength = 4; const minLength = 4;
const maxWordLength = 11; const maxLength = 11;
function randomTarget(wordLength: number): string { function randomTarget(wordLength: number): string {
const eligible = targets.filter((word) => word.length === wordLength); const eligible = targets.filter((word) => word.length === wordLength);
@@ -64,24 +65,51 @@ if (initChallenge && !dictionarySet.has(initChallenge)) {
challengeError = true; 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) { function Game(props: GameProps) {
const [gameState, setGameState] = useState(GameState.Playing); const [gameState, setGameState] = useState(GameState.Playing);
const [guesses, setGuesses] = useState<string[]>([]); const [guesses, setGuesses] = useState<string[]>([]);
const [currentGuess, setCurrentGuess] = useState<string>(""); const [currentGuess, setCurrentGuess] = useState<string>("");
const [challenge, setChallenge] = useState<string>(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<string>( const [hint, setHint] = useState<string>(
challengeError challengeError
? `Invalid challenge string, playing random game.` ? `Invalid challenge string, playing random game.`
: `Make your first guess!` : `Make your first guess!`
); );
const [challenge, setChallenge] = useState<string>(initChallenge); const currentSeedParams = () =>
const [wordLength, setWordLength] = useState( `?seed=${seed}&length=${wordLength}&game=${gameNumber}`;
challenge ? challenge.length : 5 useEffect(() => {
); if (seed) {
const [target, setTarget] = useState(() => { window.history.replaceState(
resetRng(); {},
return challenge || randomTarget(wordLength); document.title,
}); window.location.pathname + currentSeedParams()
const [gameNumber, setGameNumber] = useState(1); );
}
}, [wordLength, gameNumber]);
const tableRef = useRef<HTMLTableElement>(null); const tableRef = useRef<HTMLTableElement>(null);
const startNextGame = () => { const startNextGame = () => {
if (challenge) { if (challenge) {
@@ -90,17 +118,20 @@ function Game(props: GameProps) {
} }
setChallenge(""); setChallenge("");
const newWordLength = const newWordLength =
wordLength < minWordLength || wordLength > maxWordLength ? 5 : wordLength; wordLength >= minLength && wordLength <= maxLength ? wordLength : 5;
setWordLength(newWordLength); setWordLength(newWordLength);
setTarget(randomTarget(newWordLength)); setTarget(randomTarget(newWordLength));
setHint("");
setGuesses([]); setGuesses([]);
setCurrentGuess(""); setCurrentGuess("");
setHint("");
setGameState(GameState.Playing); setGameState(GameState.Playing);
setGameNumber((x) => x + 1); 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 : ""); const body = url + (text ? "\n\n" + text : "");
if ( if (
/android|iphone|ipad|ipod|webos/i.test(navigator.userAgent) && /android|iphone|ipad|ipod|webos/i.test(navigator.userAgent) &&
@@ -231,8 +262,8 @@ function Game(props: GameProps) {
<label htmlFor="wordLength">Letters:</label> <label htmlFor="wordLength">Letters:</label>
<input <input
type="range" type="range"
min={minWordLength} min={minLength}
max={maxWordLength} max={maxLength}
id="wordLength" id="wordLength"
disabled={ disabled={
gameState === GameState.Playing && gameState === GameState.Playing &&
@@ -287,25 +318,28 @@ function Game(props: GameProps) {
letterInfo={letterInfo} letterInfo={letterInfo}
onKey={onKey} onKey={onKey}
/> />
{gameState !== GameState.Playing && ( <div className="Game-seed-info">
<p> {challenge
<button ? "playing a challenge game"
onClick={() => { : seed
share( ? `${describeSeed(seed)} — length ${wordLength}, game ${gameNumber}`
getChallengeUrl(target), : "playing a random game"}
"Challenge link copied to clipboard!" </div>
); <p>
}} <button
> onClick={() => {
Challenge a friend to this word share("Link copied to clipboard!");
</button>{" "} }}
>
Share a link to this game
</button>{" "}
{gameState !== GameState.Playing && (
<button <button
onClick={() => { onClick={() => {
const emoji = props.colorBlind const emoji = props.colorBlind
? ["⬛", "🟦", "🟧"] ? ["⬛", "🟦", "🟧"]
: ["⬛", "🟨", "🟩"]; : ["⬛", "🟨", "🟩"];
share( share(
getChallengeUrl(target),
"Result copied to clipboard!", "Result copied to clipboard!",
guesses guesses
.map((guess) => .map((guess) =>
@@ -319,15 +353,8 @@ function Game(props: GameProps) {
> >
Share emoji results Share emoji results
</button> </button>
</p> )}
)} </p>
{challenge ? (
<div className="Game-seed-info">playing a challenge game</div>
) : seed ? (
<div className="Game-seed-info">
seed {seed}, length {wordLength}, game {gameNumber}
</div>
) : undefined}
</div> </div>
); );
} }

View File

@@ -62,3 +62,28 @@ export function ordinal(n: number): string {
export const englishNumbers = export const englishNumbers =
"zero one two three four five six seven eight nine ten eleven".split(" "); "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;
}
}