turbotusmo/src/lib/guesser/reducing.ts

180 lines
6.2 KiB
TypeScript

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 { info_of_guess } from "../game/simulator.ts";
import { Awaitable, enumerate, first, range, zip } from "../utils.ts";
import { Guessing } from "./guesser.ts";
export class ReducingGuesser implements Guessing {
length;
words;
candidates;
public constructor(words: Dict) {
this.words = words.words;
this.length = words.length;
this.candidates = new Set(this.words.values());
}
public declare_properties() {
return ["candidates", "best score"];
}
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);
const result = await try_(guess, this.candidates.size, score);
if (result.kind === "success") return result;
this.learn(guess, result.informations);
return null;
}
learn(word: string, infos: Info[]) {
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);
}
this.candidates = new Set(next_cnd.keys());
}
restraint_candidates(constraints: [string, Info][]) {
const new_cand = new Set<string>();
for (const cand of this.candidates.values()) if (matches_constraints(cand, constraints)) new_cand.add(cand);
return new_cand;
}
}
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 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);
}
/*
note : The algorithm must proceed as follow :
1. establish a list of possible words
2. loop :
2.1 for each word, for each letter, estimate by how much this letter cuts possibilities for all three possible outcomes.
(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>) {
let best = null as null | [string, number];
for (const candidate of 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];
}
assertExists(best);
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;
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;
// is somewhere
if (letters[index] === letter) return false;
if (!letters.includes(letter)) return false;
const index_ = letters.findIndex((i) => i === letter);
letters[index_] = ".";
}
return true;
}