Add step tree dispaly.

This commit is contained in:
JOLIMAITRE Matthieu 2025-05-06 00:42:11 +02:00
parent 458f217c30
commit 4aab03fd98
5 changed files with 286 additions and 74 deletions

View file

@ -1,6 +1,7 @@
{ {
"fmt": { "fmt": {
"useTabs": true, "useTabs": true,
"lineWidth": 120 "lineWidth": 120,
} "semiColons": false
}
} }

11
example/failing Normal file
View file

@ -0,0 +1,11 @@
arbre
1 mort
mort
2 feur
feur
1 arbre
1 feur
1 barbe

48
example/mc-induction-cell Normal file
View file

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

View file

@ -1,79 +1,36 @@
#!/bin/env -S deno run --allow-read #!/bin/env -S deno run --allow-read
import { CircDepErr, RecipeSet, Step } from "./tree.ts"
async function main() { async function main() {
const { path } = parse_args(Deno.args); const { path } = parse_args(Deno.args)
const content = await Deno.readTextFile(path); const content = await Deno.readTextFile(path)
const { targets, definitions } = parse_file(content); const [targets, recipes] = RecipeSet.parse(content)
const ingredients = calculate_ingredients(targets, definitions); const steps = Step.resolve(targets, recipes)
const merged = merge_ingredients(ingredients); if (steps instanceof CircDepErr) return console.error("Circular recipe detected", steps.path)
console.log("To produce :\n", ...format_ingredients(targets), "\nGet :\n", ...format_ingredients(merged)); report_steps(steps)
} }
function parse_args(args: string[]) { function parse_args(args: string[]) {
const invalid_usage = () => Deno.exit(console.log("Usage:\nreciper <recipe_file>") as undefined); const invalid_usage = () => Deno.exit(console.log("Usage:\nreciper <recipe_file>") as undefined)
if (args.length !== 1) invalid_usage(); if (args.length !== 1) invalid_usage()
const [path] = args; const [path] = args
if (["-h", "--help"].includes(path)) invalid_usage(); if (["-h", "--help"].includes(path)) invalid_usage()
return { path }; return { path }
} }
function parse_file(content: string) { function report_steps(steps: Step) {
const lines = content.trim().split("\n"); console.log("Steps")
const definitions = [] as Definition[]; console.log()
const main = { target: "main", ingredients: [] } as Definition; console.log(steps.display())
let current_block = null as Definition | null; console.log()
for (const line of lines) { console.log("Intermediaries")
if (line.trim().startsWith("#")) continue; console.log()
if (line.trim() === "") { console.log(steps.intermediaries().map(([name, amount]) => `${amount}\t${name}`).join("\n"))
if (current_block !== null) definitions.push(current_block); console.log()
current_block = null; console.log("Inputs")
continue; console.log()
} console.log(steps.inputs().map(([name, amount]) => `${amount}\t${name}`).join("\n"))
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 };
} }
type Ingredient = { item: string; count: number }; if (import.meta.main) await main()
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<string, number>();
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();

195
src/tree.ts Normal file
View file

@ -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<string, Recipe>(),
) {}
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<string> {
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<K, V> {
public map = new Map<K, V>()
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
}
}