import { assertExists } from "https://deno.land/std@0.224.0/assert/assert_exists.ts"; import { assertEquals } from "https://deno.land/std@0.224.0/assert/assert_equals.ts"; import { Dict } from "../dict.ts"; import { GuessResult, Info } from "../game/game.ts"; import { info_of_guess } from "../game/simulator.ts"; import { Awaitable, enumerate, first, range, zip } from "../utils.ts"; import { Guessing } from "./guesser.ts"; export class ReducingGuesser implements Guessing { length; words; candidates; public constructor(words: Dict) { this.words = words.words; this.length = words.length; this.candidates = new Set(this.words.values()); } public declare_properties() { return ["candidates", "best score"]; } public async guess(try_: (guess: string, candidates: number, score: number) => Awaitable) { if (this.candidates.size === 1) return await try_(first(this.candidates)!, this.candidates.size, 1); const [guess, score] = get_word_with_smallest_cuts(this.candidates, this.words); const result = await try_(guess, this.candidates.size, score); if (result.kind === "success") return result; this.learn(guess, result.informations); return null; } learn(word: string, infos: Info[]) { let next_cnd = new Map([...this.candidates.values()].map((value) => [value, value])); // TODO : should treat all there before somewheres for (const [i, [info, c]] of enumerate(zip(infos, word))) { if (info.kind === "abscent") next_cnd = filter_map(next_cnd, (_, r) => !r.includes(c)); if (info.kind === "somewhere") next_cnd = filter_map(next_cnd, (_, r) => r.includes(c) ? consume(r, c) : null); if (info.kind === "there") next_cnd = filter_map(next_cnd, (_, r) => r[i] === c ? consume(r, c) : null); } this.candidates = new Set(next_cnd.keys()); } restraint_candidates(constraints: [string, Info][]) { const new_cand = new Set(); for (const cand of this.candidates.values()) if (matches_constraints(cand, constraints)) new_cand.add(cand); return new_cand; } } function filter_map( map: Map, predicate: (k: K, v: T) => boolean | null | T, ) { const new_set = new Map(); for (const [key, value] of map.entries()) { const result = predicate(key, value); if (result === null) continue; if (result === false) continue; if (result === true) new_set.set(key, value); else new_set.set(key, result); } return new_set; } function consume(word: string, letter: string, at: number | null = null, replace = ".") { if (at !== null) { if (word[at] !== letter) return word; else return word.slice(0, at) + replace + word.slice(at + 1); } return word.replace(letter, replace); } /* note : The algorithm must proceed as follow : 1. establish a list of possible words 2. loop : 2.1 for each word, for each letter, estimate by how much this letter cuts possibilities for all three possible outcomes. (2.1-bis weigts those scores by the frequency of the word ?) 2.2 play the word which individual letters cuts most of the possible space. */ function word_cuts(word: string, dict: Set) { const result = [] as [Set, Set, Set][]; const valued = [...dict.values()].map((value) => ({ value, rest: value })); // note : first pass to detect corrects. for (const [index, letter] of enumerate(word)) { const [theres, somewheres, abscents] = [new Set(), new Set(), new Set()]; for (const option of valued) { if (option.rest[index] === letter) { theres.add(option.value); option.rest = consume(option.value, letter, index); } result.push([theres, somewheres, abscents]); } } // note : second pass to detect rest. for (const letter of word) { for (const [option, [theres, somewheres, abscents]] of zip(valued, result)) { if (option.rest.includes(letter)) { somewheres.add(option.value); option.rest = consume(option.rest, letter); } else if (!theres.has(option.value)) abscents.add(option.value); } } return result; } function word_cuts_alt(word: string, dict: Set) { const results = [...range(0, word.length)] .map(() => [new Set(), new Set(), new Set()] as const); for (const option of dict) { const infos = info_of_guess(word, option); for (const [index, info] of enumerate(infos)) { if (info.kind === "there") results[index][0].add(option); if (info.kind === "somewhere") results[index][1].add(option); if (info.kind === "abscent") results[index][2].add(option); } } return results; } function get_word_with_smallest_cuts(candidates: Set, dict: Set) { let best = null as null | [string, number]; for (const candidate of dict.values()) { const cuts = word_cuts_alt(candidate, candidates); let max_part_size = 0; for (const cut of cuts) for (const part of cut) if (part.size > max_part_size) max_part_size = part.size; if (best === null) best = [candidate, max_part_size]; else if (max_part_size < best[1]) best = [candidate, max_part_size]; } assertExists(best); return best; } Deno.test("test_word_cuts", () => { { const cuts = word_cuts("a", new Set(["a", "b"])); assertEquals(cuts, [[new Set(["a"]), new Set([]), new Set(["b"])]]); } { const cuts = word_cuts("aa", new Set(["aa", "ab", "ba", "bb"])); assertEquals(cuts, [ [new Set(["aa", "ab"]), new Set(["ba"]), new Set(["bb"])], [new Set(["aa", "ba"]), new Set(["ab"]), new Set(["bb"])], ]); } }); Deno.test("test_smallest_cuts", () => { const [best] = get_word_with_smallest_cuts( new Set(["aa", "ab", "ac", "ba", "bc"]), new Set(["ab", "ba"]), ); assertEquals(best, "ba"); }); function matches_constraints(candidate: string, constraints: [string, Info][]) { const letters = [...candidate]; for (const [index, [letter, info]] of enumerate(constraints)) { if (info.kind !== "there") continue; if (letters[index] !== letter) return false; letters[index] = "."; } for (const [index, [letter, info]] of enumerate(constraints)) { if (info.kind === "there") continue; if (info.kind === "abscent") if (letters.includes(letter)) return false; // is somewhere if (letters[index] === letter) return false; if (!letters.includes(letter)) return false; const index_ = letters.findIndex((i) => i === letter); letters[index_] = "."; } return true; }