add reducing implementation

This commit is contained in:
JOLIMAITRE Matthieu 2024-05-02 15:45:54 +02:00
parent 756c989eab
commit 336efff5c1
5 changed files with 166 additions and 64 deletions

View file

@ -1,3 +1,5 @@
import { range } from "./utils.ts";
export class Dict { export class Dict {
words; words;
letters; letters;
@ -40,6 +42,16 @@ export class Dict {
const words = new Set(this.words.values()); const words = new Set(this.words.values());
return new Dict(words, this.length); return new Dict(words, this.length);
} }
sample(quantity: number) {
const words = new Set<string>();
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[]) { export function contains_any(text: string, words: string[]) {

View file

@ -15,29 +15,9 @@ export class Simulator implements Gaming {
return new Simulator(word); return new Simulator(word);
} }
guess(guess_: string, _known: string): GuessResult { guess(guess: string, _known: string): GuessResult {
if (guess_ === this.word) return { kind: "success" }; if (guess === this.word) return { kind: "success" };
const rest_actual = [...this.word].map((letter) => letter as (string | null)); const informations = info_of_guess(guess, this.word);
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 }; return { kind: "failure", informations };
} }
@ -45,3 +25,27 @@ export class Simulator implements Gaming {
return this.word.length; 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);
}

View file

@ -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 { Dict } from "../dict.ts";
import { GuessResult, Info } from "../game/game.ts"; import { GuessResult, Info } from "../game/game.ts";
import { Value } from "../utils.ts"; import { info_of_guess } from "../game/simulator.ts";
import { Awaitable, enumerate, range, zip } from "../utils.ts"; import { Awaitable, enumerate, first, zip } from "../utils.ts";
import { Guessing } from "./guesser.ts"; import { Guessing } from "./guesser.ts";
import { range } from "../utils.ts";
export class ReducingGuesser implements Guessing { export class ReducingGuesser implements Guessing {
length; length;
words; words;
possibilities; candidates;
public constructor(words: Dict) { public constructor(words: Dict) {
this.words = words; this.words = words.words;
this.length = words.length; this.length = words.length;
this.possibilities = words.clone(); this.candidates = new Set(this.words.values());
} }
public async guess(try_: (guess: string, known: string) => Awaitable<GuessResult>) { public async guess(try_: (guess: string, known: string) => Awaitable<GuessResult>) {
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, ""); const result = await try_(guess, "");
if (result.kind === "success") return result; if (result.kind === "success") return result;
this.learn(guess, result.informations); this.learn(guess, result.informations);
return null; 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[]) { learn(word: string, infos: Info[]) {
const to_delete = new Set<string>(); let next_cnd = new Map([...this.candidates.values()].map((value) => [value, value]));
const pos = this.possibilities; // TODO : should treat all there before somewheres
for (const [index, [letter, info]] of enumerate(zip(word, infos))) { for (const [i, [info, c]] of enumerate(zip(infos, word))) {
if (info.kind === "there") for (const word of pos.words) if (word[index] !== letter) to_delete.add(word); if (info.kind === "abscent") next_cnd = filter_map(next_cnd, (_, r) => !r.includes(c));
if (info.kind === "somewhere") for (const word of pos.words) if (word[index] === letter) to_delete.add(word); if (info.kind === "somewhere") next_cnd = filter_map(next_cnd, (_, r) => r.includes(c) ? consume(r, c) : null);
if (info.kind === "abscent") for (const word of pos.words) if (word.includes(letter)) to_delete.add(word); 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 filter_map<K, T>(
function score_word_from_ranks(word: string, ranks: Map<string, Value<number>>[]) { map: Map<K, T>,
let result = 0; predicate: (k: K, v: T) => boolean | null | T,
for (const [index, letter] of enumerate(word)) { ) {
// note : bonus for ANY letter. const new_set = new Map<K, T>();
for (const index_ranks of ranks) { for (const [key, value] of map.entries()) {
const letter_rank = index_ranks.get(letter)!.value; const result = predicate(key, value);
result += letter_rank; if (result === null) continue;
} if (result === false) continue;
// note : bonus for THIS letter. if (result === true) new_set.set(key, value);
const index_ranks = ranks[index]; else new_set.set(key, result);
const letter_rank = index_ranks.get(letter)!.value;
result += letter_rank;
} }
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.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. 2.2 play the word which individual letters cuts most of the possible space.
*/ */
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) {
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<string>, dict: Set<string>) {
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");
});

View file

@ -1,5 +1,6 @@
export { Dict } from "./dict.ts"; export { Dict } from "./dict.ts";
export { BaseGuesser } from "./guesser/base.ts"; export { BaseGuesser } from "./guesser/base.ts";
export { ReducingGuesser } from "./guesser/reducing.ts";
export { Simulator } from "./game/simulator.ts"; export { Simulator } from "./game/simulator.ts";
export { ManualProxy } from "./game/proxy.ts"; export { ManualProxy } from "./game/proxy.ts";
export { Runner } from "./runner.ts"; export { Runner } from "./runner.ts";

View file

@ -31,4 +31,13 @@ export function last<T>(iterable: Iterable<T>) {
return last; return last;
} }
export function first<T>(iterable: Iterable<T>) {
for (const item of iterable) return item;
}
export function dbg<T>(value: T, ...rest: unknown[]) {
console.log("[DBG]", value, ...rest);
return value;
}
export type Value<T> = { value: T }; export type Value<T> = { value: T };