Compare commits

..

10 commits

11 changed files with 262 additions and 320 deletions

77
deno.lock generated Normal file
View file

@ -0,0 +1,77 @@
{
"version": "3",
"redirects": {
"https://deno.land/x/cliffy/command/mod.ts": "https://deno.land/x/cliffy@v1.0.0-rc.4/command/mod.ts"
},
"remote": {
"https://deno.land/std@0.221.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5",
"https://deno.land/std@0.221.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8",
"https://deno.land/std@0.221.0/console/_data.json": "cf2cc9d039a192b3adbfe64627167c7e6212704c888c25c769fc8f1709e1e1b8",
"https://deno.land/std@0.221.0/console/_run_length.ts": "7da8642a0f4f41ac27c0adb1364e18886be856c1d08c5cce6c6b5c00543c8722",
"https://deno.land/std@0.221.0/console/unicode_width.ts": "d92f085c0ab9c7ab171e4e7862dfd9d3a36ffd369939be5d3e1140ec58bc820f",
"https://deno.land/std@0.221.0/fmt/colors.ts": "d239d84620b921ea520125d778947881f62c50e78deef2657073840b8af9559a",
"https://deno.land/std@0.221.0/text/closest_string.ts": "8a91ee8b6d69ff96addcb7c251dad53b476ac8be9c756a0ef786abe9e13a93a5",
"https://deno.land/std@0.221.0/text/levenshtein_distance.ts": "24be5cc88326bbba83ca7c1ea89259af0050cffda2817ff3a6d240ad6495eae2",
"https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd",
"https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/_argument_types.ts": "ab269dacea2030f865a07c2a1e953ec437a64419a05bad1f1ddaab3f99752ead",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/_errors.ts": "d78e1b4d69d84b8b476b5f3c0b028e3906d48f21b8f1ca1d36d5abe9ccfe48bc",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/_spread.ts": "0cc6eb70a6df97b5d7d26008822d39f3e8a1232ee0a27f395aa19e68de738245",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/_type_utils.ts": "820004a59bc858e355b11f80e5b3ff1be2c87e66f31f53f253610170795602f0",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/_utils.ts": "fa0e88cc4215b18554a7308e8e2ae3a12be0fb91c54d1473c54c530dbd4adfcb",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/command.ts": "83cbece11c1459d5bc5add32c3cad0bf49e92c4ddd3ef00f22f80efdae30994e",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/_bash_completions_generator.ts": "0c6cb1df4d378d22f001155781d97a9c3519fd10c48187a198fef2cc63b0f84a",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/_fish_completions_generator.ts": "8ba4455f7f76a756e05c3db4ce35332b2951af65a2891f2750b530e06880f495",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/_zsh_completions_generator.ts": "9df79fbac17a32b9645d01628c41a2bfd295d7976b87b0ae235f50a9c8975fbc",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/bash.ts": "53fe78994eb2359110dc4fa79235bdd86800a38c1d6b1c4fe673c81756f3a0e2",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/complete.ts": "58df61caa5e6220ff2768636a69337923ad9d4b8c1932aeb27165081c4d07d8b",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/completions_command.ts": "506f97f1c6b0b1c3e9956e5069070028b818942310600d4157f64c9b644d3c49",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/fish.ts": "6f0b44b4067740b2931c9ec8863b6619b1d3410fea0c5a3988525a4c53059197",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/mod.ts": "8dda715ca25f3f66d5ec232b76d7c9a96dd4c64b5029feff91738cc0c9586fb1",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/zsh.ts": "f1263c3946975e090d4aadc8681db811d86b52a8ae680f246e03248025885c21",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/deprecated.ts": "bbe6670f1d645b773d04b725b8b8e7814c862c9f1afba460c4d599ffe9d4983c",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/deps.ts": "a58ea2fa4e2ed9b39bb8dd8c35dd0498c74f05392517ff230a9a4d04c4c766b7",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/help/_help_generator.ts": "98619da83ff25523280a6fdcad89af3f13a6fafefc81b71f8230f3344b5ff2c5",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/help/help_command.ts": "fbbf0c0827dd21d3cec7bcc68c00c20b55f53e2b621032891b9d23ac4191231c",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/help/mod.ts": "8369b292761dcc9ddaf41f2d34bfb06fb6800b69efe80da4fc9752c3b890275b",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/mod.ts": "4b708df1b97152522bee0e3828f06abbbc1d2250168910e5cf454950d7b7404b",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/type.ts": "f588f5d9635b79100044e62aced4b00e510e75b83801f9b089c40c2d98674de2",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/types.ts": "bc9ff7459b9cc1079eeb95ff101690a51b4b4afa4af5623340076ee361d08dbb",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/action_list.ts": "33c98d449617c7a563a535c9ceb3741bde9f6363353fd492f90a74570c611c27",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/boolean.ts": "3879ec16092b4b5b1a0acb8675f8c9250c0b8a972e1e4c7adfba8335bd2263ed",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/child_command.ts": "f1fca390c7fbfa7a713ca15ef55c2c7656bcbb394d50e8ef54085bdf6dc22559",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/command.ts": "325d0382e383b725fd8d0ef34ebaeae082c5b76a1f6f2e843fee5dbb1a4fe3ac",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/enum.ts": "8a7cd2898e03089234083bb78c8b1d9b7172254c53c32d4710321638165a48ec",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/file.ts": "8618f16ac9015c8589cbd946b3de1988cc4899b90ea251f3325c93c46745140e",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/integer.ts": "29864725fd48738579d18123d7ee78fed37515e6dc62146c7544c98a82f1778d",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/number.ts": "aeba96e6f470309317a16b308c82e0e4138a830ec79c9877e4622c682012bc1f",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/string.ts": "e4dadb08a11795474871c7967beab954593813bb53d9f69ea5f9b734e43dc0e0",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/_check_version.ts": "6cfa7dc26bc0dc46381500e8d4b130fb224f4c5456152dada15bd3793edca89b",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/mod.ts": "4eff69c489467be17dea27fb95a795396111ee385d170ac0cbcc82f0ea38156c",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/provider.ts": "c23253334097dc4b8a147ccdeb3aa44f5a95aa953a6386cb5396f830d95d77a5",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/provider/deno_land.ts": "24f8d82e38c51e09be989f30f8ad21f9dd41ac1bb1973b443a13883e8ba06d6d",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/provider/github.ts": "99e1b133dd446c6aa79f69e69c46eb8bc1c968dd331c2a7d4064514a317c7b59",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/provider/nest_land.ts": "0e07936cea04fa41ac9297f32d87f39152ea873970c54cb5b4934b12fee1885e",
"https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/upgrade_command.ts": "27191f4b1ce93581b6d5ee2fff6003fe4fca437f476ecb98b6eae92f2b4d0716",
"https://deno.land/x/cliffy@v1.0.0-rc.4/flags/_errors.ts": "f1fbb6bfa009e7950508c9d491cfb4a5551027d9f453389606adb3f2327d048f",
"https://deno.land/x/cliffy@v1.0.0-rc.4/flags/_utils.ts": "25e519ce1f35acc8b43c75d1ca1c4ab591e7dab08327b7b408705b591e27d8bd",
"https://deno.land/x/cliffy@v1.0.0-rc.4/flags/_validate_flags.ts": "e60b9038c0136ab7e6bd1baf0e993a07bf23f18afbfb6e12c59adf665a622957",
"https://deno.land/x/cliffy@v1.0.0-rc.4/flags/deprecated.ts": "a72a35de3cc7314e5ebea605ca23d08385b218ef171c32a3f135fb4318b08126",
"https://deno.land/x/cliffy@v1.0.0-rc.4/flags/deps.ts": "bed26afff36eeb25509440edec9d5d141b3411e08cc7a90e38a370969b5166bb",
"https://deno.land/x/cliffy@v1.0.0-rc.4/flags/flags.ts": "3e62c4a9756b5705aada29e7e94847001356b3a83cd18ad56f4207387a71cf51",
"https://deno.land/x/cliffy@v1.0.0-rc.4/flags/types.ts": "9e2f75edff2217d972fc711a21676a59dfd88378da2f1ace440ea84c07db1dcc",
"https://deno.land/x/cliffy@v1.0.0-rc.4/flags/types/boolean.ts": "4c026dd66ec9c5436860dc6d0241427bdb8d8e07337ad71b33c08193428a2236",
"https://deno.land/x/cliffy@v1.0.0-rc.4/flags/types/integer.ts": "b60d4d590f309ddddf066782d43e4dc3799f0e7d08e5ede7dc62a5ee94b9a6d9",
"https://deno.land/x/cliffy@v1.0.0-rc.4/flags/types/number.ts": "610936e2d29de7c8c304b65489a75ebae17b005c6122c24e791fbed12444d51e",
"https://deno.land/x/cliffy@v1.0.0-rc.4/flags/types/string.ts": "e89b6a5ce322f65a894edecdc48b44956ec246a1d881f03e97bbda90dd8638c5",
"https://deno.land/x/cliffy@v1.0.0-rc.4/table/_layout.ts": "73a9bcb8a87b3a6817c4c9d2a31a21b874a7dd690ade1c64c9a1f066d628d626",
"https://deno.land/x/cliffy@v1.0.0-rc.4/table/_utils.ts": "13390db3f11977b7a4fc1202fa8386be14696b475a7f46a65178354f9a6640b7",
"https://deno.land/x/cliffy@v1.0.0-rc.4/table/border.ts": "5c6e9ef5078c6930169aacb668b274bdbb498461c724a7693ac9270fe9d3f5d5",
"https://deno.land/x/cliffy@v1.0.0-rc.4/table/cell.ts": "65e3ee699c3cebeb4d4d44e8f156e37a8532a0f317359d73178a95724d3f9267",
"https://deno.land/x/cliffy@v1.0.0-rc.4/table/column.ts": "cf14009f2cb14bad156f879946186c1893acdc6a2fee6845db152edddb6a2714",
"https://deno.land/x/cliffy@v1.0.0-rc.4/table/consume_words.ts": "369d065dbf7f15c664ea8523e0ef750fb952aea6d88e146c375e64aec9503052",
"https://deno.land/x/cliffy@v1.0.0-rc.4/table/deps.ts": "cbb896e8d7a6b5e3c2b9dda7d16638c202d9b46eb738c2dae1fa9480d8091486",
"https://deno.land/x/cliffy@v1.0.0-rc.4/table/row.ts": "79eb1468aafdd951e5963898cdafe0752d4ab4c519d5f847f3d8ecb8fe857d4f",
"https://deno.land/x/cliffy@v1.0.0-rc.4/table/table.ts": "298671e72e61f1ab18b42ae36643181993f79e29b39dc411fdc6ffd53aa04684"
}
}

245
main.ts
View file

@ -1,245 +0,0 @@
#!/usr/bin/env -S deno run --allow-read
import { assertExists } from "https://deno.land/std@0.224.0/assert/assert_exists.ts";
async function main() {
const length = 6;
const dict = await Dict.from_file("./data/liste_francais.txt", length);
console.log(dict);
const history = new History(dict);
const game = Game.from_dict_rand(dict);
console.log("Target is", game.word);
let index = 0;
while (true) {
index += 1;
const found = await history.next_guess((guess) => {
console.log("Guessing", guess);
const result = game.try_guess(guess);
console.log(format_result(result));
return result;
});
if (found !== undefined) {
console.log("Found", found, "in", index);
break;
}
await wait(500);
}
}
type GuessResult = { kind: "success" } | { kind: "failure"; informations: Info[] };
function format_result(result: GuessResult) {
if (result.kind === "success") return `success`;
let line = "failure: ";
for (const info of result.informations) line += info.kind === "abscent" ? "-" : info.kind === "somewhere" ? "+" : "#";
return line;
}
class Dict {
words;
letters;
length;
constructor(words: Set<string>, length: number) {
this.words = words;
this.length = length;
this.letters = new Set([...words.values()].map((w) => w.split("")).flat());
}
static async from_file(path: string, length: number) {
const words = new Set<string>();
const content = await Deno.readTextFile(path);
for (const word of content.split("\n")) {
const word_ = word.trim().toLowerCase();
if (word_.length !== length) continue;
for (const forbidden of [" ", "-", "."]) if (word_.includes(forbidden)) continue;
words.add(remove_accent(word_));
}
return new Dict(words, length);
}
[Symbol.for("Deno.customInspect")]() {
return `Dict { ${this.words.size} words }`;
}
}
type Knowledge = {
letter: string;
at: Set<number>;
not_at: Set<number>;
};
class History {
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() });
}
}
async next_guess(operation: (guess: string) => GuessResult | Promise<GuessResult>) {
const res = this.expected_result();
console.log("knows: ", res.known);
if (res.completed) return operation(res.known);
const words = [...this.dict.words.values()];
const scored = words.map((word) => [this.info_for(word), word] as const);
const sorted = scored.toSorted(([sa], [sb]) => sb - sa);
const [_score, guess] = sorted[0];
const result = await operation(guess);
if (result.kind === "success") return guess;
for (const [letter, new_info] of zip(guess, result.informations)) {
//
}
}
learn_letter_at(letter: string, at: number) {
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);
}
}
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_for(word: string) {
let total = 0;
for (const [index, letter] of enumerate(word)) {
const information = this.informations.get(letter);
if (information === undefined) {
total += 1;
continue;
}
if ()
if (information.kind === "abscent") {
continue;
}
if (information.kind === "present") {
const known_pos = information.not_at.has(index);
if (!known_pos) total += this.length; // note : not 1 to priorize locating known letters.
continue;
}
}
return total;
}
expected_result() {
const known_arr = [...range(0, this.length)].map(() => null as string | null);
for (const [letter, info] of this.informations.entries()) {
if (info.kind !== "known") continue;
for (const i of info.at.values()) known_arr[i] = letter;
}
const known = known_arr.map((l) => l === null ? "." : l).join("");
if (known.includes(".")) return { completed: false as const, known };
const guess = known_arr as string[];
return { completed: true as const, known, guess };
}
}
type Info = { kind: "abscent" } | { kind: "somewhere" } | { kind: "there" };
function remove_accent(text: string) {
const accents = [
["à", "a"],
["â", "a"],
["ä", "a"],
["é", "e"],
["è", "e"],
["ê", "e"],
["ë", "e"],
["î", "i"],
["ï", "i"],
["ô", "o"],
["ö", "o"],
["û", "u"],
];
let result = text;
for (const [accent, alternative] of accents) result = result.replaceAll(accent, alternative);
return result;
}
function* enumerate<T>(iterator: Iterable<T>) {
let index = 0;
for (const item of iterator) yield [index++, item] as const;
}
function* zip<A, B>(iterable_a: Iterable<A>, iterable_b: Iterable<B>) {
const iter_a = iterable_a[Symbol.iterator]();
const iter_b = iterable_b[Symbol.iterator]();
while (true) {
const next_a = iter_a.next().value;
const next_b = iter_b.next().value;
if (next_a === undefined) return;
if (next_b === undefined) return;
yield [next_a as A, next_b as B] as const;
}
}
class Game {
word;
constructor(word: string) {
this.word = word;
}
static from_dict_rand(dict: Dict) {
const index = Math.floor(Math.random() * dict.words.size);
const word = [...dict.words.values()][index];
return new Game(word);
}
try_guess(guess: string): GuessResult {
if (guess === this.word) return { kind: "success" };
const rest_actual = [...this.word].map((letter) => letter as (string | null));
const rest_guess = [...guess].map((letter) => letter as (string | null));
const info = [...range(0, this.word.length)].map(() => null) as (Info | null)[];
for (const [index, [guessed, actual]] of enumerate(zip(rest_guess, rest_actual))) {
if (guessed !== actual) continue;
rest_actual[index] = null;
rest_guess[index] = null;
info[index] = { kind: "known", at: new Set([index]) };
}
for (const [index, guessed] of enumerate(rest_guess)) {
if (guessed === null) continue;
if (!rest_actual.includes(guessed)) continue;
info[index] = { kind: "present", not_at: new Set([index]) };
rest_guess[index] = null;
// note : removes only one.
rest_actual[rest_actual.indexOf(guessed)] = null;
}
const informations = info.map((i) => i != undefined ? i : ({ kind: "abscent" }) as Info);
return { kind: "failure", informations };
}
}
function* range(from: number, to: number) {
while (from < to) yield from++;
}
async function wait(ms: number) {
await new Promise((resolver) => setTimeout(resolver, ms));
}
if (import.meta.main) await main();

View file

@ -1,5 +1,3 @@
import { remove_accent } from "./utils.ts";
export class Dict { export class Dict {
words; words;
letters; letters;
@ -17,8 +15,8 @@ export class Dict {
for (const word of content.split("\n")) { for (const word of content.split("\n")) {
const word_ = word.trim().toLowerCase(); const word_ = word.trim().toLowerCase();
if (word_.length !== length) continue; if (word_.length !== length) continue;
for (const forbidden of [" ", "-", "."]) if (word_.includes(forbidden)) continue; if (contains_any(word_, [" ", "-", "."])) continue;
words.add(remove_accent(word_)); words.add(remove_accents(word_));
} }
return new Dict(words, length); return new Dict(words, length);
} }
@ -27,3 +25,22 @@ export class Dict {
return `Dict { ${this.words.size} words }`; return `Dict { ${this.words.size} words }`;
} }
} }
export function contains_any(text: string, words: string[]) {
for (const word of words) if (text.includes(word)) return true;
return false;
}
export function remove_accents(text: string) {
const accents = [
...[["à", "a"], ["â", "a"], ["ä", "a"]],
...[["ç", "c"]],
...[["é", "e"], ["è", "e"], ["ê", "e"], ["ë", "e"]],
...[["î", "i"], ["ï", "i"]],
...[["ô", "o"], ["ö", "o"]],
...[["û", "u"]],
];
let result = text;
for (const [accent, alternative] of accents) result = result.replaceAll(accent, alternative);
return result;
}

View file

@ -1,3 +1,10 @@
import { Awaitable } from "../utils.ts";
export type Info = { kind: "abscent" } | { kind: "somewhere" } | { kind: "there" }; export type Info = { kind: "abscent" } | { kind: "somewhere" } | { kind: "there" };
export type GuessResult = { kind: "success" } | { kind: "failure"; informations: Info[] }; export type GuessResult = { kind: "success" } | { kind: "failure"; informations: Info[] };
export interface Gaming {
length(): number;
guess(guess: string, known: string): Awaitable<GuessResult>;
}

View file

@ -1,8 +1,8 @@
import { Dict } from "../dict.ts"; import { Dict } from "../dict.ts";
import { enumerate, range, zip } from "../utils.ts"; import { enumerate, range, zip } from "../utils.ts";
import { GuessResult, Info } from "./game.ts"; import { Gaming, GuessResult, Info } from "./game.ts";
export class Simulator { export class Simulator implements Gaming {
word; word;
constructor(word: string) { constructor(word: string) {
@ -15,10 +15,10 @@ export class Simulator {
return new Simulator(word); return new Simulator(word);
} }
try_guess(guess: 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 rest_actual = [...this.word].map((letter) => letter as (string | null));
const rest_guess = [...guess].map((letter) => letter as (string | null)); const rest_guess = [...guess_].map((letter) => letter as (string | null));
const info = [...range(0, this.word.length)].map(() => null) as (Info | null)[]; const info = [...range(0, this.word.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))) {
@ -40,4 +40,8 @@ export class Simulator {
const informations = info.map((i) => i != undefined ? i : ({ kind: "abscent" }) as Info); const informations = info.map((i) => i != undefined ? i : ({ kind: "abscent" }) as Info);
return { kind: "failure", informations }; return { kind: "failure", informations };
} }
length(): number {
return this.word.length;
}
} }

View file

@ -1,9 +1,13 @@
import { assertExists } from "https://deno.land/std@0.224.0/assert/assert_exists.ts"; import { assertExists } from "https://deno.land/std@0.224.0/assert/assert_exists.ts";
import { enumerate, range } from "./utils.ts"; import { Awaitable, enumerate, range } from "./utils.ts";
import { Dict } from "./dict.ts"; import { Dict } from "./dict.ts";
import { GuessResult } from "./game/game.ts"; import { GuessResult } from "./game/game.ts";
export interface Guessing {
guess(try_: (guess: string, known: string) => Awaitable<GuessResult>): Promise<GuessResult | null>;
}
type Knowledge = { type Knowledge = {
letter: string; letter: string;
at: Set<number>; at: Set<number>;
@ -11,7 +15,7 @@ type Knowledge = {
not_at: Set<number>; not_at: Set<number>;
}; };
export class Gueser { export class Guesser implements Guessing {
length; length;
dict; dict;
informations; informations;
@ -25,9 +29,9 @@ export class Gueser {
} }
} }
async next_guess(operation: (guess: string, known: string) => GuessResult | Promise<GuessResult>) { async guess(try_: (guess: string, known: string) => Awaitable<GuessResult>) {
const res = this.expected_result(); const res = this.expected_result();
if (res.completed) return operation(res.result, res.result); if (res.completed) return await try_(res.result, res.result);
const words = [...this.dict.words.values()]; const words = [...this.dict.words.values()];
const scored = words.map((word) => [this.info_score_for(word), word] as const); const scored = words.map((word) => [this.info_score_for(word), word] as const);
@ -35,8 +39,8 @@ export class Gueser {
const bests = scored.filter(([s]) => s === best_score).map(([_, word]) => word); const bests = scored.filter(([s]) => s === best_score).map(([_, word]) => word);
const guess = pick_random_common_word(bests); const guess = pick_random_common_word(bests);
const result = await operation(guess, res.result); const result = await try_(guess, res.result);
if (result.kind === "success") return guess; if (result.kind === "success") return result;
for (const [index, info] of enumerate(result.informations)) { for (const [index, info] of enumerate(result.informations)) {
const letter = guess[index]; const letter = guess[index];
if (info.kind === "there") this.learn_letter_at(letter, index); if (info.kind === "there") this.learn_letter_at(letter, index);
@ -46,6 +50,7 @@ export class Gueser {
this.learn_letter_not_at(letter, index); this.learn_letter_not_at(letter, index);
} }
} }
return null;
} }
learn_does_not_exist(letter: string) { learn_does_not_exist(letter: string) {

View file

@ -1,3 +1,4 @@
export { Dict } from "./dict.ts"; export { Dict } from "./dict.ts";
export { Gueser } from "./guesser.ts"; export { Guesser } from "./guesser.ts";
export { Simulator } from "./game/simulator.ts"; export { Simulator } from "./game/simulator.ts";
export { Runner } from "./runner.ts";

91
src/lib/runner.ts Normal file
View file

@ -0,0 +1,91 @@
import { assertExists } from "https://deno.land/std@0.224.0/assert/assert_exists.ts";
import { Guessing } from "./guesser.ts";
import { Gaming, GuessResult } from "./game/game.ts";
import { last, wait } from "./utils.ts";
export class Runner<Ga extends Gaming, Gu extends Guessing> {
game;
guesser;
delay_ms;
turns;
logging;
constructor(game: Ga, guesser: Gu, logging: LoggingStrategy, delay_ms = 0) {
this.game = game;
this.guesser = guesser;
this.delay_ms = delay_ms;
this.turns = [] as Turn[];
this.logging = logging;
}
async play_all() {
this.logging.on_start(this.game.length());
while (true) {
const result = await this.guesser.guess(async (guess, known) => {
const result = await this.game.guess(guess, known);
this.turns.push({ guess, result });
this.logging.on_guess(known, guess, result);
return result;
});
if (result !== null) break;
await wait(this.delay_ms);
}
this.logging.on_finish(this.turns);
}
}
type Turn = { guess: string; result: GuessResult };
function format_result(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;
}
interface LoggingStrategy {
on_start(length: number): void;
on_guess(known: string, guess: string, result: GuessResult): void;
on_finish(turns: Turn[]): void;
}
export class VerboseLogging implements LoggingStrategy {
on_start(length: number) {}
on_guess(known: string, guess: string, result: GuessResult): void {
console.log(" Knows:", known);
console.log("Guessing:", guess);
console.log(" Got:", format_result(result));
console.log();
}
on_finish(turns: Turn[]): void {
const last_ = last(turns);
assertExists(last_);
console.log("Stopped on", last_.guess, ".");
console.log("With", last_.result.kind, ".");
console.log("In", turns.length, "steps");
}
}
export class TableLogging implements LoggingStrategy {
on_start(length: number): void {
const pad = (title: string) => title.padEnd(length);
console.log(`${pad("known")}\t${pad("guess")}\t${pad("result")}`);
console.log();
}
on_guess(known: string, guess: string, result: GuessResult): void {
console.log(`${known}\t${guess}\t${format_result(result)}`);
}
on_finish(turns: Turn[]): void {
const last_ = last(turns);
assertExists(last_);
console.log();
console.log(last_.result.kind, "in", turns.length, "steps");
}
}

View file

@ -23,23 +23,10 @@ export async function wait(ms: number) {
await new Promise((resolver) => setTimeout(resolver, ms)); await new Promise((resolver) => setTimeout(resolver, ms));
} }
export function remove_accent(text: string) { export type Awaitable<T> = T | Promise<T>;
const accents = [
["à", "a"], export function last<T>(iterable: Iterable<T>) {
["â", "a"], let last = undefined as T | undefined;
["ä", "a"], for (const item of iterable) last = item;
["ç", "c"], return last;
["é", "e"],
["è", "e"],
["ê", "e"],
["ë", "e"],
["î", "i"],
["ï", "i"],
["ô", "o"],
["ö", "o"],
["û", "u"],
];
let result = text;
for (const [accent, alternative] of accents) result = result.replaceAll(accent, alternative);
return result;
} }

View file

@ -1,40 +0,0 @@
#!/usr/bin/env -S deno run --allow-read
import { GuessResult } from "./lib/game/game.ts";
import { Dict, Gueser, Simulator } from "./lib/lib.ts";
import { wait } from "./lib/utils.ts";
async function main() {
const length = 6;
const dict = await Dict.from_file("./data/liste_francais.txt", length);
console.log(dict);
const guesser = new Gueser(dict);
const game = Simulator.from_dict_rand(dict);
console.log("Target is", game.word);
let index = 0;
while (true) {
console.log();
index += 1;
const found = await guesser.next_guess((guess, known) => {
console.log("knows: ", known);
console.log("Guessing:", guess);
const result = game.try_guess(guess);
console.log(format_result(result));
return result;
});
if (found !== undefined) {
console.log("Found", found, "in", index);
break;
}
await wait(500);
}
}
function format_result(result: GuessResult) {
if (result.kind === "success") return `success`;
let line = "failure: ";
for (const info of result.informations) line += info.kind === "abscent" ? "-" : info.kind === "somewhere" ? "+" : "#";
return line;
}
if (import.meta.main) await main();

38
src/simulation.ts Executable file
View file

@ -0,0 +1,38 @@
#!/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, Guesser, Runner, Simulator } from "./lib/lib.ts";
import { TableLogging } from "./lib/runner.ts";
async function main() {
const args = await new Command().name("simulation")
.description(
"Program to simulate TUSMO game with guesser controller.",
)
.option(
"-d, --dictionnary <path:string>",
"Sets dictionnary to use words from.",
{ default: "./data/liste_francais.txt" },
).option(
"-n, --length <length:number>",
"Length of the word to use from the dictionnary.",
{ default: 6 },
).option(
"-w, --wait <wait:number>",
"Time to wait between guesses, in ms.",
{ default: 500 },
).parse(Deno.args);
const dict = await Dict.from_file(args.options.dictionnary, args.options.length);
const guesser = new Guesser(dict);
const game = Simulator.from_dict_rand(dict);
console.log("Target is", game.word);
console.log();
const runner = new Runner(game, guesser, new TableLogging(), args.options.wait);
await runner.play_all();
}
if (import.meta.main) await main();