add reducing implementation
This commit is contained in:
parent
756c989eab
commit
336efff5c1
5 changed files with 166 additions and 64 deletions
|
@ -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[]) {
|
||||||
|
|
|
@ -15,11 +15,21 @@ 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));
|
return { kind: "failure", informations };
|
||||||
const info = [...range(0, this.word.length)].map(() => null) as (Info | null)[];
|
}
|
||||||
|
|
||||||
|
length(): number {
|
||||||
|
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))) {
|
for (const [index, [guessed, actual]] of enumerate(zip(rest_guess, rest_actual))) {
|
||||||
if (guessed !== actual) continue;
|
if (guessed !== actual) continue;
|
||||||
|
@ -37,11 +47,5 @@ export class Simulator implements Gaming {
|
||||||
rest_actual[rest_actual.indexOf(guessed)] = null;
|
rest_actual[rest_actual.indexOf(guessed)] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const informations = info.map((i) => i != undefined ? i : ({ kind: "abscent" }) as Info);
|
return info.map((i) => i != undefined ? i : ({ kind: "abscent" }) as Info);
|
||||||
return { kind: "failure", informations };
|
|
||||||
}
|
|
||||||
|
|
||||||
length(): number {
|
|
||||||
return this.word.length;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
if (result === true) new_set.set(key, value);
|
||||||
|
else new_set.set(key, result);
|
||||||
}
|
}
|
||||||
// note : bonus for THIS letter.
|
return new_set;
|
||||||
const index_ranks = ranks[index];
|
|
||||||
const letter_rank = index_ranks.get(letter)!.value;
|
|
||||||
result += letter_rank;
|
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue