Compare commits

..

2 commits

10 changed files with 225 additions and 85 deletions

View file

@ -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[]) {

View file

@ -6,5 +6,5 @@ export type GuessResult = { kind: "success" } | { kind: "failure"; informations:
export interface Gaming { export interface Gaming {
length(): number; length(): number;
guess(guess: string, known: string): Awaitable<GuessResult>; guess(guess: string): Awaitable<GuessResult>;
} }

View file

@ -10,7 +10,7 @@ export class ManualProxy implements Gaming {
this.word_length = length; this.word_length = length;
} }
guess(guess: string, _known: string): Awaitable<GuessResult> { guess(guess: string): Awaitable<GuessResult> {
console.log("<proxy> Guessing:", guess); console.log("<proxy> Guessing:", guess);
const result = read_until_correct(this.word_length); const result = read_until_correct(this.word_length);
console.log(); console.log();

View file

@ -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): 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;
}
} }

View file

@ -26,6 +26,10 @@ export class BaseGuesser implements Guessing {
} }
} }
declare_properties(): string[] {
return ["known".padStart(this.length)];
}
async guess(try_: (guess: string, known: string) => Awaitable<GuessResult>) { async guess(try_: (guess: string, known: string) => Awaitable<GuessResult>) {
const res = this.expected_result(); const res = this.expected_result();
if (res.completed) return await try_(res.result, res.result); if (res.completed) return await try_(res.result, res.result);

View file

@ -2,5 +2,6 @@ import { GuessResult } from "../game/game.ts";
import { Awaitable } from "../utils.ts"; import { Awaitable } from "../utils.ts";
export interface Guessing { export interface Guessing {
guess(try_: (guess: string, known: string) => Awaitable<GuessResult>): Promise<GuessResult | null>; declare_properties(): string[];
guess(try_: (guess: string, ...properties: unknown[]) => Awaitable<GuessResult>): Promise<GuessResult | null>;
} }

View file

@ -1,69 +1,69 @@
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 declare_properties() {
const guess = this.make_guess(); return ["candidates"];
const result = await try_(guess, ""); }
public async guess(try_: (guess: string, candidates: number) => Awaitable<GuessResult>) {
if (this.candidates.size === 1) return await try_(first(this.candidates)!, this.candidates.size);
const guess = get_word_with_smallest_cuts(this.candidates, this.words);
const result = await try_(guess, this.candidates.size);
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; 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 result; return word.replace(letter, replace);
} }
/* /*
@ -75,3 +75,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");
});

View file

@ -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";

View file

@ -2,7 +2,7 @@ import { assertExists } from "https://deno.land/std@0.224.0/assert/assert_exists
import { Guessing } from "./guesser/guesser.ts"; import { Guessing } from "./guesser/guesser.ts";
import { Gaming, GuessResult } from "./game/game.ts"; import { Gaming, GuessResult } from "./game/game.ts";
import { last, wait } from "./utils.ts"; import { last, wait, zip } from "./utils.ts";
export class Runner<Ga extends Gaming, Gu extends Guessing> { export class Runner<Ga extends Gaming, Gu extends Guessing> {
game; game;
@ -22,10 +22,10 @@ export class Runner<Ga extends Gaming, Gu extends Guessing> {
async play_all() { async play_all() {
this.logging.on_start(this.game.length()); this.logging.on_start(this.game.length());
while (true) { while (true) {
const result = await this.guesser.guess(async (guess, known) => { const result = await this.guesser.guess(async (guess, ...properties) => {
const result = await this.game.guess(guess, known); const result = await this.game.guess(guess);
this.turns.push({ guess, result }); this.turns.push({ guess, result });
this.logging.on_guess(known, guess, result); this.logging.on_guess(guess, result, ...properties);
return result; return result;
}); });
if (result !== null) break; if (result !== null) break;
@ -49,18 +49,29 @@ function format_result(result: GuessResult) {
} }
interface LoggingStrategy { interface LoggingStrategy {
on_start(length: number): void; on_start(length: number, ...properties: string[]): void;
on_guess(known: string, guess: string, result: GuessResult): void; on_guess(guess: string, result: GuessResult, ...properties: unknown[]): void;
on_finish(turns: Turn[]): void; on_finish(turns: Turn[]): void;
} }
export class VerboseLogging implements LoggingStrategy { export class VerboseLogging implements LoggingStrategy {
on_start(_length: number) {} properties;
length;
on_guess(known: string, guess: string, result: GuessResult): void { constructor() {
console.log(" Knows:", known); this.length = 0;
console.log("Guessing:", guess); this.properties = [] as string[];
console.log(" Got:", format_result(result)); }
on_start(_length: number, ...properties: string[]) {
this.length = Math.max(8, ...properties.map((p) => p.length));
this.properties = properties;
}
on_guess(guess: string, result: GuessResult, ...properties: unknown[]): void {
for (const [name, value] of zip(this.properties, properties)) this.log_entry(name, value);
this.log_entry("Guessing", guess);
this.log_entry("Got", format_result(result));
console.log(); console.log();
} }
@ -71,17 +82,35 @@ export class VerboseLogging implements LoggingStrategy {
console.log("With", last_.result.kind, "."); console.log("With", last_.result.kind, ".");
console.log("In", turns.length, "steps"); console.log("In", turns.length, "steps");
} }
log_entry(name: string, value: unknown) {
console.log(name.padStart(this.length) + ": " + value);
}
} }
export class TableLogging implements LoggingStrategy { export class TableLogging implements LoggingStrategy {
on_start(length: number): void { columns;
const pad = (title: string) => title.padEnd(length);
console.log(`${pad("known")}\t${pad("guess")}\t${pad("result")}`); constructor() {
this.columns = [] as (readonly [string, number])[];
}
on_start(length: number, ...properties: string[]): void {
this.columns = properties.map((p) => [p, p.length] as const);
this.columns.splice(0, 0, ["guess", length], ["result", length]);
let line = "";
for (const [name, width] of this.columns) line += name.padStart(width) + " ";
console.log(line);
console.log(); console.log();
} }
on_guess(known: string, guess: string, result: GuessResult): void {
console.log(`${known}\t${guess}\t${format_result(result)}`); on_guess(guess: string, result: GuessResult, ...properties: unknown[]): void {
const properties_ = [guess, format_result(result), ...properties];
let line = "";
for (const [[_, width], value] of zip(this.columns, properties_)) line += `${value}`.padStart(width) + " ";
console.log(line);
} }
on_finish(turns: Turn[]): void { on_finish(turns: Turn[]): void {
const last_ = last(turns); const last_ = last(turns);
assertExists(last_); assertExists(last_);

View file

@ -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 };