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 {
words;
letters;
@ -40,6 +42,16 @@ export class Dict {
const words = new Set(this.words.values());
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[]) {

View file

@ -15,29 +15,9 @@ export class Simulator implements Gaming {
return new Simulator(word);
}
guess(guess_: string, _known: 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);
guess(guess: string, _known: string): GuessResult {
if (guess === this.word) return { kind: "success" };
const informations = info_of_guess(guess, this.word);
return { kind: "failure", informations };
}
@ -45,3 +25,27 @@ export class Simulator implements Gaming {
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 { GuessResult, Info } from "../game/game.ts";
import { Value } from "../utils.ts";
import { Awaitable, enumerate, range, zip } from "../utils.ts";
import { info_of_guess } from "../game/simulator.ts";
import { Awaitable, enumerate, first, zip } from "../utils.ts";
import { Guessing } from "./guesser.ts";
import { range } from "../utils.ts";
export class ReducingGuesser implements Guessing {
length;
words;
possibilities;
candidates;
public constructor(words: Dict) {
this.words = words;
this.words = words.words;
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>) {
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, "");
if (result.kind === "success") return result;
this.learn(guess, result.informations);
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[]) {
const to_delete = new Set<string>();
const pos = this.possibilities;
for (const [index, [letter, info]] of enumerate(zip(word, infos))) {
if (info.kind === "there") for (const word of pos.words) if (word[index] !== letter) to_delete.add(word);
if (info.kind === "somewhere") for (const word of pos.words) if (word[index] === letter) to_delete.add(word);
if (info.kind === "abscent") for (const word of pos.words) if (word.includes(letter)) to_delete.add(word);
let next_cnd = new Map([...this.candidates.values()].map((value) => [value, value]));
// TODO : should treat all there before somewheres
for (const [i, [info, c]] of enumerate(zip(infos, word))) {
if (info.kind === "abscent") next_cnd = filter_map(next_cnd, (_, r) => !r.includes(c));
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);
}
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 score_word_from_ranks(word: string, ranks: Map<string, Value<number>>[]) {
let result = 0;
for (const [index, letter] of enumerate(word)) {
// note : bonus for ANY letter.
for (const index_ranks of ranks) {
const letter_rank = index_ranks.get(letter)!.value;
result += letter_rank;
}
// note : bonus for THIS letter.
const index_ranks = ranks[index];
const letter_rank = index_ranks.get(letter)!.value;
result += letter_rank;
function filter_map<K, T>(
map: Map<K, T>,
predicate: (k: K, v: T) => boolean | null | T,
) {
const new_set = new Map<K, T>();
for (const [key, value] of map.entries()) {
const result = predicate(key, value);
if (result === null) continue;
if (result === false) continue;
if (result === true) new_set.set(key, value);
else new_set.set(key, result);
}
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.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 { BaseGuesser } from "./guesser/base.ts";
export { ReducingGuesser } from "./guesser/reducing.ts";
export { Simulator } from "./game/simulator.ts";
export { ManualProxy } from "./game/proxy.ts";
export { Runner } from "./runner.ts";

View file

@ -31,4 +31,13 @@ export function last<T>(iterable: Iterable<T>) {
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 };