From de66dec9395e5718420a07b0da81060a55d16cde Mon Sep 17 00:00:00 2001 From: JOLIMAITRE Matthieu Date: Tue, 30 Apr 2024 15:21:25 +0200 Subject: [PATCH] working state --- src/lib/lib.ts | 226 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.ts | 38 +++++++++ 2 files changed, 264 insertions(+) create mode 100644 src/lib/lib.ts create mode 100755 src/main.ts diff --git a/src/lib/lib.ts b/src/lib/lib.ts new file mode 100644 index 0000000..2b7b6f8 --- /dev/null +++ b/src/lib/lib.ts @@ -0,0 +1,226 @@ +import { assertExists } from "https://deno.land/std@0.224.0/assert/assert_exists.ts"; + +export type GuessResult = { kind: "success" } | { kind: "failure"; informations: Info[] }; + +export class Dict { + words; + letters; + length; + + constructor(words: Set, length: number) { + this.words = words; + this.length = length; + this.letters = new Set([...words.values()].map((w) => w.split("")).flat()); + } + + static async from_file(path: string, length: number) { + const words = new Set(); + const content = await Deno.readTextFile(path); + for (const word of content.split("\n")) { + const word_ = word.trim().toLowerCase(); + if (word_.length !== length) continue; + for (const forbidden of [" ", "-", "."]) if (word_.includes(forbidden)) continue; + words.add(remove_accent(word_)); + } + return new Dict(words, length); + } + + [Symbol.for("Deno.customInspect")]() { + return `Dict { ${this.words.size} words }`; + } +} + +type Knowledge = { + letter: string; + at: Set; + exists: "unknown" | boolean; + not_at: Set; +}; + +export class Gueser { + 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" }); + } + } + + async next_guess(operation: (guess: string, known: string) => GuessResult | Promise) { + const res = this.expected_result(); + if (res.completed) return operation(res.result, 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 operation(guess, res.result); + if (result.kind === "success") return guess; + 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); + } + } + } + + learn_does_not_exist(letter: string) { + const letter_info = this.informations.get(letter); + assertExists(letter_info); + letter_info.exists = false; + } + + 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; + } + + learn_letter_at(letter: string, at: number) { + 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); + } + } + + 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; + if (information.exists === true) total += this.length * this.length; + } + 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 }; + } +} + +type Info = { kind: "abscent" } | { kind: "somewhere" } | { kind: "there" }; + +function remove_accent(text: string) { + const accents = [ + ["à", "a"], + ["â", "a"], + ["ä", "a"], + ["ç", "c"], + ["é", "e"], + ["è", "e"], + ["ê", "e"], + ["ë", "e"], + ["î", "i"], + ["ï", "i"], + ["ô", "o"], + ["ö", "o"], + ["û", "u"], + ]; + let result = text; + for (const [accent, alternative] of accents) result = result.replaceAll(accent, alternative); + return result; +} + +function* enumerate(iterator: Iterable) { + let index = 0; + for (const item of iterator) yield [index++, item] as const; +} + +function* zip(iterable_a: Iterable, iterable_b: Iterable) { + const iter_a = iterable_a[Symbol.iterator](); + const iter_b = iterable_b[Symbol.iterator](); + while (true) { + const next_a = iter_a.next().value; + const next_b = iter_b.next().value; + if (next_a === undefined) return; + if (next_b === undefined) return; + yield [next_a as A, next_b as B] as const; + } +} + +export class Game { + word; + + constructor(word: string) { + this.word = word; + } + + static from_dict_rand(dict: Dict) { + const index = Math.floor(Math.random() * dict.words.size); + const word = [...dict.words.values()][index]; + return new Game(word); + } + + try_guess(guess: 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); + return { kind: "failure", informations }; + } +} + +function* range(from: number, to: number) { + while (from < to) yield from++; +} + +export async function wait(ms: number) { + await new Promise((resolver) => setTimeout(resolver, ms)); +} + +function pick_random_common_word(bests: string[]) { + const index = Math.floor(Math.random() * bests.length); + return bests[index]; +} diff --git a/src/main.ts b/src/main.ts new file mode 100755 index 0000000..2784005 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,38 @@ +#!/usr/bin/env -S deno run --allow-read + +import { Dict, Game, Gueser, GuessResult, wait } from "./lib/lib.ts"; + +async function main() { + const length = 6; + const dict = await Dict.from_file("./data/liste_francais.txt", length); + console.log(dict); + const guesser = new Gueser(dict); + const game = Game.from_dict_rand(dict); + console.log("Target is", game.word); + let index = 0; + while (true) { + console.log(); + index += 1; + const found = await guesser.next_guess((guess, known) => { + console.log("knows: ", known); + console.log("Guessing:", guess); + const result = game.try_guess(guess); + console.log(format_result(result)); + return result; + }); + if (found !== undefined) { + console.log("Found", found, "in", index); + break; + } + await wait(500); + } +} + +function format_result(result: GuessResult) { + if (result.kind === "success") return `success`; + let line = "failure: "; + for (const info of result.informations) line += info.kind === "abscent" ? "-" : info.kind === "somewhere" ? "+" : "#"; + return line; +} + +if (import.meta.main) await main();