MULTI PUZZLE DRIFTING
This commit is contained in:
parent
72b45087b6
commit
0ab57ddd65
9 changed files with 2084 additions and 201 deletions
1053
optimle/puzzles.txt
Normal file
1053
optimle/puzzles.txt
Normal file
File diff suppressed because it is too large
Load diff
|
@ -18,7 +18,7 @@ pub struct Puzzle<const N: usize> {
|
|||
}
|
||||
|
||||
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 history: Option<GuessHistory<N>> = None;
|
||||
let mut solutions: usize = 0;
|
||||
|
@ -37,7 +37,7 @@ impl<const N: usize> Puzzle<N> {
|
|||
game_history.push(result);
|
||||
if game_history.len() > 2 {
|
||||
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() {
|
||||
history = Some(game_history);
|
||||
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();
|
||||
if count < 3 || count > 6 {
|
||||
return None;
|
||||
|
@ -65,7 +70,7 @@ impl<const N: usize> Puzzle<N> {
|
|||
.iter()
|
||||
.filter(|&m| *m == LetterMatch::HERE)
|
||||
.count();
|
||||
if heres < 3 {
|
||||
if heres != wanted_heres {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
@ -94,13 +99,9 @@ impl<const N: usize> Puzzle<N> {
|
|||
}
|
||||
}
|
||||
|
||||
let max_solutions = match heres {
|
||||
4 => 20,
|
||||
3 => 100,
|
||||
_ => panic!(),
|
||||
};
|
||||
const MAX_SOLUTIONS: usize = 100;
|
||||
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 {
|
||||
Some(solution_count)
|
||||
|
|
|
@ -21,8 +21,10 @@ impl PuzzleOutput {
|
|||
|
||||
fn main() {
|
||||
let common_words: Vec<Word<5>> = wordlelike::load::load_words(include_str!("../../wordlelike/src/words/common.txt"));
|
||||
for seed in 11..=365 {
|
||||
let puzzle = Puzzle::generate(&common_words, seed);
|
||||
for heres in 2..=4 {
|
||||
println!("heres: {}", heres);
|
||||
for seed in 16..=365 {
|
||||
let puzzle = Puzzle::generate(&common_words, seed, heres);
|
||||
println!("{}", serde_json::to_string(&PuzzleOutput {
|
||||
history: puzzle.history
|
||||
.iter()
|
||||
|
@ -43,3 +45,4 @@ fn main() {
|
|||
}).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -137,6 +137,10 @@ table.Game-rows > tbody {
|
|||
outline: none;
|
||||
}
|
||||
|
||||
.puzzle-type {
|
||||
padding: 0 .25em;
|
||||
}
|
||||
|
||||
.letter-correct {
|
||||
border: 2px solid rgba(0, 0, 0, 0.3);
|
||||
background-color: rgb(87, 172, 120);
|
||||
|
@ -266,6 +270,17 @@ a:active {
|
|||
margin-inline-end: 8px;
|
||||
}
|
||||
|
||||
.top-left {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
.pick-puzzle-type {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.top-right {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
|
|
|
@ -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 * as wasm from 'web-optimle';
|
||||
|
@ -6,14 +6,13 @@ import { clue, describeClue, type Clue, type GuessResult } from './clue';
|
|||
import { gameName, speak } from './util';
|
||||
|
||||
import { COMMON_WORDS, UNCOMMON_WORDS } from './dictionary';
|
||||
import * as PUZZLES from './puzzles.json';
|
||||
import { Row, RowState } from './Row';
|
||||
import { Keyboard } from './Keyboard';
|
||||
import { useLocalStorage } from './localstorage';
|
||||
import { PuzzleType, PuzzleTypeDisplay } from './PuzzleType';
|
||||
|
||||
const MAX_GUESSES = 2;
|
||||
const WORD_LENGTH = 5;
|
||||
const EPOCH = new Date(2022, 1, 27);
|
||||
|
||||
enum GameState {
|
||||
Playing,
|
||||
|
@ -21,65 +20,110 @@ enum GameState {
|
|||
Lost,
|
||||
}
|
||||
|
||||
type History = GuessResult[];
|
||||
|
||||
|
||||
type Puzzle = {
|
||||
history: History,
|
||||
seed: number,
|
||||
solutions: number,
|
||||
}
|
||||
|
||||
interface GameProps {
|
||||
puzzleType: PuzzleType;
|
||||
puzzle: Puzzle;
|
||||
day: number;
|
||||
hidden: boolean;
|
||||
colorBlind: boolean;
|
||||
keyboardLayout: string;
|
||||
}
|
||||
|
||||
type History = GuessResult[];
|
||||
|
||||
type DailyStats = {
|
||||
type PuzzleStats = {
|
||||
tries: number,
|
||||
won: boolean,
|
||||
winningGuesses?: GuessResult[],
|
||||
guesses?: GuessResult[],
|
||||
};
|
||||
|
||||
function defaultPuzzleStats() {
|
||||
return { tries: 1, won: false };
|
||||
}
|
||||
|
||||
type DailyStats = {
|
||||
[key in PuzzleType]: PuzzleStats
|
||||
};
|
||||
|
||||
export function Game(props: GameProps) {
|
||||
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 puzzle = PUZZLES[day];
|
||||
const { puzzleType, puzzle, day } = props;
|
||||
|
||||
const [gameState, setGameState] = useState(GameState.Playing);
|
||||
const [guesses, setGuesses] = useState<GuessResult[]>([...puzzle.history]);
|
||||
const [guesses, setGuesses] = useState<GuessResult[]>([]);
|
||||
const [currentGuess, setCurrentGuess] = useState<string>("");
|
||||
const [dailyStats, setDailyStats] = useLocalStorage<{ [key: number]: DailyStats }>("dailyTries", {});
|
||||
const [todayStats, setTodayStats] = useState<DailyStats>({ tries: 1, won: false });
|
||||
const [dailyStats, setDailyStats] = useLocalStorage<{ [key: number]: DailyStats }>("dailyStats", {});
|
||||
const [todayStats, setTodayStats] = useState<DailyStats>({
|
||||
2: defaultPuzzleStats(),
|
||||
3: defaultPuzzleStats(),
|
||||
4: defaultPuzzleStats(),
|
||||
});
|
||||
const [hint, setHint] = useState<JSX.Element | string>(
|
||||
<p>
|
||||
Try to guarantee a win in <strong>2 guesses</strong>!
|
||||
</p>
|
||||
);
|
||||
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 reset = useCallback(() => {
|
||||
game.reset();
|
||||
setGuesses([...puzzle.history]);
|
||||
setCurrentGuess("");
|
||||
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(() => {
|
||||
const storedTodayStats = dailyStats[day];
|
||||
if (storedTodayStats !== undefined) {
|
||||
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(() => {
|
||||
|
@ -157,29 +201,37 @@ export function Game(props: GameProps) {
|
|||
matches: matches,
|
||||
}
|
||||
setGuesses((guesses) => guesses.concat([result]));
|
||||
setTodayStats((oldStats) => ({
|
||||
...oldStats,
|
||||
[puzzleType]: {
|
||||
...oldStats[puzzleType],
|
||||
guesses: [
|
||||
...(oldStats[puzzleType].guesses || []),
|
||||
result,
|
||||
],
|
||||
},
|
||||
}));
|
||||
setCurrentGuess("");
|
||||
|
||||
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) => ({
|
||||
...oldStats,
|
||||
[puzzleType]: {
|
||||
...oldStats[puzzleType],
|
||||
won: true,
|
||||
winningGuesses: [
|
||||
...guesses.slice(puzzle.history.length),
|
||||
guesses: [
|
||||
...(oldStats[puzzleType].guesses || []),
|
||||
result,
|
||||
],
|
||||
}))
|
||||
},
|
||||
}));
|
||||
} 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) => ({
|
||||
...oldStats,
|
||||
tries: oldStats.tries + 1,
|
||||
[puzzleType]: {
|
||||
...oldStats[puzzleType],
|
||||
tries: oldStats[puzzleType].tries + 1,
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
speak(describeClue(clue(result)));
|
||||
|
@ -271,10 +323,13 @@ export function Game(props: GameProps) {
|
|||
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(
|
||||
"Result copied to clipboard!",
|
||||
`I solved ${gameName} #${puzzle.seed} on ${score}\n` +
|
||||
`I solved ${gameName} #${puzzle.seed} (${puzzleType}🟩) on ${score}\n` +
|
||||
guesses
|
||||
.slice(puzzle.history.length)
|
||||
.map((result) =>
|
||||
|
|
9
web-optimle/www/src/PuzzleType.tsx
Normal file
9
web-optimle/www/src/PuzzleType.tsx
Normal 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>;
|
||||
}
|
|
@ -1,15 +1,39 @@
|
|||
import { h, render, Fragment } from 'preact';
|
||||
import { useState, useEffect } from 'preact/hooks';
|
||||
import { useState, useEffect, useMemo } from 'preact/hooks';
|
||||
import { Game } from './Game';
|
||||
import { gameName, urlParam } from "./util";
|
||||
import "./App.css";
|
||||
import { About } from './About';
|
||||
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() {
|
||||
type Page = "game" | "about" | "settings";
|
||||
const [page, setPage] = useState<Page>("game");
|
||||
|
||||
const prefersDark =
|
||||
window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
|
@ -21,6 +45,15 @@ export function App() {
|
|||
);
|
||||
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(() => {
|
||||
document.body.className = dark ? "dark" : "";
|
||||
setTimeout(() => {
|
||||
|
@ -44,6 +77,11 @@ export function App() {
|
|||
|
||||
return (
|
||||
<div className={"App-container" + (colorBlind ? " color-blind" : "")}>
|
||||
<div className="top-left">
|
||||
{page === "game" && (
|
||||
<PickPuzzleType puzzleType={puzzleType} setPuzzleType={setPuzzleType} />
|
||||
)}
|
||||
</div>
|
||||
<h1>
|
||||
{gameName}
|
||||
</h1>
|
||||
|
@ -103,7 +141,11 @@ export function App() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{puzzle ? (
|
||||
<Game
|
||||
puzzleType={puzzleType}
|
||||
puzzle={puzzle}
|
||||
day={day}
|
||||
hidden={page !== "game"}
|
||||
colorBlind={colorBlind}
|
||||
keyboardLayout={keyboard.replaceAll(
|
||||
|
@ -111,6 +153,13 @@ export function App() {
|
|||
(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>
|
||||
);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -34,4 +34,9 @@ module.exports = {
|
|||
meta: {viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no'},
|
||||
}),
|
||||
],
|
||||
performance: {
|
||||
hints: false,
|
||||
maxEntrypointSize: 512000,
|
||||
maxAssetSize: 512000
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue