From 336efff5c1b39092bcca9dc6be6162fe3b811f7f Mon Sep 17 00:00:00 2001 From: JOLIMAITRE Matthieu Date: Thu, 2 May 2024 15:45:54 +0200 Subject: [PATCH] add reducing implementation --- src/lib/dict.ts | 12 +++ src/lib/game/simulator.ts | 50 ++++++------ src/lib/guesser/reducing.ts | 158 ++++++++++++++++++++++++++---------- src/lib/lib.ts | 1 + src/lib/utils.ts | 9 ++ 5 files changed, 166 insertions(+), 64 deletions(-) diff --git a/src/lib/dict.ts b/src/lib/dict.ts index 23215da..08eb75b 100644 --- a/src/lib/dict.ts +++ b/src/lib/dict.ts @@ -1,3 +1,5 @@ +import { range } from "./utils.ts"; + export class Dict { words; letters; @@ -40,6 +42,16 @@ export class Dict { const words = new Set(this.words.values()); return new Dict(words, this.length); } + + sample(quantity: number) { + const words = new Set(); + const this_words = [...this.words.values()]; + while (words.size < quantity) { + const index = Math.floor(Math.random() * this_words.length); + words.add(this_words[index]); + } + return new Dict(words, this.length); + } } export function contains_any(text: string, words: string[]) { diff --git a/src/lib/game/simulator.ts b/src/lib/game/simulator.ts index 7b4d247..045abbc 100644 --- a/src/lib/game/simulator.ts +++ b/src/lib/game/simulator.ts @@ -15,29 +15,9 @@ export class Simulator implements Gaming { return new Simulator(word); } - guess(guess_: string, _known: string): GuessResult { - if (guess_ === this.word) return { kind: "success" }; - const rest_actual = [...this.word].map((letter) => letter as (string | null)); - const rest_guess = [...guess_].map((letter) => letter as (string | null)); - const info = [...range(0, this.word.length)].map(() => null) as (Info | null)[]; - - for (const [index, [guessed, actual]] of enumerate(zip(rest_guess, rest_actual))) { - if (guessed !== actual) continue; - rest_actual[index] = null; - rest_guess[index] = null; - info[index] = { kind: "there" }; - } - - for (const [index, guessed] of enumerate(rest_guess)) { - if (guessed === null) continue; - if (!rest_actual.includes(guessed)) continue; - info[index] = { kind: "somewhere" }; - rest_guess[index] = null; - // note : removes only one. - rest_actual[rest_actual.indexOf(guessed)] = null; - } - - const informations = info.map((i) => i != undefined ? i : ({ kind: "abscent" }) as Info); + guess(guess: string, _known: string): GuessResult { + if (guess === this.word) return { kind: "success" }; + const informations = info_of_guess(guess, this.word); return { kind: "failure", informations }; } @@ -45,3 +25,27 @@ export class Simulator implements Gaming { return this.word.length; } } + +export function info_of_guess(guess: string, actual: string) { + const rest_actual = [...actual].map((letter) => letter as (string | null)); + const rest_guess = [...guess].map((letter) => letter as (string | null)); + const info = [...range(0, actual.length)].map(() => null) as (Info | null)[]; + + for (const [index, [guessed, actual]] of enumerate(zip(rest_guess, rest_actual))) { + if (guessed !== actual) continue; + rest_actual[index] = null; + rest_guess[index] = null; + info[index] = { kind: "there" }; + } + + for (const [index, guessed] of enumerate(rest_guess)) { + if (guessed === null) continue; + if (!rest_actual.includes(guessed)) continue; + info[index] = { kind: "somewhere" }; + rest_guess[index] = null; + // note : removes only one. + rest_actual[rest_actual.indexOf(guessed)] = null; + } + + return info.map((i) => i != undefined ? i : ({ kind: "abscent" }) as Info); +} diff --git a/src/lib/guesser/reducing.ts b/src/lib/guesser/reducing.ts index 7c1ab2b..2785352 100644 --- a/src/lib/guesser/reducing.ts +++ b/src/lib/guesser/reducing.ts @@ -1,69 +1,65 @@ +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 { Value } from "../utils.ts"; -import { Awaitable, enumerate, range, zip } from "../utils.ts"; +import { info_of_guess } from "../game/simulator.ts"; +import { Awaitable, enumerate, first, zip } from "../utils.ts"; import { Guessing } from "./guesser.ts"; +import { range } from "../utils.ts"; export class ReducingGuesser implements Guessing { length; words; - possibilities; + candidates; public constructor(words: Dict) { - this.words = words; + this.words = words.words; this.length = words.length; - this.possibilities = words.clone(); + this.candidates = new Set(this.words.values()); } public async guess(try_: (guess: string, known: string) => Awaitable) { - const guess = this.make_guess(); + if (this.candidates.size === 1) return await try_(first(this.candidates)!, ""); + const guess = get_word_with_smallest_cuts(this.candidates, this.words); 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); + 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); } - for (const d of to_delete) pos.words.delete(d); + this.candidates = new Set(next_cnd.keys()); } } -// 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; +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 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); } /* @@ -75,3 +71,83 @@ note : The algorithm must proceed as follow : (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) { + const total = dict.size; + let best = null as null | [string, number]; + for (const [index, candidate] of enumerate(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]; + // const frac = Math.floor(100 * index / total); + // console.log({ index, total, frac, candidate }); + } + assertExists(best); + // console.log("Got", best); + + return best[0]; +} + +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"); +}); diff --git a/src/lib/lib.ts b/src/lib/lib.ts index cc2c7a7..fd3428c 100644 --- a/src/lib/lib.ts +++ b/src/lib/lib.ts @@ -1,5 +1,6 @@ export { Dict } from "./dict.ts"; export { BaseGuesser } from "./guesser/base.ts"; +export { ReducingGuesser } from "./guesser/reducing.ts"; export { Simulator } from "./game/simulator.ts"; export { ManualProxy } from "./game/proxy.ts"; export { Runner } from "./runner.ts"; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index f1a64c5..4903927 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -31,4 +31,13 @@ export function last(iterable: Iterable) { return last; } +export function first(iterable: Iterable) { + for (const item of iterable) return item; +} + +export function dbg(value: T, ...rest: unknown[]) { + console.log("[DBG]", value, ...rest); + return value; +} + export type Value = { value: T };