#!/usr/bin/env -S deno run --allow-read import { assertExists } from "https://deno.land/std@0.224.0/assert/assert_exists.ts"; async function main() { const length = 6; const dict = await Dict.from_file("./data/liste_francais.txt", length); console.log(dict); const history = new History(dict); const game = Game.from_dict_rand(dict); console.log("Target is", game.word); let index = 0; while (true) { index += 1; const found = await history.next_guess((guess) => { 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); } } type GuessResult = { kind: "success" } | { kind: "failure"; informations: Info[] }; 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; } 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; not_at: Set; }; class History { 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() }); } } async next_guess(operation: (guess: string) => GuessResult | Promise) { const res = this.expected_result(); console.log("knows: ", res.known); if (res.completed) return operation(res.known); const words = [...this.dict.words.values()]; const scored = words.map((word) => [this.info_for(word), word] as const); const sorted = scored.toSorted(([sa], [sb]) => sb - sa); const [_score, guess] = sorted[0]; const result = await operation(guess); if (result.kind === "success") return guess; for (const [letter, new_info] of zip(guess, result.informations)) { // } } 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_for(word: string) { let total = 0; for (const [index, letter] of enumerate(word)) { const information = this.informations.get(letter); if (information === undefined) { total += 1; continue; } if () if (information.kind === "abscent") { continue; } if (information.kind === "present") { const known_pos = information.not_at.has(index); if (!known_pos) total += this.length; // note : not 1 to priorize locating known letters. continue; } } return total; } expected_result() { const known_arr = [...range(0, this.length)].map(() => null as string | null); for (const [letter, info] of this.informations.entries()) { if (info.kind !== "known") continue; for (const i of info.at.values()) known_arr[i] = letter; } const known = known_arr.map((l) => l === null ? "." : l).join(""); if (known.includes(".")) return { completed: false as const, known }; const guess = known_arr as string[]; return { completed: true as const, known, guess }; } } type Info = { kind: "abscent" } | { kind: "somewhere" } | { kind: "there" }; function remove_accent(text: string) { const accents = [ ["à", "a"], ["â", "a"], ["ä", "a"], ["é", "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; } } 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: "known", at: new Set([index]) }; } for (const [index, guessed] of enumerate(rest_guess)) { if (guessed === null) continue; if (!rest_actual.includes(guessed)) continue; info[index] = { kind: "present", not_at: new Set([index]) }; 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++; } async function wait(ms: number) { await new Promise((resolver) => setTimeout(resolver, ms)); } if (import.meta.main) await main();