From bc211621e5260f567d7dcf7b9c67cbaca0365bf8 Mon Sep 17 00:00:00 2001 From: JOLIMAITRE Matthieu Date: Fri, 3 May 2024 06:12:12 +0200 Subject: [PATCH] fixes and cleanup --- src/lib/game/simulator.ts | 99 ++++++++++++++++++++---- src/lib/guesser/explorer.ts | 5 -- src/lib/guesser/guesser.ts | 8 ++ src/lib/guesser/reducing.ts | 150 ++++++++++++++++++++++-------------- src/lib/runner.ts | 10 ++- src/simulation.ts | 10 ++- 6 files changed, 199 insertions(+), 83 deletions(-) diff --git a/src/lib/game/simulator.ts b/src/lib/game/simulator.ts index 8fc8028..12be24a 100644 --- a/src/lib/game/simulator.ts +++ b/src/lib/game/simulator.ts @@ -1,6 +1,10 @@ +import { assertNotEquals } from "https://deno.land/std@0.224.0/assert/assert_not_equals.ts"; + import { Dict } from "../dict.ts"; -import { enumerate, range, zip } from "../utils.ts"; +import { enumerate } from "../utils.ts"; import { Gaming, GuessResult, Info } from "./game.ts"; +import { assertEquals } from "https://deno.land/std@0.224.0/assert/assert_equals.ts"; +import { assertExists } from "https://deno.land/std@0.224.0/assert/assert_exists.ts"; export class Simulator implements Gaming { word; @@ -27,25 +31,86 @@ export class Simulator implements Gaming { } 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)[]; + const actual_letters = [...actual]; + const infos = [...guess].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" }; + const used_letter_indices = new Set(); + for (const [index, letter] of enumerate(guess)) { + if (actual_letters[index] !== letter) continue; + infos[index] = { kind: "there" }; + actual_letters[index] = "."; + used_letter_indices.add(index); } - 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; + for (const [index, letter] of enumerate(guess)) { + assertNotEquals(letter, "."); + if (used_letter_indices.has(index)) continue; + if (!actual_letters.includes(letter)) { + infos[index] = { kind: "abscent" }; + continue; + } + const actual_index = actual_letters.findIndex((i) => i === letter); + assertNotEquals(actual_index, -1); + infos[index] = { kind: "somewhere" }; + actual_letters[actual_index] = "."; } - return info.map((i) => i != undefined ? i : ({ kind: "abscent" }) as Info); + const infos_ = infos.map((i) => i === null ? ({ kind: "abscent" }) : i); + for (const info of infos_) assertExists(info); + return infos_ as Info[]; } + +Deno.test("test_info_of_guess_peigne", () => { + const guess_ = "soiree"; + const actual = "peigne"; + const constraints = [ + { kind: "abscent" }, + { kind: "abscent" }, + { kind: "there" }, + { kind: "abscent" }, + { kind: "somewhere" }, + { kind: "there" }, + ] as Info[]; + assertEquals(info_of_guess(guess_, actual), constraints); +}); + +Deno.test("test_info_of_guess_genant", () => { + const guess_ = "genant"; + const actual = "peigne"; + const constraints = [ + { kind: "somewhere" }, + { kind: "there" }, + { kind: "abscent" }, + { kind: "abscent" }, + { kind: "there" }, + { kind: "abscent" }, + ] as Info[]; + assertEquals(info_of_guess(guess_, actual), constraints); +}); + +Deno.test("test_info_of_guess_arbre", () => { + const guess_ = "resta"; + const actual = "arbre"; + const constraints = [ + { kind: "somewhere" }, + { kind: "somewhere" }, // <- "abscent" ; ptn ... gerté la lettre dans la mauvaise liste + { kind: "abscent" }, + { kind: "abscent" }, + { kind: "somewhere" }, + ] as Info[]; + assertEquals(info_of_guess(guess_, actual), constraints); +}); + +Deno.test("test_info_of_guess_poncif", () => { + const guess_ = "pacino"; + const actual = "poncif"; + const constraints = [ + { kind: "there" }, + { kind: "abscent" }, + { kind: "somewhere" }, + { kind: "somewhere" }, + { kind: "somewhere" }, + { kind: "somewhere" }, + ] as Info[]; + assertEquals(info_of_guess(guess_, actual), constraints); +}); diff --git a/src/lib/guesser/explorer.ts b/src/lib/guesser/explorer.ts index 72e2ee0..73dd703 100644 --- a/src/lib/guesser/explorer.ts +++ b/src/lib/guesser/explorer.ts @@ -129,8 +129,3 @@ export class ExplorerGuesser implements Guessing { return true; } } - -function pick_random_common_word(bests: string[]) { - const index = Math.floor(Math.random() * bests.length); - return bests[index]; -} diff --git a/src/lib/guesser/guesser.ts b/src/lib/guesser/guesser.ts index a0f202c..9090598 100644 --- a/src/lib/guesser/guesser.ts +++ b/src/lib/guesser/guesser.ts @@ -1,3 +1,5 @@ +import { assertNotEquals } from "https://deno.land/std@0.224.0/assert/assert_not_equals.ts"; + import { GuessResult } from "../game/game.ts"; import { Awaitable } from "../utils.ts"; @@ -5,3 +7,9 @@ export interface Guessing { declare_properties(): string[]; guess(try_: (guess: string, ...properties: unknown[]) => Awaitable): Promise; } + +export function pick_random_common_word(bests: string[]) { + assertNotEquals(bests.length, 0); + const index = Math.floor(Math.random() * bests.length); + return bests[index]; +} diff --git a/src/lib/guesser/reducing.ts b/src/lib/guesser/reducing.ts index bb88f78..2f34558 100644 --- a/src/lib/guesser/reducing.ts +++ b/src/lib/guesser/reducing.ts @@ -4,8 +4,8 @@ import { assertEquals } from "https://deno.land/std@0.224.0/assert/assert_equals 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"; +import { Awaitable, dbg, enumerate, first, range, zip } from "../utils.ts"; +import { Guessing, pick_random_common_word } from "./guesser.ts"; export class ReducingGuesser implements Guessing { length; @@ -25,6 +25,13 @@ export class ReducingGuesser implements Guessing { 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); + if (score >= this.candidates.size) { + const pick = pick_random_common_word([...this.candidates.values()]); + assertExists(pick); + const result = await try_(pick, this.candidates.size, score); + if (result.kind === "success") return result; + else return null; + } const result = await try_(guess, this.candidates.size, score); if (result.kind === "success") return result; this.learn(guess, result.informations); @@ -39,7 +46,9 @@ export class ReducingGuesser implements Guessing { 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()); + const new_candidates = new Set(next_cnd.keys()); + if (new_candidates.size === this.candidates.size) console.log({ new_candidates }); + this.candidates = new_candidates; } restraint_candidates(constraints: [string, Info][]) { @@ -83,32 +92,6 @@ note : The algorithm must proceed as follow : */ 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) { @@ -125,7 +108,7 @@ function word_cuts_alt(word: string, dict: Set) { 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); + const cuts = word_cuts(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]; @@ -135,46 +118,101 @@ function get_word_with_smallest_cuts(candidates: Set, dict: Set) 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; + if (letters[index] !== letter) return dbg(false, "a", index); 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; + if (info.kind === "abscent") { + if (letters.includes(letter)) return dbg(false, "b", index); + continue; + } // is somewhere - if (letters[index] === letter) return false; - if (!letters.includes(letter)) return false; + if (letters[index] === letter) return dbg(false, "c", index); + if (!letters.includes(letter)) return dbg(false, "d", index); const index_ = letters.findIndex((i) => i === letter); letters[index_] = "."; } return true; } + +Deno.test("test_matches", () => { + { + const candidate = "peigne"; + const constraints = [ + ["s", { kind: "abscent" }], + ["o", { kind: "abscent" }], + ["i", { kind: "there" }], + ["r", { kind: "abscent" }], + ["e", { kind: "somewhere" }], + ["e", { kind: "there" }], + ] as [string, Info][]; + assertEquals(matches_constraints(candidate, constraints), true); + } + { + const candidate = "peigne"; + const constraints = [ + ["g", { kind: "somewhere" }], + ["e", { kind: "there" }], + ["n", { kind: "abscent" }], + ["a", { kind: "abscent" }], + ["n", { kind: "there" }], + ["t", { kind: "abscent" }], + ] as [string, Info][]; + assertEquals(matches_constraints(candidate, constraints), true); + } + { + const candidate = "arbre"; + const constraints = [ + ["r", { kind: "somewhere" }], + ["e", { kind: "somewhere" }], + ["s", { kind: "abscent" }], + ["t", { kind: "abscent" }], + ["a", { kind: "somewhere" }], + ] as [string, Info][]; + assertEquals(matches_constraints(candidate, constraints), true); + } + { + const candidate = "poncif"; + const constraints = [ + ["p", { kind: "there" }], + ["a", { kind: "abscent" }], + ["c", { kind: "somewhere" }], + ["i", { kind: "somewhere" }], + ["n", { kind: "somewhere" }], + ["o", { kind: "somewhere" }], + ] as [string, Info][]; + assertEquals(matches_constraints(candidate, constraints), true); + } + { + const candidate = "piteux"; + const constraints = [ + ["s", { kind: "abscent" }], + ["o", { kind: "abscent" }], + ["i", { kind: "somewhere" }], + ["r", { kind: "abscent" }], + ["e", { kind: "somewhere" }], + ["e", { kind: "abscent" }], + ] as [string, Info][]; + assertEquals(matches_constraints(candidate, constraints), true); + } + { + const candidate = "piteux"; + const constraints = [ + ["l", { kind: "abscent" }], + ["a", { kind: "abscent" }], + ["t", { kind: "there" }], + ["i", { kind: "somewhere" }], + ["n", { kind: "abscent" }], + ["e", { kind: "somewhere" }], + ] as [string, Info][]; + assertEquals(matches_constraints(candidate, constraints), true); + } +}); diff --git a/src/lib/runner.ts b/src/lib/runner.ts index 518e40e..a9edf50 100644 --- a/src/lib/runner.ts +++ b/src/lib/runner.ts @@ -10,13 +10,15 @@ export class Runner { delay_ms; turns; logging; + max; - constructor(game: Ga, guesser: Gu, logging: LoggingStrategy, delay_ms = 0) { + constructor(game: Ga, guesser: Gu, logging: LoggingStrategy, delay_ms = 0, max: number | undefined = undefined) { this.game = game; this.guesser = guesser; this.delay_ms = delay_ms; this.turns = [] as Turn[]; this.logging = logging; + this.max = max; } async play_all() { @@ -29,9 +31,11 @@ export class Runner { return result; }); if (result !== null) break; + if (this.max !== undefined) if (this.turns.length >= this.max) return this.turns; await wait(this.delay_ms); } this.logging.on_finish(this.turns); + return this.turns; } } @@ -99,7 +103,7 @@ export class TableLogging implements LoggingStrategy { this.columns = properties.map((p) => [p, p.length] as const); this.columns.splice(0, 0, ["guess", length], ["result", Math.max(7, length)]); let line = ""; - for (const [name, width] of this.columns) line += name.padStart(width) + " "; + for (const [name, width] of this.columns) line += name.padStart(width) + " "; console.log(line); console.log(); } @@ -107,7 +111,7 @@ export class TableLogging implements LoggingStrategy { on_guess(guess: string, result: GuessResult, ...properties: unknown[]): void { const properties_ = [guess, format_result(result), ...properties]; let line = ""; - for (const [[_, width], value] of zip(this.columns, properties_)) line += `${value}`.padStart(width) + " "; + for (const [[_, width], value] of zip(this.columns, properties_)) line += `${value}`.padStart(width) + " "; console.log(line); } diff --git a/src/simulation.ts b/src/simulation.ts index a524353..4b4ed65 100755 --- a/src/simulation.ts +++ b/src/simulation.ts @@ -15,6 +15,7 @@ import { } from "./lib/lib.ts"; import { francais } from "../data/data.ts"; +import { last } from "./lib/utils.ts"; async function main() { const args = await new Command().name("simulation") @@ -27,6 +28,9 @@ async function main() { ).option( "-n, --length ", "Length of the word to use from the dictionnary.", + ).option( + "-m, --max ", + "Maximum number of iterations.", ).option( "-w, --wait ", "Time to wait between guesses, in ms.", @@ -56,8 +60,10 @@ async function main() { console.log("Target is", game.word); console.log(); - const runner = new Runner(game, guesser, new TableLogging(), args.options.wait); - await runner.play_all(); + const runner = new Runner(game, guesser, new TableLogging(), args.options.wait, args.options.max); + const result = last(await runner.play_all()); + if (result === undefined) Deno.exit(1); + if (result.result.kind === "failure") Deno.exit(1); } function validate_target(target: string, length: number) {