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 {
opacity: 0.5;
opacity: 0.7;
margin-top: 1em;
font-variant-numeric: tabular-nums;
}

View File

@@ -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<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>(
challengeError
? `Invalid challenge string, playing random game.`
: `Make your first guess!`
);
const [challenge, setChallenge] = useState<string>(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<HTMLTableElement>(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) {
<label htmlFor="wordLength">Letters:</label>
<input
type="range"
min={minWordLength}
max={maxWordLength}
min={minLength}
max={maxLength}
id="wordLength"
disabled={
gameState === GameState.Playing &&
@@ -287,25 +318,28 @@ function Game(props: GameProps) {
letterInfo={letterInfo}
onKey={onKey}
/>
{gameState !== GameState.Playing && (
<p>
<button
onClick={() => {
share(
getChallengeUrl(target),
"Challenge link copied to clipboard!"
);
}}
>
Challenge a friend to this word
</button>{" "}
<div className="Game-seed-info">
{challenge
? "playing a challenge game"
: seed
? `${describeSeed(seed)} — length ${wordLength}, game ${gameNumber}`
: "playing a random game"}
</div>
<p>
<button
onClick={() => {
share("Link copied to clipboard!");
}}
>
Share a link to this game
</button>{" "}
{gameState !== GameState.Playing && (
<button
onClick={() => {
const emoji = props.colorBlind
? ["⬛", "🟦", "🟧"]
: ["⬛", "🟨", "🟩"];
share(
getChallengeUrl(target),
"Result copied to clipboard!",
guesses
.map((guess) =>
@@ -319,15 +353,8 @@ function Game(props: GameProps) {
>
Share emoji results
</button>
</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}
)}
</p>
</div>
);
}

View File

@@ -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;
}
}