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> {
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)

View file

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

View file

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

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 * 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) =>

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 { 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

View file

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