MULTI PUZZLE DRIFTING

This commit is contained in:
ashastral 2022-03-15 22:00:04 -07:00
parent 72b45087b6
commit 0ab57ddd65
9 changed files with 2084 additions and 201 deletions

1053
optimle/puzzles.txt Normal file

File diff suppressed because it is too large Load diff

View file

@ -18,7 +18,7 @@ pub struct Puzzle<const N: usize> {
} }
impl<const N: usize> Puzzle<N> { impl<const N: usize> Puzzle<N> {
pub fn generate(dictionary: &Dictionary<N>, seed: u64) -> Self { pub fn generate(dictionary: &Dictionary<N>, seed: u64, heres: usize) -> Self {
let mut rng: ChaCha8Rng = ChaCha8Rng::seed_from_u64(seed); let mut rng: ChaCha8Rng = ChaCha8Rng::seed_from_u64(seed);
let mut history: Option<GuessHistory<N>> = None; let mut history: Option<GuessHistory<N>> = None;
let mut solutions: usize = 0; let mut solutions: usize = 0;
@ -37,7 +37,7 @@ impl<const N: usize> Puzzle<N> {
game_history.push(result); game_history.push(result);
if game_history.len() > 2 { if game_history.len() > 2 {
let possible_words = game.guesser.get_possible_words(); let possible_words = game.guesser.get_possible_words();
let interesting_solutions = Puzzle::interesting(dictionary, &game_history, possible_words); let interesting_solutions = Puzzle::interesting(dictionary, &game_history, possible_words, heres);
if interesting_solutions.is_some() { if interesting_solutions.is_some() {
history = Some(game_history); history = Some(game_history);
solutions = interesting_solutions.unwrap(); solutions = interesting_solutions.unwrap();
@ -55,7 +55,12 @@ impl<const N: usize> Puzzle<N> {
} }
} }
fn interesting(dictionary: &Dictionary<N>, history: &GuessHistory<N>, possible_words: Dictionary<N>) -> Option<usize> { fn interesting(
dictionary: &Dictionary<N>,
history: &GuessHistory<N>,
possible_words: Dictionary<N>,
wanted_heres: usize,
) -> Option<usize> {
let count = possible_words.len(); let count = possible_words.len();
if count < 3 || count > 6 { if count < 3 || count > 6 {
return None; return None;
@ -65,7 +70,7 @@ impl<const N: usize> Puzzle<N> {
.iter() .iter()
.filter(|&m| *m == LetterMatch::HERE) .filter(|&m| *m == LetterMatch::HERE)
.count(); .count();
if heres < 3 { if heres != wanted_heres {
return None; return None;
} }
@ -94,13 +99,9 @@ impl<const N: usize> Puzzle<N> {
} }
} }
let max_solutions = match heres { const MAX_SOLUTIONS: usize = 100;
4 => 20,
3 => 100,
_ => panic!(),
};
let solution_count = solutions.len(); let solution_count = solutions.len();
let interesting = solution_count > 0 && solution_count <= max_solutions; let interesting = solution_count > 0 && solution_count <= MAX_SOLUTIONS;
if interesting { if interesting {
Some(solution_count) Some(solution_count)

View file

@ -21,8 +21,10 @@ impl PuzzleOutput {
fn main() { fn main() {
let common_words: Vec<Word<5>> = wordlelike::load::load_words(include_str!("../../wordlelike/src/words/common.txt")); let common_words: Vec<Word<5>> = wordlelike::load::load_words(include_str!("../../wordlelike/src/words/common.txt"));
for seed in 11..=365 { for heres in 2..=4 {
let puzzle = Puzzle::generate(&common_words, seed); println!("heres: {}", heres);
for seed in 16..=365 {
let puzzle = Puzzle::generate(&common_words, seed, heres);
println!("{}", serde_json::to_string(&PuzzleOutput { println!("{}", serde_json::to_string(&PuzzleOutput {
history: puzzle.history history: puzzle.history
.iter() .iter()
@ -42,4 +44,5 @@ fn main() {
solutions: puzzle.solutions, solutions: puzzle.solutions,
}).unwrap()); }).unwrap());
} }
}
} }

View file

@ -137,6 +137,10 @@ table.Game-rows > tbody {
outline: none; outline: none;
} }
.puzzle-type {
padding: 0 .25em;
}
.letter-correct { .letter-correct {
border: 2px solid rgba(0, 0, 0, 0.3); border: 2px solid rgba(0, 0, 0, 0.3);
background-color: rgb(87, 172, 120); background-color: rgb(87, 172, 120);
@ -266,6 +270,17 @@ a:active {
margin-inline-end: 8px; margin-inline-end: 8px;
} }
.top-left {
position: absolute;
top: 5px;
left: 5px;
}
.pick-puzzle-type {
display: flex;
flex-direction: row;
}
.top-right { .top-right {
position: absolute; position: absolute;
top: 5px; top: 5px;

View file

@ -1,4 +1,4 @@
import { h, JSX } from 'preact'; import { h, JSX, Fragment } from 'preact';
import { useState, useRef, useMemo, useCallback, useEffect } from 'preact/hooks'; import { useState, useRef, useMemo, useCallback, useEffect } from 'preact/hooks';
import * as wasm from 'web-optimle'; import * as wasm from 'web-optimle';
@ -6,14 +6,13 @@ import { clue, describeClue, type Clue, type GuessResult } from './clue';
import { gameName, speak } from './util'; import { gameName, speak } from './util';
import { COMMON_WORDS, UNCOMMON_WORDS } from './dictionary'; import { COMMON_WORDS, UNCOMMON_WORDS } from './dictionary';
import * as PUZZLES from './puzzles.json';
import { Row, RowState } from './Row'; import { Row, RowState } from './Row';
import { Keyboard } from './Keyboard'; import { Keyboard } from './Keyboard';
import { useLocalStorage } from './localstorage'; import { useLocalStorage } from './localstorage';
import { PuzzleType, PuzzleTypeDisplay } from './PuzzleType';
const MAX_GUESSES = 2; const MAX_GUESSES = 2;
const WORD_LENGTH = 5; const WORD_LENGTH = 5;
const EPOCH = new Date(2022, 1, 27);
enum GameState { enum GameState {
Playing, Playing,
@ -21,65 +20,110 @@ enum GameState {
Lost, Lost,
} }
type History = GuessResult[];
type Puzzle = {
history: History,
seed: number,
solutions: number,
}
interface GameProps { interface GameProps {
puzzleType: PuzzleType;
puzzle: Puzzle;
day: number;
hidden: boolean; hidden: boolean;
colorBlind: boolean; colorBlind: boolean;
keyboardLayout: string; keyboardLayout: string;
} }
type History = GuessResult[];
type DailyStats = { type PuzzleStats = {
tries: number, tries: number,
won: boolean, won: boolean,
winningGuesses?: GuessResult[], guesses?: GuessResult[],
};
function defaultPuzzleStats() {
return { tries: 1, won: false };
}
type DailyStats = {
[key in PuzzleType]: PuzzleStats
}; };
export function Game(props: GameProps) { export function Game(props: GameProps) {
const day = useMemo(() => { const { puzzleType, puzzle, day } = props;
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
return Math.round((date.getTime() - EPOCH.getTime()) / (24 * 60 * 60 * 1000));
}, []);
const puzzle = PUZZLES[day];
const [gameState, setGameState] = useState(GameState.Playing); const [gameState, setGameState] = useState(GameState.Playing);
const [guesses, setGuesses] = useState<GuessResult[]>([...puzzle.history]); const [guesses, setGuesses] = useState<GuessResult[]>([]);
const [currentGuess, setCurrentGuess] = useState<string>(""); const [currentGuess, setCurrentGuess] = useState<string>("");
const [dailyStats, setDailyStats] = useLocalStorage<{ [key: number]: DailyStats }>("dailyTries", {}); const [dailyStats, setDailyStats] = useLocalStorage<{ [key: number]: DailyStats }>("dailyStats", {});
const [todayStats, setTodayStats] = useState<DailyStats>({ tries: 1, won: false }); const [todayStats, setTodayStats] = useState<DailyStats>({
2: defaultPuzzleStats(),
3: defaultPuzzleStats(),
4: defaultPuzzleStats(),
});
const [hint, setHint] = useState<JSX.Element | string>( const [hint, setHint] = useState<JSX.Element | string>(
<p> <p>
Try to guarantee a win in <strong>2 guesses</strong>! Try to guarantee a win in <strong>2 guesses</strong>!
</p> </p>
); );
const tableRef = useRef<HTMLTableElement>(null); const tableRef = useRef<HTMLTableElement>(null);
const game = useMemo(() => wasm.Game.new(COMMON_WORDS, puzzle.history), []); const game = useMemo(
() => wasm.Game.new(COMMON_WORDS, puzzle.history),
[puzzle]
);
const [gameTree, setGameTree] = useState<GuessResult[][]>(); const [gameTree, setGameTree] = useState<GuessResult[][]>();
const reset = useCallback(() => { const reset = useCallback(() => {
game.reset(); game.reset();
setGuesses([...puzzle.history]); setGuesses([...puzzle.history]);
setCurrentGuess(""); setCurrentGuess("");
setGameState(GameState.Playing); setGameState(GameState.Playing);
}, []); setTodayStats((oldStats) => ({
...oldStats,
[puzzleType]: {
...oldStats[puzzleType],
guesses: [],
},
}));
}, [puzzle]);
useEffect(() => {
const selectedPuzzle = todayStats[puzzleType];
setGuesses([...puzzle.history, ...(selectedPuzzle.guesses || [])]);
game.reset();
setGameTree(undefined);
selectedPuzzle.guesses?.forEach(
(result) => game.guess(result.word)
);
if (selectedPuzzle.won) {
setGameState(GameState.Won);
if (selectedPuzzle.tries === 1) {
setHint(<>
You won today's <PuzzleTypeDisplay type={puzzleType} />{' '}
puzzle on your first try! Amazing job.
</>);
} else {
setHint(<>
You won today's <PuzzleTypeDisplay type={puzzleType} />{' '}
puzzle in {selectedPuzzle.tries} tries! Great job.
</>);
}
} else if ((selectedPuzzle.guesses?.length || 0) >= 2) {
setGameState(GameState.Lost);
setHint(<p>Not quite! The answer could've been <strong>{game.possible_word().toUpperCase()}</strong>. (Enter to try again)</p>);
} else {
setGameState(GameState.Playing);
setHint("");
}
}, [puzzle, puzzleType, todayStats]);
useEffect(() => { useEffect(() => {
const storedTodayStats = dailyStats[day]; const storedTodayStats = dailyStats[day];
if (storedTodayStats !== undefined) { if (storedTodayStats !== undefined) {
setTodayStats(storedTodayStats); setTodayStats(storedTodayStats);
if (storedTodayStats.won) {
setGameState(GameState.Won);
setHint("You already won today's puzzle!");
if (storedTodayStats.winningGuesses) {
setGuesses([
...puzzle.history,
...storedTodayStats.winningGuesses,
]);
storedTodayStats.winningGuesses.forEach(
(result) => game.guess(result.word)
);
}
}
} }
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -157,29 +201,37 @@ export function Game(props: GameProps) {
matches: matches, matches: matches,
} }
setGuesses((guesses) => guesses.concat([result])); setGuesses((guesses) => guesses.concat([result]));
setTodayStats((oldStats) => ({
...oldStats,
[puzzleType]: {
...oldStats[puzzleType],
guesses: [
...(oldStats[puzzleType].guesses || []),
result,
],
},
}));
setCurrentGuess(""); setCurrentGuess("");
if (matches.every((m) => m === 2)) { if (matches.every((m) => m === 2)) {
if (todayStats.tries === 1) {
setHint("You won on your first try! Amazing job.");
} else {
setHint(`You won in ${todayStats.tries} tries! Great job.`);
}
setGameState(GameState.Won);
setTodayStats((oldStats) => ({ setTodayStats((oldStats) => ({
...oldStats, ...oldStats,
[puzzleType]: {
...oldStats[puzzleType],
won: true, won: true,
winningGuesses: [ guesses: [
...guesses.slice(puzzle.history.length), ...(oldStats[puzzleType].guesses || []),
result, result,
], ],
})) },
}));
} else if (guesses.length + 1 === puzzle.history.length + MAX_GUESSES) { } 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);
setTodayStats((oldStats) => ({ setTodayStats((oldStats) => ({
...oldStats, ...oldStats,
tries: oldStats.tries + 1, [puzzleType]: {
...oldStats[puzzleType],
tries: oldStats[puzzleType].tries + 1,
},
})); }));
} else { } else {
speak(describeClue(clue(result))); speak(describeClue(clue(result)));
@ -271,10 +323,13 @@ export function Game(props: GameProps) {
const emoji = props.colorBlind const emoji = props.colorBlind
? ["⬛", "🟦", "🟧"] ? ["⬛", "🟦", "🟧"]
: ["⬛", "🟨", "🟩"]; : ["⬛", "🟨", "🟩"];
const score = todayStats.tries === 1 ? "my first try!" : `try #${todayStats.tries}!`; const score = (todayStats[puzzleType].tries === 1
? "my first try!"
: `try #${todayStats[puzzleType].tries}!`
);
share( share(
"Result copied to clipboard!", "Result copied to clipboard!",
`I solved ${gameName} #${puzzle.seed} on ${score}\n` + `I solved ${gameName} #${puzzle.seed} (${puzzleType}🟩) on ${score}\n` +
guesses guesses
.slice(puzzle.history.length) .slice(puzzle.history.length)
.map((result) => .map((result) =>

View file

@ -0,0 +1,9 @@
import { h } from 'preact';
export type PuzzleType = 2 | 3 | 4;
export function PuzzleTypeDisplay(props: { type: PuzzleType, active?: boolean }) {
const { type, active } = props;
const isActive = active === undefined ? true : active;
return <span class={`puzzle-type ${isActive ? 'letter-correct' : ''}`}>{type}</span>;
}

View file

@ -1,15 +1,39 @@
import { h, render, Fragment } from 'preact'; import { h, render, Fragment } from 'preact';
import { useState, useEffect } from 'preact/hooks'; import { useState, useEffect, useMemo } from 'preact/hooks';
import { Game } from './Game'; import { Game } from './Game';
import { gameName, urlParam } from "./util"; import { gameName, urlParam } from "./util";
import "./App.css"; import "./App.css";
import { About } from './About'; import { About } from './About';
import { useLocalStorage } from './localstorage'; import { useLocalStorage } from './localstorage';
import { PuzzleType, PuzzleTypeDisplay } from './PuzzleType';
import * as PUZZLES from './puzzles.json';
const EPOCH = new Date(2022, 1, 27);
type PickPuzzleTypeProps = {
puzzleType: PuzzleType,
setPuzzleType: (p: PuzzleType) => void,
};
function PickPuzzleType(props: PickPuzzleTypeProps) {
const { puzzleType, setPuzzleType } = props;
const PUZZLE_TYPES: PuzzleType[] = [2, 3, 4];
return (
<div class="pick-puzzle-type">
{PUZZLE_TYPES.map((type) => (
<div style={{ cursor: 'pointer' }} onClick={() => setPuzzleType(type)}>
<PuzzleTypeDisplay type={type} active={type === puzzleType} />
</div>
))}
</div>
);
}
export function App() { export function App() {
type Page = "game" | "about" | "settings"; type Page = "game" | "about" | "settings";
const [page, setPage] = useState<Page>("game"); const [page, setPage] = useState<Page>("game");
const prefersDark = const prefersDark =
window.matchMedia && window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches; window.matchMedia("(prefers-color-scheme: dark)").matches;
@ -21,6 +45,15 @@ export function App() {
); );
const [enterLeft, setEnterLeft] = useLocalStorage<boolean>("enter-left", false); const [enterLeft, setEnterLeft] = useLocalStorage<boolean>("enter-left", false);
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
return Math.round((date.getTime() - EPOCH.getTime()) / (24 * 60 * 60 * 1000));
}, []);
const [puzzleType, setPuzzleType] = useLocalStorage<2 | 3 | 4>("puzzleType", 4);
const puzzle = useMemo(() => PUZZLES[puzzleType][day], [puzzleType]);
useEffect(() => { useEffect(() => {
document.body.className = dark ? "dark" : ""; document.body.className = dark ? "dark" : "";
setTimeout(() => { setTimeout(() => {
@ -44,6 +77,11 @@ export function App() {
return ( return (
<div className={"App-container" + (colorBlind ? " color-blind" : "")}> <div className={"App-container" + (colorBlind ? " color-blind" : "")}>
<div className="top-left">
{page === "game" && (
<PickPuzzleType puzzleType={puzzleType} setPuzzleType={setPuzzleType} />
)}
</div>
<h1> <h1>
{gameName} {gameName}
</h1> </h1>
@ -103,7 +141,11 @@ export function App() {
</div> </div>
</div> </div>
)} )}
{puzzle ? (
<Game <Game
puzzleType={puzzleType}
puzzle={puzzle}
day={day}
hidden={page !== "game"} hidden={page !== "game"}
colorBlind={colorBlind} colorBlind={colorBlind}
keyboardLayout={keyboard.replaceAll( keyboardLayout={keyboard.replaceAll(
@ -111,6 +153,13 @@ export function App() {
(x) => (enterLeft ? "EB" : "BE")["BE".indexOf(x)] (x) => (enterLeft ? "EB" : "BE")["BE".indexOf(x)]
)} )}
/> />
) : (
puzzle === null ? (
<p>Hang tight! Something new is in store for us...</p>
) : (
<p>Optimle has concluded. Take a deep breath and relax :)</p>
)
)}
</div> </div>
); );
} }

File diff suppressed because it is too large Load diff

View file

@ -34,4 +34,9 @@ module.exports = {
meta: {viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no'}, meta: {viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no'},
}), }),
], ],
performance: {
hints: false,
maxEntrypointSize: 512000,
maxAssetSize: 512000
},
}; };