change stdin api for better testability
This commit is contained in:
parent
1059391e3b
commit
334c7361fc
13 changed files with 312 additions and 44 deletions
47
src/game.ts
Normal file
47
src/game.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
#!/usr/bin/env -S deno run --allow-read
|
||||
|
||||
import { Command } from "https://deno.land/x/cliffy@v1.0.0-rc.4/command/mod.ts";
|
||||
import { Dict, GameLogging, InputGuesser, Runner, Simulator } from "./lib/lib.ts";
|
||||
import { gutenberg } from "../data/data.ts";
|
||||
import { range } from "./lib/utils.ts";
|
||||
|
||||
async function main() {
|
||||
const args = await new Command().name("simulation")
|
||||
.description(
|
||||
"Program to simulate TUSMO game with guesser controller.",
|
||||
)
|
||||
.option(
|
||||
"-f, --file <path:string>",
|
||||
"Sets dictionnary to use words from (defaults to internal french dict).",
|
||||
).option(
|
||||
"-l, --length <length:number>",
|
||||
"Length of the word to use from the dictionnary.",
|
||||
).option(
|
||||
"-n, --iterations <iterations:number>",
|
||||
"Number of iterations.",
|
||||
).option(
|
||||
"-t, --target <target:string>",
|
||||
"Target word to search for.",
|
||||
).parse(Deno.args);
|
||||
|
||||
const length = args.options.length ?? args.options.target?.length ?? 6;
|
||||
|
||||
let dict = Dict.from_lines(gutenberg, length);
|
||||
if (args.options.file !== undefined) dict = await Dict.from_text_file(args.options.file, length);
|
||||
|
||||
let game = Simulator.from_dict_rand(dict);
|
||||
if (args.options.target !== undefined) game = new Simulator(validate_target(args.options.target, length));
|
||||
|
||||
const guesser = new InputGuesser(length);
|
||||
const runner = new Runner(game, guesser, new GameLogging());
|
||||
|
||||
const iterations = args.options.iterations ?? 100_000;
|
||||
for (const _ of range(0, iterations)) await runner.play_once();
|
||||
}
|
||||
|
||||
function validate_target(target: string, length: number) {
|
||||
if (target.length !== length) throw new Error("Invalid target length");
|
||||
return target.toLowerCase();
|
||||
}
|
||||
|
||||
if (import.meta.main) await main();
|
|
@ -3,7 +3,7 @@ import { assertExists } from "https://deno.land/std@0.224.0/assert/assert_exists
|
|||
import { Awaitable, enumerate, range, zip } from "../utils.ts";
|
||||
import { Dict } from "../dict.ts";
|
||||
import { GuessResult } from "../game/game.ts";
|
||||
import { Guessing } from "./guesser.ts";
|
||||
import { Guessing, pick_random_common_word } from "./guesser.ts";
|
||||
|
||||
type Knowledge = {
|
||||
letter: string;
|
||||
|
|
42
src/lib/guesser/input.ts
Normal file
42
src/lib/guesser/input.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { assertExists } from "https://deno.land/std@0.224.0/assert/assert_exists.ts";
|
||||
import { GuessResult } from "../game/game.ts";
|
||||
import { async_next, Awaitable, LineReader } from "../utils.ts";
|
||||
import { Guessing } from "./guesser.ts";
|
||||
|
||||
export class InputGuesser implements Guessing {
|
||||
length;
|
||||
lines;
|
||||
|
||||
constructor(length: number) {
|
||||
this.length = length;
|
||||
this.lines = new LineReader(Deno.stdin.readable);
|
||||
}
|
||||
|
||||
declare_properties(): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
async next_line() {
|
||||
const value = await this.lines.read();
|
||||
assertExists(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
async guess(try_: (guess: string, ...properties: unknown[]) => Awaitable<GuessResult>) {
|
||||
let input = "";
|
||||
|
||||
while (true) {
|
||||
input = await this.next_line();
|
||||
if (input.length === this.length && are_letters(input)) break;
|
||||
console.log("Word is length", this.length, "and composed of letters.");
|
||||
}
|
||||
|
||||
return await try_(input.slice(0));
|
||||
}
|
||||
}
|
||||
|
||||
function are_letters(text: string) {
|
||||
const letters = "azertyuiopqsdfghjklmwxcvbn";
|
||||
for (const char of text) if (!letters.includes(char)) return false;
|
||||
return true;
|
||||
}
|
|
@ -6,6 +6,11 @@ import { GuessResult, Info } from "../game/game.ts";
|
|||
import { info_of_guess } from "../game/simulator.ts";
|
||||
import { Awaitable, dbg, enumerate, first, range, zip } from "../utils.ts";
|
||||
import { Guessing, pick_random_common_word } from "./guesser.ts";
|
||||
import {
|
||||
section,
|
||||
tip,
|
||||
top,
|
||||
} from "https://git.barnulf.net/mb/profiterole/raw/commit/02d19fce2a0878abd176801cf8f2a663f6db6c16/mod.ts";
|
||||
|
||||
export class ReducingGuesser implements Guessing {
|
||||
length;
|
||||
|
@ -23,19 +28,21 @@ export class ReducingGuesser implements Guessing {
|
|||
}
|
||||
|
||||
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);
|
||||
if (score >= this.candidates.size) {
|
||||
const pick = pick_random_common_word([...this.candidates.values()]);
|
||||
assertExists(pick);
|
||||
const result = await try_(pick, this.candidates.size, score);
|
||||
return await section("guess", async () => {
|
||||
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);
|
||||
if (score >= this.candidates.size) {
|
||||
const pick = pick_random_common_word([...this.candidates.values()]);
|
||||
assertExists(pick);
|
||||
const result = await try_(pick, this.candidates.size, score);
|
||||
if (result.kind === "success") return result;
|
||||
else return null;
|
||||
}
|
||||
const result = await try_(guess, this.candidates.size, score);
|
||||
if (result.kind === "success") return result;
|
||||
else return null;
|
||||
}
|
||||
const result = await try_(guess, this.candidates.size, score);
|
||||
if (result.kind === "success") return result;
|
||||
this.learn(guess, result.informations);
|
||||
return null;
|
||||
this.learn(guess, result.informations);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
learn(word: string, infos: Info[]) {
|
||||
|
@ -92,6 +99,7 @@ note : The algorithm must proceed as follow :
|
|||
*/
|
||||
|
||||
function word_cuts(word: string, dict: Set<string>) {
|
||||
tip("word_cuts");
|
||||
const results = [...range(0, word.length)]
|
||||
.map(() => [new Set<string>(), new Set<string>(), new Set<string>()] as const);
|
||||
for (const option of dict) {
|
||||
|
@ -102,20 +110,23 @@ function word_cuts(word: string, dict: Set<string>) {
|
|||
if (info.kind === "abscent") results[index][2].add(option);
|
||||
}
|
||||
}
|
||||
top("word_cuts");
|
||||
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(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;
|
||||
return section("get_word_with_smallest_cuts", () => {
|
||||
let best = null as null | [string, number];
|
||||
for (const candidate of dict.values()) {
|
||||
const cuts = word_cuts(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;
|
||||
});
|
||||
}
|
||||
|
||||
function matches_constraints(candidate: string, constraints: [string, Info][]) {
|
||||
|
|
|
@ -4,6 +4,7 @@ export type { LoggingStrategy } from "./runner.ts";
|
|||
export { Dict } from "./dict.ts";
|
||||
export { ExplorerGuesser } from "./guesser/explorer.ts";
|
||||
export { ReducingGuesser } from "./guesser/reducing.ts";
|
||||
export { InputGuesser } from "./guesser/input.ts";
|
||||
export { Simulator } from "./game/simulator.ts";
|
||||
export { ManualProxy } from "./game/proxy.ts";
|
||||
export { Runner, TableLogging, VerboseLogging } from "./runner.ts";
|
||||
export { GameLogging, Runner, TableLogging, VerboseLogging } from "./runner.ts";
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { assertExists } from "https://deno.land/std@0.224.0/assert/assert_exists.ts";
|
||||
|
||||
import { enumerate } from "./utils.ts";
|
||||
import { async_next, enumerate, LineReader, next } from "./utils.ts";
|
||||
|
||||
export function initialize_prompt() {
|
||||
export async function initialize_prompt(lines: LineReader) {
|
||||
console.log("Please input initial state of the game. Format is :");
|
||||
console.log(" letter [a-z] known letter");
|
||||
console.log(" dot . unknown letter");
|
||||
console.log("example : .rb..");
|
||||
console.log("");
|
||||
const inputted = parse_initialization_until_correct();
|
||||
const inputted = await parse_initialization_until_correct(lines);
|
||||
console.log("");
|
||||
console.log("From now on, please try the following guesses and report results. Format is :");
|
||||
console.log(" plus + correct");
|
||||
|
@ -19,13 +19,14 @@ export function initialize_prompt() {
|
|||
return inputted;
|
||||
}
|
||||
|
||||
function parse_initialization_until_correct() {
|
||||
let input = prompt("initial :");
|
||||
async function parse_initialization_until_correct(lines: LineReader) {
|
||||
console.log("initial : ");
|
||||
while (true) {
|
||||
const input = await lines.read();
|
||||
assertExists(input);
|
||||
const parsed = parse_initialization(input);
|
||||
if (parsed === null) input = prompt("Invalid, please try again :");
|
||||
else return parsed;
|
||||
if (parsed !== null) return parsed;
|
||||
console.log("Invalid, please try again : ");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,10 @@ import { assertExists } from "https://deno.land/std@0.224.0/assert/assert_exists
|
|||
import { Guessing } from "./guesser/guesser.ts";
|
||||
import { Gaming, GuessResult } from "./game/game.ts";
|
||||
import { last, wait, zip } from "./utils.ts";
|
||||
import {
|
||||
tip,
|
||||
top,
|
||||
} from "https://git.barnulf.net/mb/profiterole/raw/commit/02d19fce2a0878abd176801cf8f2a663f6db6c16/mod.ts";
|
||||
|
||||
export class Runner<Ga extends Gaming, Gu extends Guessing> {
|
||||
game;
|
||||
|
@ -21,17 +25,24 @@ export class Runner<Ga extends Gaming, Gu extends Guessing> {
|
|||
this.max = max;
|
||||
}
|
||||
|
||||
async play_once() {
|
||||
tip("play_once");
|
||||
const res = await this.guesser.guess(async (guess, ...properties) => {
|
||||
const result = await this.game.guess(guess);
|
||||
this.turns.push({ guess, result });
|
||||
this.logging.on_guess(guess, result, ...properties);
|
||||
return result;
|
||||
});
|
||||
top("play_once");
|
||||
return res;
|
||||
}
|
||||
|
||||
async play_all() {
|
||||
this.logging.on_start(this.game.length(), ...this.guesser.declare_properties());
|
||||
while (true) {
|
||||
const result = await this.guesser.guess(async (guess, ...properties) => {
|
||||
const result = await this.game.guess(guess);
|
||||
this.turns.push({ guess, result });
|
||||
this.logging.on_guess(guess, result, ...properties);
|
||||
return result;
|
||||
});
|
||||
const result = await this.play_once();
|
||||
if (result !== null) break;
|
||||
if (this.max !== undefined) if (this.turns.length >= this.max) return this.turns;
|
||||
if (this.max !== undefined) { if (this.turns.length >= this.max) return this.turns; }
|
||||
await wait(this.delay_ms);
|
||||
}
|
||||
this.logging.on_finish(this.turns);
|
||||
|
@ -122,3 +133,31 @@ export class TableLogging implements LoggingStrategy {
|
|||
console.log(last_.result.kind, "in", turns.length, "steps");
|
||||
}
|
||||
}
|
||||
|
||||
export class GameLogging implements LoggingStrategy {
|
||||
on_finish(turns: Turn[]): void {
|
||||
const last_ = last(turns);
|
||||
assertExists(last_);
|
||||
console.log();
|
||||
console.log(last_.result.kind, "in", turns.length, "steps");
|
||||
}
|
||||
|
||||
on_guess(_guess: string, result: GuessResult, ..._properties: unknown[]): void {
|
||||
console.log(this.result_to_display(result));
|
||||
}
|
||||
|
||||
on_start(_length: number, ..._properties: string[]): void {
|
||||
}
|
||||
|
||||
result_to_display(result: GuessResult) {
|
||||
if (result.kind === "success") return " success";
|
||||
|
||||
let line = " ";
|
||||
for (const info of result.informations) {
|
||||
if (info.kind === "abscent") line += ".";
|
||||
if (info.kind === "somewhere") line += "-";
|
||||
if (info.kind === "there") line += "+";
|
||||
}
|
||||
return line;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import { assertExists } from "https://deno.land/std@0.224.0/assert/assert_exists.ts";
|
||||
import { dirname } from "https://deno.land/std@0.224.0/path/dirname.ts";
|
||||
import { TextLineStream } from "https://deno.land/std@0.224.0/streams/text_line_stream.ts";
|
||||
|
||||
export function* enumerate<T>(iterator: Iterable<T>) {
|
||||
let index = 0;
|
||||
for (const item of iterator) yield [index++, item] as const;
|
||||
|
@ -41,3 +45,93 @@ export function dbg<T>(value: T, ...rest: unknown[]) {
|
|||
}
|
||||
|
||||
export type Value<T> = { value: T };
|
||||
|
||||
export function next<T>(iterator: Iterator<T>) {
|
||||
const result = iterator.next().value;
|
||||
if (result === undefined) return null;
|
||||
else return result as T;
|
||||
}
|
||||
|
||||
export async function async_next<T>(iterator: AsyncIterator<T>) {
|
||||
const result = await iterator.next();
|
||||
const value = result.value;
|
||||
if (value === undefined) return null;
|
||||
else return value as T;
|
||||
}
|
||||
|
||||
export class LineReader {
|
||||
iterator;
|
||||
|
||||
constructor(readable: ReadableStream<Uint8Array>) {
|
||||
this.iterator = readable
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
.pipeThrough(new TextLineStream())
|
||||
[Symbol.asyncIterator]();
|
||||
}
|
||||
|
||||
async read() {
|
||||
return await async_next(this.iterator);
|
||||
}
|
||||
}
|
||||
|
||||
export class LineWriter {
|
||||
writable;
|
||||
|
||||
constructor(writable: WritableStream<Uint8Array>) {
|
||||
const encoder = new TextEncoderStream();
|
||||
encoder.readable.pipeTo(writable);
|
||||
this.writable = encoder.writable.getWriter();
|
||||
}
|
||||
|
||||
async write(line: string) {
|
||||
await this.writable.write(line);
|
||||
}
|
||||
}
|
||||
|
||||
export function write_lines(writable: WritableStream<Uint8Array>) {
|
||||
const queue = new AsyncQueue<string>();
|
||||
ReadableStream.from(queue.iter()).pipeThrough(new TextEncoderStream()).pipeTo(writable);
|
||||
return { queue, write: (line: string) => queue.push(line) };
|
||||
}
|
||||
|
||||
export type Consumer<T> = (value: T) => void;
|
||||
|
||||
export function split_promise<T = void>() {
|
||||
let resolver = null as null | Consumer<T>;
|
||||
const promise = new Promise<T>((res) => resolver = res);
|
||||
assertExists(resolver);
|
||||
return [promise, resolver] as const;
|
||||
}
|
||||
|
||||
class AsyncQueue<T> {
|
||||
items;
|
||||
item_resolver;
|
||||
|
||||
constructor() {
|
||||
this.items = [] as T[];
|
||||
this.item_resolver = null as null | Consumer<T>;
|
||||
}
|
||||
|
||||
push(item: T) {
|
||||
if (this.item_resolver !== null) this.item_resolver(item);
|
||||
else this.items.push(item);
|
||||
}
|
||||
|
||||
async pull() {
|
||||
const [first] = this.items.splice(0, 1);
|
||||
if (first !== undefined) return first;
|
||||
const [promise, resolver] = split_promise<T>();
|
||||
this.item_resolver = resolver;
|
||||
return await promise;
|
||||
}
|
||||
|
||||
async *iter() {
|
||||
while (true) yield await this.pull();
|
||||
}
|
||||
}
|
||||
|
||||
export async function project_root() {
|
||||
const this_url = new URL(import.meta.url);
|
||||
const this_path = await Deno.realPath(this_url.pathname);
|
||||
return dirname(dirname(dirname(this_path)));
|
||||
}
|
||||
|
|
|
@ -5,8 +5,9 @@ import { Command } from "https://deno.land/x/cliffy@v1.0.0-rc.4/command/mod.ts";
|
|||
import { Dict, ExplorerGuesser, Guessing, ManualProxy, ReducingGuesser, Runner } from "./lib/lib.ts";
|
||||
import { initialize_prompt } from "./lib/prompt.ts";
|
||||
import { VerboseLogging } from "./lib/runner.ts";
|
||||
import { LineReader } from "./lib/utils.ts";
|
||||
|
||||
import { francais, gutenberg } from "../data/data.ts";
|
||||
import { gutenberg } from "../data/data.ts";
|
||||
|
||||
async function main() {
|
||||
const args = await new Command().name("manual_proxy")
|
||||
|
@ -22,7 +23,8 @@ async function main() {
|
|||
{ default: "reducing" },
|
||||
).parse(Deno.args);
|
||||
|
||||
const init = initialize_prompt();
|
||||
const lines_ = new LineReader(Deno.stdin.readable);
|
||||
const init = await initialize_prompt(lines_);
|
||||
|
||||
let dict = Dict.from_lines(gutenberg, init.length);
|
||||
if (args.options.file !== undefined) dict = await Dict.from_text_file(args.options.file, init.length);
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
#!/usr/bin/env -S deno run --allow-read
|
||||
#!/usr/bin/env -S deno run -A --unstable-temporal
|
||||
|
||||
import { Command } from "https://deno.land/x/cliffy@v1.0.0-rc.4/command/mod.ts";
|
||||
import {
|
||||
report,
|
||||
section,
|
||||
startup,
|
||||
tip,
|
||||
top,
|
||||
} from "https://git.barnulf.net/mb/profiterole/raw/commit/02d19fce2a0878abd176801cf8f2a663f6db6c16/mod.ts";
|
||||
|
||||
import {
|
||||
Dict,
|
||||
|
@ -14,7 +21,7 @@ import {
|
|||
VerboseLogging,
|
||||
} from "./lib/lib.ts";
|
||||
|
||||
import { francais } from "../data/data.ts";
|
||||
import { gutenberg } from "../data/data.ts";
|
||||
import { last } from "./lib/utils.ts";
|
||||
|
||||
async function main() {
|
||||
|
@ -51,8 +58,10 @@ async function main() {
|
|||
.parse(Deno.args);
|
||||
const length = args.options.length ?? args.options.target?.length ?? 6;
|
||||
|
||||
let dict = Dict.from_lines(francais, length);
|
||||
tip("dict");
|
||||
let dict = Dict.from_lines(gutenberg, length);
|
||||
if (args.options.file !== undefined) dict = await Dict.from_text_file(args.options.file, length);
|
||||
top("dict");
|
||||
|
||||
const guesser = guessers.get(args.options.guesser)!(dict);
|
||||
let game = Simulator.from_dict_rand(dict);
|
||||
|
@ -64,6 +73,8 @@ async function main() {
|
|||
const result = last(await runner.play_all());
|
||||
if (result === undefined) Deno.exit(1);
|
||||
if (result.result.kind === "failure") Deno.exit(1);
|
||||
|
||||
report();
|
||||
}
|
||||
|
||||
function validate_target(target: string, length: number) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue