fixes and cleanup

This commit is contained in:
JOLIMAITRE Matthieu 2024-05-03 06:12:12 +02:00
parent 374c42802e
commit bc211621e5
6 changed files with 199 additions and 83 deletions

View file

@ -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<number>();
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);
});

View file

@ -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];
}

View file

@ -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<GuessResult>): Promise<GuessResult | null>;
}
export function pick_random_common_word(bests: string[]) {
assertNotEquals(bests.length, 0);
const index = Math.floor(Math.random() * bests.length);
return bests[index];
}

View file

@ -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<GuessResult>) {
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<string>) {
const result = [] as [Set<string>, Set<string>, Set<string>][];
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<string>(), new Set<string>(), new Set<string>()];
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<string>) {
const results = [...range(0, word.length)]
.map(() => [new Set<string>(), new Set<string>(), new Set<string>()] as const);
for (const option of dict) {
@ -125,7 +108,7 @@ function word_cuts_alt(word: string, dict: Set<string>) {
function get_word_with_smallest_cuts(candidates: Set<string>, dict: Set<string>) {
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<string>, dict: Set<string>)
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);
}
});

View file

@ -10,13 +10,15 @@ export class Runner<Ga extends Gaming, Gu extends Guessing> {
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<Ga extends Gaming, Gu extends Guessing> {
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;
}
}

View file

@ -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:number>",
"Length of the word to use from the dictionnary.",
).option(
"-m, --max <max:number>",
"Maximum number of iterations.",
).option(
"-w, --wait <wait:number>",
"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) {