add move guesser interface
This commit is contained in:
parent
e2e2852739
commit
2e9c90dbca
6 changed files with 37 additions and 17 deletions
132
src/lib/guesser/base.ts
Normal file
132
src/lib/guesser/base.ts
Normal file
|
@ -0,0 +1,132 @@
|
|||
import { assertExists } from "https://deno.land/std@0.224.0/assert/assert_exists.ts";
|
||||
|
||||
import { Awaitable, enumerate, range, zip } from "../utils.ts";
|
||||
import { Dict } from "../dict.ts";
|
||||
import { GuessResult } from "../game/game.ts";
|
||||
import { Guessing } from "./guesser.ts";
|
||||
|
||||
type Knowledge = {
|
||||
letter: string;
|
||||
at: Set<number>;
|
||||
exists: "unknown" | boolean;
|
||||
not_at: Set<number>;
|
||||
};
|
||||
|
||||
export class BaseGuesser implements Guessing {
|
||||
length;
|
||||
dict;
|
||||
informations;
|
||||
|
||||
constructor(dict: Dict) {
|
||||
this.length = dict.length;
|
||||
this.dict = dict;
|
||||
this.informations = new Map<string, Knowledge>();
|
||||
for (const letter of dict.letters.values()) {
|
||||
this.informations.set(letter, { letter, at: new Set(), not_at: new Set(), exists: "unknown" });
|
||||
}
|
||||
}
|
||||
|
||||
async guess(try_: (guess: string, known: string) => Awaitable<GuessResult>) {
|
||||
const res = this.expected_result();
|
||||
if (res.completed) return await try_(res.result, res.result);
|
||||
|
||||
const possibilities = [...this.possible_words()];
|
||||
if (possibilities.length === 1) return await try_(possibilities[0], res.result);
|
||||
|
||||
const words = [...this.dict.words.values()];
|
||||
const scored = words.map((word) => [this.info_score_for(word), word] as const);
|
||||
const best_score = scored.reduce((acc, [next]) => Math.max(acc, next), 0);
|
||||
const bests = scored.filter(([s]) => s === best_score).map(([_, word]) => word);
|
||||
const guess = pick_random_common_word(bests);
|
||||
|
||||
const result = await try_(guess, res.result);
|
||||
if (result.kind === "success") return result;
|
||||
for (const [index, info] of enumerate(result.informations)) {
|
||||
const letter = guess[index];
|
||||
if (info.kind === "there") this.learn_letter_at(letter, index);
|
||||
if (info.kind === "abscent") this.learn_does_not_exist(letter);
|
||||
if (info.kind === "somewhere") {
|
||||
this.learn_does_exist(letter);
|
||||
this.learn_letter_not_at(letter, index);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public learn_does_not_exist(letter: string) {
|
||||
const letter_info = this.informations.get(letter);
|
||||
assertExists(letter_info);
|
||||
letter_info.exists = false;
|
||||
}
|
||||
|
||||
public learn_does_exist(letter: string) {
|
||||
const letter_info = this.informations.get(letter);
|
||||
assertExists(letter_info);
|
||||
letter_info.exists = true;
|
||||
const total_known = [...this.informations.values()].filter((i) => i.exists === true).length;
|
||||
if (total_known < this.length) return;
|
||||
for (const info of this.informations.values()) if (info.exists === "unknown") info.exists = false;
|
||||
}
|
||||
|
||||
public learn_letter_at(letter: string, at: number) {
|
||||
this.learn_does_exist(letter);
|
||||
const letter_info = this.informations.get(letter);
|
||||
assertExists(letter_info);
|
||||
letter_info.at.add(at);
|
||||
for (const l of this.informations.keys()) {
|
||||
if (letter === l) continue;
|
||||
this.learn_letter_not_at(l, at);
|
||||
}
|
||||
}
|
||||
|
||||
public learn_letter_not_at(letter: string, at: number) {
|
||||
const letter_info = this.informations.get(letter);
|
||||
assertExists(letter_info);
|
||||
letter_info.not_at.add(at);
|
||||
}
|
||||
|
||||
info_score_for(word: string) {
|
||||
let total = 0;
|
||||
for (const [index, letter] of enumerate(word)) {
|
||||
const information = this.informations.get(letter);
|
||||
assertExists(information);
|
||||
if (information.exists === false) continue;
|
||||
if (information.at.has(index)) continue;
|
||||
if (information.not_at.has(index)) continue;
|
||||
|
||||
if (information.exists === "unknown") total += this.length ** 1;
|
||||
if (information.exists === true) total += this.length ** 2;
|
||||
}
|
||||
const different_letters = new Set([...word]);
|
||||
total += different_letters.size;
|
||||
return total;
|
||||
}
|
||||
|
||||
expected_result() {
|
||||
const known_arr = [...range(0, this.length)].map(() => null as string | null);
|
||||
for (const [letter, info] of this.informations.entries()) {
|
||||
for (const pos of info.at.values()) known_arr[pos] = letter;
|
||||
}
|
||||
const result = known_arr.map((l) => l === null ? "." : l).join("");
|
||||
const completed = !result.includes(".");
|
||||
return { completed, result };
|
||||
}
|
||||
|
||||
*possible_words() {
|
||||
for (const word of this.dict.words.values()) if (this.matches_expected(word)) yield word;
|
||||
}
|
||||
|
||||
matches_expected(word: string) {
|
||||
const expected = this.expected_result();
|
||||
for (const [act, exp] of zip(word, expected.result)) {
|
||||
if (exp === ".") continue;
|
||||
if (act !== exp) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function pick_random_common_word(bests: string[]) {
|
||||
const index = Math.floor(Math.random() * bests.length);
|
||||
return bests[index];
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue