import { assertExists } from "https://deno.land/std@0.224.0/assert/assert_exists.ts"; import { Awaitable, enumerate, range, zip } from "../utils.ts"; import { Dict } from "../dict.ts"; import { GuessResult } from "../game/game.ts"; import { Guessing } from "./guesser.ts"; type Knowledge = { letter: string; at: Set; exists: "unknown" | boolean; not_at: Set; }; export class ExplorerGuesser implements Guessing { length; dict; informations; constructor(dict: Dict) { this.length = dict.length; this.dict = dict; this.informations = new Map(); for (const letter of dict.letters.values()) { this.informations.set(letter, { letter, at: new Set(), not_at: new Set(), exists: "unknown" }); } } declare_properties(): string[] { return ["known".padStart(this.length)]; } async guess(try_: (guess: string, known: string) => Awaitable) { const res = this.expected_result(); if (res.completed) return await try_(res.result, res.result); const possibilities = [...this.possible_words()]; if (possibilities.length === 1) return await try_(possibilities[0], res.result); const words = [...this.dict.words.values()]; const scored = words.map((word) => [this.info_score_for(word), word] as const); const best_score = scored.reduce((acc, [next]) => Math.max(acc, next), 0); const bests = scored.filter(([s]) => s === best_score).map(([_, word]) => word); const guess = pick_random_common_word(bests); const result = await try_(guess, res.result); if (result.kind === "success") return result; for (const [index, info] of enumerate(result.informations)) { const letter = guess[index]; if (info.kind === "there") this.learn_letter_at(letter, index); if (info.kind === "abscent") this.learn_does_not_exist(letter); if (info.kind === "somewhere") { this.learn_does_exist(letter); this.learn_letter_not_at(letter, index); } } return null; } public learn_does_not_exist(letter: string) { const letter_info = this.informations.get(letter); assertExists(letter_info); letter_info.exists = false; } public learn_does_exist(letter: string) { const letter_info = this.informations.get(letter); assertExists(letter_info); letter_info.exists = true; const total_known = [...this.informations.values()].filter((i) => i.exists === true).length; if (total_known < this.length) return; for (const info of this.informations.values()) if (info.exists === "unknown") info.exists = false; } public learn_letter_at(letter: string, at: number) { this.learn_does_exist(letter); const letter_info = this.informations.get(letter); assertExists(letter_info); letter_info.at.add(at); for (const l of this.informations.keys()) { if (letter === l) continue; this.learn_letter_not_at(l, at); } } public learn_letter_not_at(letter: string, at: number) { const letter_info = this.informations.get(letter); assertExists(letter_info); letter_info.not_at.add(at); } info_score_for(word: string) { let total = 0; for (const [index, letter] of enumerate(word)) { const information = this.informations.get(letter); assertExists(information); if (information.exists === false) continue; if (information.at.has(index)) continue; if (information.not_at.has(index)) continue; if (information.exists === "unknown") total += this.length ** 1; if (information.exists === true) total += this.length ** 2; } const different_letters = new Set([...word]); total += different_letters.size; return total; } expected_result() { const known_arr = [...range(0, this.length)].map(() => null as string | null); for (const [letter, info] of this.informations.entries()) { for (const pos of info.at.values()) known_arr[pos] = letter; } const result = known_arr.map((l) => l === null ? "." : l).join(""); const completed = !result.includes("."); return { completed, result }; } *possible_words() { for (const word of this.dict.words.values()) if (this.matches_expected(word)) yield word; } matches_expected(word: string) { const expected = this.expected_result(); for (const [act, exp] of zip(word, expected.result)) { if (exp === ".") continue; if (act !== exp) return false; } return true; } } function pick_random_common_word(bests: string[]) { const index = Math.floor(Math.random() * bests.length); return bests[index]; }