Seeded game links, allow share before end (fix #64)
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
103
src/Game.tsx
103
src/Game.tsx
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/util.ts
25
src/util.ts
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user