From 4aab03fd9828259da600a7611a7991a7bb4664cb Mon Sep 17 00:00:00 2001 From: JOLIMAITRE Matthieu Date: Tue, 6 May 2025 00:42:11 +0200 Subject: [PATCH] Add step tree dispaly. --- deno.json | 11 ++- example/failing | 11 +++ example/mc-induction-cell | 48 ++++++++++ src/main.ts | 95 +++++-------------- src/tree.ts | 195 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 286 insertions(+), 74 deletions(-) create mode 100644 example/failing create mode 100644 example/mc-induction-cell create mode 100644 src/tree.ts diff --git a/deno.json b/deno.json index 38d5831..c6c6351 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,7 @@ { - "fmt": { - "useTabs": true, - "lineWidth": 120 - } -} \ No newline at end of file + "fmt": { + "useTabs": true, + "lineWidth": 120, + "semiColons": false + } +} diff --git a/example/failing b/example/failing new file mode 100644 index 0000000..4bdc4d7 --- /dev/null +++ b/example/failing @@ -0,0 +1,11 @@ +arbre +1 mort + +mort +2 feur + +feur +1 arbre + +1 feur +1 barbe \ No newline at end of file diff --git a/example/mc-induction-cell b/example/mc-induction-cell new file mode 100644 index 0000000..52f09d0 --- /dev/null +++ b/example/mc-induction-cell @@ -0,0 +1,48 @@ +1 elite induction cell + +elite induction cell +1 elite energy cube +4 advanced induction cell +4 energy tablet + +advanced induction cell +1 advanced energy cube +4 basic induction cell +4 energy tablet + +basic induction cell +1 basic energy cube +4 lithium dust +4 energy tablet + +elite energy cube +1 advanced energy cube +4 reinforced alloy +2 gold ingot +2 energy tablet + +advanced energy cube +1 basic energy cube +4 enriched alloy +2 osmium ingot +2 energy tablet + +basic energy cube +1 steel casing +4 redstrone dust +2 iron ingot +2 energy tablet + +steel casing +1 osmium ingot +4 steel ingot +4 glass block + +energy tablet +3 gold ingot +2 enriched alloy +4 redstrone dust + +reinforced alloy +1 enriched alloy +0.125 diamond \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 7bceb42..b49c17c 100755 --- a/src/main.ts +++ b/src/main.ts @@ -1,79 +1,36 @@ #!/bin/env -S deno run --allow-read +import { CircDepErr, RecipeSet, Step } from "./tree.ts" + async function main() { - const { path } = parse_args(Deno.args); - const content = await Deno.readTextFile(path); - const { targets, definitions } = parse_file(content); - const ingredients = calculate_ingredients(targets, definitions); - const merged = merge_ingredients(ingredients); - console.log("To produce :\n", ...format_ingredients(targets), "\nGet :\n", ...format_ingredients(merged)); + const { path } = parse_args(Deno.args) + const content = await Deno.readTextFile(path) + const [targets, recipes] = RecipeSet.parse(content) + const steps = Step.resolve(targets, recipes) + if (steps instanceof CircDepErr) return console.error("Circular recipe detected", steps.path) + report_steps(steps) } function parse_args(args: string[]) { - const invalid_usage = () => Deno.exit(console.log("Usage:\nreciper ") as undefined); - if (args.length !== 1) invalid_usage(); - const [path] = args; - if (["-h", "--help"].includes(path)) invalid_usage(); - return { path }; + const invalid_usage = () => Deno.exit(console.log("Usage:\nreciper ") as undefined) + if (args.length !== 1) invalid_usage() + const [path] = args + if (["-h", "--help"].includes(path)) invalid_usage() + return { path } } -function parse_file(content: string) { - const lines = content.trim().split("\n"); - const definitions = [] as Definition[]; - const main = { target: "main", ingredients: [] } as Definition; - let current_block = null as Definition | null; - for (const line of lines) { - if (line.trim().startsWith("#")) continue; - if (line.trim() === "") { - if (current_block !== null) definitions.push(current_block); - current_block = null; - continue; - } - const [count_str, ...words] = line.split(" "); - const count = parseFloat(count_str); - if (isNaN(count)) { - if (current_block !== null) definitions.push(current_block); - current_block = null; - current_block = { target: line, ingredients: [] }; - } else { - const item = words.join(" "); - if (current_block === null) main.ingredients.push({ item, count }); - else current_block.ingredients.push({ item, count }); - } - } - if (current_block !== null) definitions.push(current_block); - const targets = main.ingredients; - return { targets, definitions }; +function report_steps(steps: Step) { + console.log("Steps") + console.log() + console.log(steps.display()) + console.log() + console.log("Intermediaries") + console.log() + console.log(steps.intermediaries().map(([name, amount]) => `${amount}\t${name}`).join("\n")) + console.log() + console.log("Inputs") + console.log() + console.log(steps.inputs().map(([name, amount]) => `${amount}\t${name}`).join("\n")) } -type Ingredient = { item: string; count: number }; -type Definition = { target: string; ingredients: Ingredient[] }; - -function calculate_ingredients(targets: Ingredient[], definitions: Definition[]) { - const result = [] as Ingredient[]; - for (const target of targets) { - const recipe = definitions.find((recipe) => recipe.target === target.item); - if (recipe === undefined) { - result.push(target); - continue; - } - const defs = definitions.filter((def) => def.target !== target.item); - for (const ingredient of calculate_ingredients(recipe.ingredients, defs)) { - const count = ingredient.count * target.count; - result.push({ item: ingredient.item, count }); - } - } - return result; -} - -function merge_ingredients(ingredients: Ingredient[]): Ingredient[] { - const set = new Map(); - for (const { count, item } of ingredients) set.set(item, count + (set.get(item) ?? 0)); - return Array.from(set.entries()).map(([item, count]) => ({ item, count })); -} - -function* format_ingredients(ingredients: Ingredient[]) { - for (const { count, item } of ingredients) yield* ["-", count, item, "\n"]; -} - -if (import.meta.main) await main(); +if (import.meta.main) await main() diff --git a/src/tree.ts b/src/tree.ts new file mode 100644 index 0000000..47766df --- /dev/null +++ b/src/tree.ts @@ -0,0 +1,195 @@ +import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts" + +export class Ingredient { + public constructor( + public name: string, + public amount: number, + ) {} +} + +export class Recipe { + public constructor( + public name: string, + public ingredients: Ingredient[], + ) {} +} + +export class RecipeSet { + public constructor( + public recipes = new Map(), + ) {} + + public append(recipe: Recipe) { + this.recipes.set(recipe.name, recipe) + } + + public static parse(text: string) { + const main = new Recipe("main", []) + const result = new RecipeSet() + let current_block = null as Recipe | null + function append_current() { + if (current_block !== null) result.append(current_block) + current_block = null + } + for (const line of text.split("\n")) { + if (line.trim().startsWith("#")) continue + if (line.trim() === "") { + append_current() + continue + } + const [count_str, ...words] = line.split(" ") + const count = parseFloat(count_str) + if (isNaN(count)) { + append_current() + current_block = new Recipe(line, []) + } else { + const ingredient = new Ingredient(words.join(" "), count) + if (current_block === null) main.ingredients.push(ingredient) + else current_block.ingredients.push(ingredient) + } + } + append_current() + return [main, result] as const + } +} + +Deno.test("test_recipe_set_parse", () => { + const [main, recipe_set] = RecipeSet.parse(` +a +1 b + +b +2 c + +2 a +1 c +`) + assertEquals( + recipe_set, + new RecipeSet( + new Map([ + ["a", new Recipe("a", [new Ingredient("b", 1)])], + ["b", new Recipe("b", [new Ingredient("c", 2)])], + ]), + ), + ) + + assertEquals(main, new Recipe("main", [new Ingredient("a", 2), new Ingredient("c", 1)])) +}) + +export class Step { + public constructor( + public name: string, + public amount: number, + public deps: Step[], + ) {} + + public static resolve(root: Recipe, recipes: RecipeSet): Step | CircDepErr { + function recurse(recipe: Recipe, multiplier: number, path: string[]): Step | CircDepErr { + const next_parents = [...path, recipe.name] + const steps = [] as Step[] + for (const ingr of recipe.ingredients) { + if (next_parents.includes(ingr.name)) return new CircDepErr(next_parents) + const ingr_multiplier = ingr.amount * multiplier + const ingr_recipe = recipes.recipes.get(ingr.name) + if (ingr_recipe === undefined) { + steps.push(new Step(ingr.name, ingr_multiplier, [])) + continue + } + const res = recurse(ingr_recipe, ingr_multiplier, next_parents) + if (res instanceof CircDepErr) return res + steps.push(res) + } + return new Step(recipe.name, multiplier, steps) + } + return recurse(root, 1, []) + } + + public display() { + function* rec(step: Step): Generator { + yield `${step.amount} ${step.name}` + const deps = [...step.deps] + if (deps.length < 1) return + const [last] = deps.splice(deps.length - 1, 1) + for (const dep of deps) { + const [first, ...lines] = rec(dep) + yield `├─${first}` + for (const line of lines) yield `│ ${line}` + } + const [first, ...lines] = rec(last) + yield `└─${first}` + for (const line of lines) yield ` ${line}` + } + return [...this.deps.map(rec).map((l) => l.toArray())].flat().join("\n") + } + + public intermediaries() { + function* rec(step: Step): Generator<[string, number]> { + if (step.deps.length > 0) yield [step.name, step.amount] + for (const dep of step.deps) yield* rec(dep) + } + const result = new DefaultMap((_: string) => ({ total: 0 })) + for (const dep of this.deps) for (const [name, value] of rec(dep)) result.get(name).total += value + return result.map.entries() + .map(([k, { total }]) => [k, total] as const) + .toArray() + .toSorted(([a], [b]) => a.localeCompare(b)) + } + + public inputs() { + function* rec(step: Step): Generator<[string, number]> { + if (step.deps.length < 1) yield [step.name, step.amount] + for (const dep of step.deps) yield* rec(dep) + } + const result = new DefaultMap((_: string) => ({ total: 0 })) + for (const dep of this.deps) for (const [name, value] of rec(dep)) result.get(name).total += value + return result.map.entries() + .map(([k, { total }]) => [k, total] as const) + .toArray() + .toSorted(([a], [b]) => a.localeCompare(b)) + } +} + +export class CircDepErr { + public constructor( + public path: string[], + ) {} +} + +Deno.test("test_display_lines", () => { + const [main, recipe_set] = RecipeSet.parse(` +a +1 b +1 c + +b +2 c + +2 a +1 c + `.trim()) + const root = Step.resolve(main, recipe_set) + if (root instanceof CircDepErr) throw new Error() + const expects = ` +1 main +├─2 a +│ ├─2 b +│ │ └─4 c +│ └─2 c +└─1 c + `.trim() + assertEquals(root.display(), expects) +}) + +export class DefaultMap { + public map = new Map() + + constructor( + public default_construct: (key: K) => V, + ) {} + + get(key: K) { + if (!this.map.has(key)) this.map.set(key, this.default_construct(key)) + return this.map.get(key) as V + } +}