diff --git a/src/lib/dict.ts b/src/lib/dict.ts index cb679c9..23215da 100644 --- a/src/lib/dict.ts +++ b/src/lib/dict.ts @@ -35,6 +35,11 @@ export class Dict { [Symbol.for("Deno.customInspect")]() { return `Dict { ${this.words.size} words, ${this.letters.size} letters }`; } + + clone() { + const words = new Set(this.words.values()); + return new Dict(words, this.length); + } } export function contains_any(text: string, words: string[]) { diff --git a/src/lib/guesser/reducing.ts b/src/lib/guesser/reducing.ts new file mode 100644 index 0000000..7c1ab2b --- /dev/null +++ b/src/lib/guesser/reducing.ts @@ -0,0 +1,77 @@ +import { Dict } from "../dict.ts"; +import { GuessResult, Info } from "../game/game.ts"; +import { Value } from "../utils.ts"; +import { Awaitable, enumerate, range, zip } from "../utils.ts"; +import { Guessing } from "./guesser.ts"; + +export class ReducingGuesser implements Guessing { + length; + words; + possibilities; + + public constructor(words: Dict) { + this.words = words; + this.length = words.length; + this.possibilities = words.clone(); + } + + public async guess(try_: (guess: string, known: string) => Awaitable) { + const guess = this.make_guess(); + const result = await try_(guess, ""); + if (result.kind === "success") return result; + this.learn(guess, result.informations); + return null; + } + + make_guess() { + const letters = [...this.words.letters]; + const letters_ranks = [...range(0, this.length)].map(() => new Map(letters.map((l) => [l, { value: 0 }]))); + for (const word of this.possibilities.words) { + for (const [index, letter] of enumerate(word)) { + letters_ranks[index].get(letter)!.value += 1; + } + } + const candidates = [...this.words.words]; + const scored = candidates.map((word) => [word, score_word_from_ranks(word, letters_ranks)] as const); + const [best] = scored.reduce((a, b) => a[1] > b[1] ? a : b); + return best; + } + + learn(word: string, infos: Info[]) { + const to_delete = new Set(); + const pos = this.possibilities; + for (const [index, [letter, info]] of enumerate(zip(word, infos))) { + if (info.kind === "there") for (const word of pos.words) if (word[index] !== letter) to_delete.add(word); + if (info.kind === "somewhere") for (const word of pos.words) if (word[index] === letter) to_delete.add(word); + if (info.kind === "abscent") for (const word of pos.words) if (word.includes(letter)) to_delete.add(word); + } + for (const d of to_delete) pos.words.delete(d); + } +} + +// note : does not take into account knowledge we already have. +function score_word_from_ranks(word: string, ranks: Map>[]) { + let result = 0; + for (const [index, letter] of enumerate(word)) { + // note : bonus for ANY letter. + for (const index_ranks of ranks) { + const letter_rank = index_ranks.get(letter)!.value; + result += letter_rank; + } + // note : bonus for THIS letter. + const index_ranks = ranks[index]; + const letter_rank = index_ranks.get(letter)!.value; + result += letter_rank; + } + return result; +} + +/* +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. + */ diff --git a/src/lib/utils.ts b/src/lib/utils.ts index be4f837..f1a64c5 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -30,3 +30,5 @@ export function last(iterable: Iterable) { for (const item of iterable) last = item; return last; } + +export type Value = { value: T };