180 lines
6.2 KiB
TypeScript
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;
|
|
}
|