commit 4a7adf10062871c319566147acedbaf7f9be8dab Author: ZareMate <0.zaremate@gmail.com> Date: Mon Mar 23 15:35:09 2026 +0100 Add Kelp Profit Calculator with GUI and CLI support - Implemented a comprehensive profit calculator for kelp processing, including smelting and crafting scenarios. - Added a graphical user interface using Tkinter for user-friendly input and output. - Included detailed calculations for profit, fuel costs, and processing times. - Supported various input formats for kelp amounts and prices, including suffixes (k/m/b) and shulker counts (s/ks/ms/bs). - Provided formatted output for results, including profit breakdowns and useful metrics. diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c9ebf2d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python-envs.defaultEnvManager": "ms-python.python:system" +} \ No newline at end of file diff --git a/__pycache__/kelp_profit_calculator.cpython-314.pyc b/__pycache__/kelp_profit_calculator.cpython-314.pyc new file mode 100644 index 0000000..6a3ea84 Binary files /dev/null and b/__pycache__/kelp_profit_calculator.cpython-314.pyc differ diff --git a/kelp_profit_calculator.py b/kelp_profit_calculator.py new file mode 100644 index 0000000..e45ff31 --- /dev/null +++ b/kelp_profit_calculator.py @@ -0,0 +1,555 @@ +import math + +try: + import tkinter as tk + from tkinter import messagebox + from tkinter import ttk + TK_AVAILABLE = True + TK_IMPORT_ERROR = None +except Exception as exc: + tk = None + messagebox = None + ttk = None + TK_AVAILABLE = False + TK_IMPORT_ERROR = exc + +# ========================= +# EDIT VALUES HERE (CONFIG) +# ========================= + +KELP_PRICE = 61.6 # per kelp item +BLAZE_ROD_PRICE = 150 # per blaze rod +DRIED_KELP_PRICE = 86 # per dried kelp item +DRIED_KELP_BLOCK_PRICE = 751.01 # per dried kelp block + +SMOKERS = 64 # amount of smokers +KELP_AMOUNT = "1s" # amount of kelp to process (items) + +# ========================= +# GAME CONSTANTS (DON'T EDIT) +# ========================= + +SECONDS_PER_ITEM_SMOKER = 5 +ITEMS_PER_BLAZE_ROD = 12 +DRIED_KELP_PER_BLOCK = 9 + +NUMBER_SUFFIXES = { + "": 1, + "k": 1_000, + "m": 1_000_000, + "b": 1_000_000_000, +} + + +def money(x: float) -> str: + return f"{x:,.2f}" + + +def format_duration(total_seconds: float) -> str: + """Format seconds as a compact duration like 2d 3h 4m 5s.""" + seconds = max(0, int(round(total_seconds))) + days, rem = divmod(seconds, 86_400) + hours, rem = divmod(rem, 3_600) + minutes, secs = divmod(rem, 60) + + parts = [] + if days: + parts.append(f"{days}d") + if hours: + parts.append(f"{hours}h") + if minutes: + parts.append(f"{minutes}m") + if secs or not parts: + parts.append(f"{secs}s") + + return " ".join(parts) + + +def parse_number_with_suffix(value) -> float: + """Parse values like 1200, 1.2k, 3m, 4B (case-insensitive).""" + if isinstance(value, (int, float)): + return float(value) + + text = str(value).strip() + if not text: + raise ValueError("Value cannot be empty") + + suffix = "" + if text[-1].isalpha(): + suffix = text[-1].lower() + text = text[:-1].strip() + + if suffix not in NUMBER_SUFFIXES: + raise ValueError("Invalid suffix. Use k, m, or b") + + try: + base_value = float(text) + except ValueError as exc: + raise ValueError(f"Invalid numeric value: {value}") from exc + + return base_value * NUMBER_SUFFIXES[suffix] + + +def parse_int_with_suffix(value, field_name: str) -> int: + parsed = parse_number_with_suffix(value) + result = int(round(parsed)) + if result < 0: + raise ValueError(f"{field_name} must be >= 0") + return result + + +def parse_kelp_amount(value) -> int: + """Parse kelp amount as items; supports k/m/b and shulker forms s/ks/ms/bs.""" + text = str(value).strip() + if not text: + raise ValueError("KELP_AMOUNT cannot be empty") + + lower_text = text.lower() + shulker_multipliers = [ + ("bs", 1_000_000_000), + ("ms", 1_000_000), + ("ks", 1_000), + ("s", 1), + ] + + amount = None + for suffix, shulker_scale in shulker_multipliers: + if lower_text.endswith(suffix): + number_part = text[:-len(suffix)].strip() + if not number_part: + raise ValueError("Invalid shulker amount format") + try: + shulker_count = float(number_part) + except ValueError as exc: + raise ValueError("Invalid shulker amount format") from exc + amount = int(round(shulker_count * shulker_scale * 1728)) + break + + if amount is None: + amount = parse_int_with_suffix(text, "KELP_AMOUNT") + + if amount < 0: + raise ValueError("KELP_AMOUNT must be >= 0") + + return amount + + +def calculate( + kelp_price: float, + blaze_rod_price: float, + dried_kelp_price: float, + dried_kelp_block_price: float, + smokers: int, + kelp_amount: int, +) -> dict: + + if smokers <= 0: + raise ValueError("SMOKERS must be >= 1") + if kelp_amount < 0: + raise ValueError("KELP_AMOUNT must be >= 0") + + dried_kelp_out = kelp_amount # 1 kelp -> 1 dried kelp + + # Fuel usage (rounded up to whole rods) + blaze_rods_used = math.ceil(kelp_amount / ITEMS_PER_BLAZE_ROD) + fuel_cost = blaze_rods_used * blaze_rod_price + + # Time + total_seconds = (kelp_amount * SECONDS_PER_ITEM_SMOKER) / smokers + total_minutes = total_seconds / 60 + total_hours = total_minutes / 60 + + # --- Scenario 1: Smelt and sell dried kelp --- + kelp_cost = kelp_amount * kelp_price + cost_smelt_only = kelp_cost + fuel_cost + revenue_smelt_only = dried_kelp_out * dried_kelp_price + profit_smelt_only = revenue_smelt_only - cost_smelt_only + + # Blocks and leftovers from the smelt output + blocks_from_amount = dried_kelp_out // DRIED_KELP_PER_BLOCK + leftover_dried = dried_kelp_out % DRIED_KELP_PER_BLOCK + + # --- Scenario 2: Craft blocks only (buy dried kelp for blocks) --- + cost_craft_only = (blocks_from_amount * DRIED_KELP_PER_BLOCK) * dried_kelp_price + revenue_craft_only = blocks_from_amount * dried_kelp_block_price + profit_craft_only = revenue_craft_only - cost_craft_only + + # --- Scenario 3: Smelt then craft, sell blocks + leftover dried kelp --- + cost_smelt_then_craft = kelp_amount * kelp_price + fuel_cost + revenue_smelt_then_craft = (blocks_from_amount * dried_kelp_block_price) + (leftover_dried * dried_kelp_price) + profit_smelt_then_craft = revenue_smelt_then_craft - cost_smelt_then_craft + + print("=== Kelp Smelting / Block Craft Profit Calculator ===") + print("\n=== Config ===") + print(f"KELP_PRICE: {kelp_price}") + print(f"BLAZE_ROD_PRICE: {blaze_rod_price}") + print(f"DRIED_KELP_PRICE: {dried_kelp_price}") + print(f"DRIED_KELP_BLOCK_PRICE: {dried_kelp_block_price}") + print(f"SMOKERS: {smokers}") + print(f"KELP_AMOUNT: {kelp_amount}") + + print("\n=== Results ===") + print(f"Kelp processed: {kelp_amount}") + print(f"Dried kelp produced: {dried_kelp_out}") + print(f"Blocks craftable: {blocks_from_amount} (leftover dried kelp: {leftover_dried})") + + print("\n--- Smelting logistics ---") + print(f"Blaze rods used: {blaze_rods_used} (1 rod smelts {ITEMS_PER_BLAZE_ROD} items)") + print(f"Fuel cost: {money(fuel_cost)}") + print(f"Time to smelt all: {total_seconds:,.0f} s ({total_minutes:,.2f} min, {total_hours:,.2f} hr)") + + print("\n--- Profit calculations ---") + print(f"Profit (smelt -> sell dried kelp): {money(profit_smelt_only)}") + print(f"Profit (craft blocks only): {money(profit_craft_only)}") + print(f"Profit (smelt -> craft -> sell blocks + leftover dried): {money(profit_smelt_then_craft)}") + + print("\n--- Useful unit breakdowns ---") + avg_fuel_cost_per_item = blaze_rod_price / ITEMS_PER_BLAZE_ROD + smelt_profit_per_kelp = (dried_kelp_price - kelp_price) - avg_fuel_cost_per_item + craft_profit_per_block = dried_kelp_block_price - (DRIED_KELP_PER_BLOCK * dried_kelp_price) + + return { + "kelp_price": kelp_price, + "blaze_rod_price": blaze_rod_price, + "dried_kelp_price": dried_kelp_price, + "dried_kelp_block_price": dried_kelp_block_price, + "smokers": smokers, + "kelp_amount": kelp_amount, + "dried_kelp_out": dried_kelp_out, + "blocks_from_amount": blocks_from_amount, + "leftover_dried": leftover_dried, + "blaze_rods_used": blaze_rods_used, + "kelp_cost": kelp_cost, + "fuel_cost": fuel_cost, + "total_seconds": total_seconds, + "total_minutes": total_minutes, + "total_hours": total_hours, + "profit_smelt_only": profit_smelt_only, + "profit_craft_only": profit_craft_only, + "profit_smelt_then_craft": profit_smelt_then_craft, + "avg_fuel_cost_per_item": avg_fuel_cost_per_item, + "smelt_profit_per_kelp": smelt_profit_per_kelp, + "craft_profit_per_block": craft_profit_per_block, + } + + +def format_results(data: dict) -> str: + return ( + "=== Kelp Smelting / Block Craft Profit Calculator ===\n\n" + "=== Config ===\n" + f"KELP_PRICE: {data['kelp_price']}\n" + f"BLAZE_ROD_PRICE: {data['blaze_rod_price']}\n" + f"DRIED_KELP_PRICE: {data['dried_kelp_price']}\n" + f"DRIED_KELP_BLOCK_PRICE: {data['dried_kelp_block_price']}\n" + f"SMOKERS: {data['smokers']}\n" + f"KELP_AMOUNT: {data['kelp_amount']}\n\n" + "=== Results ===\n" + f"Kelp processed: {data['kelp_amount']}\n" + f"Dried kelp produced: {data['dried_kelp_out']}\n" + f"Blocks craftable: {data['blocks_from_amount']} " + f"(leftover dried kelp: {data['leftover_dried']})\n\n" + "--- Smelting logistics ---\n" + f"Blaze rods used: {data['blaze_rods_used']} " + f"(1 rod smelts {ITEMS_PER_BLAZE_ROD} items)\n" + f"Fuel cost: {money(data['fuel_cost'])}\n" + f"Time to smelt all: {format_duration(data['total_seconds'])} " + f"({data['total_seconds']:,.0f} s, {data['total_minutes']:,.2f} min, {data['total_hours']:,.2f} hr)\n\n" + "--- Profit calculations ---\n" + f"Profit (smelt -> sell dried kelp): {money(data['profit_smelt_only'])}\n" + f"Profit (craft blocks only): {money(data['profit_craft_only'])}\n" + f"Profit (smelt -> craft -> sell blocks + leftover dried): " + f"{money(data['profit_smelt_then_craft'])}\n\n" + "--- Useful unit breakdowns ---\n" + f"Avg fuel cost per smelted item: {money(data['avg_fuel_cost_per_item'])}\n" + "Estimated profit per kelp smelted (avg fuel): " + f"{money(data['smelt_profit_per_kelp'])}\n" + f"Profit per dried kelp block crafted: {money(data['craft_profit_per_block'])}" + ) + + +def main(): + data = calculate( + kelp_price=parse_number_with_suffix(KELP_PRICE), + blaze_rod_price=parse_number_with_suffix(BLAZE_ROD_PRICE), + dried_kelp_price=parse_number_with_suffix(DRIED_KELP_PRICE), + dried_kelp_block_price=parse_number_with_suffix(DRIED_KELP_BLOCK_PRICE), + smokers=parse_int_with_suffix(SMOKERS, "SMOKERS"), + kelp_amount=parse_kelp_amount(KELP_AMOUNT), + ) + + print(format_results(data)) + + +def launch_ui(): + if not TK_AVAILABLE: + raise RuntimeError(f"Tkinter is unavailable: {TK_IMPORT_ERROR}") + + root = tk.Tk() + root.title("Kelp Profit Calculator") + root.geometry("980x700") + root.minsize(900, 620) + root.configure(bg="#000000") + + style = ttk.Style(root) + try: + style.theme_use("clam") + except tk.TclError: + pass + + style.configure("App.TFrame", background="#000000") + style.configure("Card.TFrame", background="#111214", relief="flat") + style.configure("Header.TLabel", background="#000000", foreground="#f2f3f5", font=("Segoe UI", 21, "bold")) + style.configure("SubHeader.TLabel", background="#000000", foreground="#b5bac1", font=("Segoe UI", 10)) + style.configure("FieldLabel.TLabel", background="#111214", foreground="#dbdee1", font=("Segoe UI", 10, "bold")) + style.configure("KpiLabel.TLabel", background="#111214", foreground="#949ba4", font=("Segoe UI", 9, "bold")) + style.configure("KpiValue.TLabel", background="#111214", foreground="#f2f3f5", font=("Segoe UI", 13, "bold")) + style.configure("KpiValueBest.TLabel", background="#111214", foreground="#57f287", font=("Segoe UI", 13, "bold")) + style.configure("KpiValueProfit.TLabel", background="#111214", foreground="#f2f3f5", font=("Segoe UI", 13, "bold")) + style.configure("KpiValueLoss.TLabel", background="#111214", foreground="#ed4245", font=("Segoe UI", 13, "bold")) + style.configure("Accent.TButton", font=("Segoe UI", 10, "bold"), padding=8) + style.map( + "Accent.TButton", + background=[("active", "#4752c4"), ("!disabled", "#5865f2")], + foreground=[("!disabled", "#ffffff")], + ) + + style.configure( + "TEntry", + fieldbackground="#1e1f22", + foreground="#f2f3f5", + bordercolor="#2b2d31", + insertcolor="#f2f3f5", + ) + + style.configure("Dark.Vertical.TScrollbar", background="#2b2d31", troughcolor="#1e1f22") + + app = ttk.Frame(root, style="App.TFrame", padding=(22, 18, 22, 18)) + app.pack(fill="both", expand=True) + app.grid_columnconfigure(0, weight=1) + app.grid_columnconfigure(1, weight=1) + app.grid_rowconfigure(2, weight=1) + + ttk.Label(app, text="Kelp Profit Dashboard", style="Header.TLabel").grid( + row=0, column=0, columnspan=2, sticky="w" + ) + ttk.Label( + app, + text="Smelting, crafting, and profit snapshots in one place", + style="SubHeader.TLabel", + ).grid(row=1, column=0, columnspan=2, sticky="w", pady=(0, 14)) + + input_card = ttk.Frame(app, style="Card.TFrame", padding=(16, 16, 16, 16)) + input_card.grid(row=2, column=0, sticky="nsew", padx=(0, 10)) + input_card.grid_columnconfigure(0, weight=1) + input_card.grid_columnconfigure(1, weight=1) + + results_card = ttk.Frame(app, style="Card.TFrame", padding=(16, 16, 16, 16)) + results_card.grid(row=2, column=1, sticky="nsew", padx=(10, 0)) + results_card.grid_columnconfigure(0, weight=1) + results_card.grid_rowconfigure(4, weight=1) + + fields = [ + ("Kelp Price (k/m/b)", str(KELP_PRICE)), + ("Blaze Rod Price (k/m/b)", str(BLAZE_ROD_PRICE)), + ("Dried Kelp Price (k/m/b)", str(DRIED_KELP_PRICE)), + ("Dried Kelp Block Price (k/m/b)", str(DRIED_KELP_BLOCK_PRICE)), + ("Smokers (k/m/b)", str(SMOKERS)), + ("Kelp Amount (items k/m/b or s/ks/ms/bs)", str(KELP_AMOUNT)), + ] + + entries = {} + + ttk.Label(input_card, text="Inputs", style="FieldLabel.TLabel").grid( + row=0, column=0, columnspan=2, sticky="w", pady=(0, 10) + ) + + for row, (label_text, default_value) in enumerate(fields): + label = ttk.Label(input_card, text=label_text, style="FieldLabel.TLabel", anchor="w") + label.grid(row=row + 1, column=0, sticky="w", padx=(0, 8), pady=6) + + entry = ttk.Entry(input_card, width=24) + entry.insert(0, default_value) + entry.grid(row=row + 1, column=1, sticky="ew", pady=6) + entries[label_text] = entry + + kpi_row = ttk.Frame(results_card, style="Card.TFrame") + kpi_row.grid(row=0, column=0, sticky="ew") + kpi_row.grid_columnconfigure(0, weight=1) + kpi_row.grid_columnconfigure(1, weight=1) + kpi_row.grid_columnconfigure(2, weight=1) + + kpi_1 = ttk.Frame(kpi_row, style="Card.TFrame", padding=(10, 8, 10, 8)) + kpi_1.grid(row=0, column=0, sticky="nsew", padx=(0, 6)) + kpi_2 = ttk.Frame(kpi_row, style="Card.TFrame", padding=(10, 8, 10, 8)) + kpi_2.grid(row=0, column=1, sticky="nsew", padx=3) + kpi_3 = ttk.Frame(kpi_row, style="Card.TFrame", padding=(10, 8, 10, 8)) + kpi_3.grid(row=0, column=2, sticky="nsew", padx=(6, 0)) + + ttk.Label(kpi_1, text="Smelt Profit", style="KpiLabel.TLabel").pack(anchor="w") + kpi_1_value = ttk.Label(kpi_1, text="0.00", style="KpiValue.TLabel") + kpi_1_value.pack(anchor="w") + + ttk.Label(kpi_2, text="Craft Profit", style="KpiLabel.TLabel").pack(anchor="w") + kpi_2_value = ttk.Label(kpi_2, text="0.00", style="KpiValue.TLabel") + kpi_2_value.pack(anchor="w") + + ttk.Label(kpi_3, text="Hybrid Profit", style="KpiLabel.TLabel").pack(anchor="w") + kpi_3_value = ttk.Label(kpi_3, text="0.00", style="KpiValue.TLabel") + kpi_3_value.pack(anchor="w") + + metrics_row = ttk.Frame(results_card, style="Card.TFrame") + metrics_row.grid(row=1, column=0, sticky="ew", pady=(10, 0)) + metrics_row.grid_columnconfigure(0, weight=1) + metrics_row.grid_columnconfigure(1, weight=1) + metrics_row.grid_columnconfigure(2, weight=1) + + smelt_card = ttk.Frame(metrics_row, style="Card.TFrame", padding=(10, 8, 10, 8)) + smelt_card.grid(row=0, column=0, sticky="nsew", padx=(0, 6)) + fuel_card = ttk.Frame(metrics_row, style="Card.TFrame", padding=(10, 8, 10, 8)) + fuel_card.grid(row=0, column=1, sticky="nsew", padx=3) + kelp_card = ttk.Frame(metrics_row, style="Card.TFrame", padding=(10, 8, 10, 8)) + kelp_card.grid(row=0, column=2, sticky="nsew", padx=(6, 0)) + + ttk.Label(smelt_card, text="Smelt Time", style="KpiLabel.TLabel").pack(anchor="w") + smelt_time_value = ttk.Label(smelt_card, text="0m", style="KpiValueProfit.TLabel") + smelt_time_value.pack(anchor="w") + + ttk.Label(fuel_card, text="Fuel Cost", style="KpiLabel.TLabel").pack(anchor="w") + fuel_cost_value = ttk.Label(fuel_card, text="0.00", style="KpiValueProfit.TLabel") + fuel_cost_value.pack(anchor="w") + + ttk.Label(kelp_card, text="Kelp Cost", style="KpiLabel.TLabel").pack(anchor="w") + kelp_cost_value = ttk.Label(kelp_card, text="0.00", style="KpiValueProfit.TLabel") + kelp_cost_value.pack(anchor="w") + + ttk.Separator(results_card, orient="horizontal").grid(row=2, column=0, sticky="ew", pady=(12, 10)) + + ttk.Label(results_card, text="Full Breakdown", style="FieldLabel.TLabel").grid(row=3, column=0, sticky="w", pady=(0, 8)) + + text_wrap = ttk.Frame(results_card, style="Card.TFrame") + text_wrap.grid(row=4, column=0, sticky="nsew") + text_wrap.grid_columnconfigure(0, weight=1) + text_wrap.grid_rowconfigure(0, weight=1) + + result_text = tk.Text( + text_wrap, + wrap="word", + height=24, + bg="#1e1f22", + fg="#f2f3f5", + insertbackground="#f2f3f5", + relief="flat", + padx=10, + pady=10, + font=("Consolas", 10), + ) + result_text.grid(row=0, column=0, sticky="nsew") + + scrollbar = ttk.Scrollbar(text_wrap, orient="vertical", command=result_text.yview, style="Dark.Vertical.TScrollbar") + scrollbar.grid(row=0, column=1, sticky="ns") + result_text.configure(yscrollcommand=scrollbar.set) + + result_text.tag_configure("section", foreground="#f2f3f5", font=("Consolas", 10, "bold")) + result_text.tag_configure("profit_best", foreground="#57f287", font=("Consolas", 10, "bold")) + result_text.tag_configure("profit_other", foreground="#f2f3f5", font=("Consolas", 10, "bold")) + result_text.tag_configure("loss", foreground="#ed4245", font=("Consolas", 10, "bold")) + + button_row = ttk.Frame(input_card, style="Card.TFrame") + button_row.grid(row=len(fields) + 2, column=0, columnspan=2, sticky="ew", pady=(14, 2)) + button_row.grid_columnconfigure(0, weight=1) + + calculate_btn = ttk.Button(button_row, text="Calculate", style="Accent.TButton") + calculate_btn.grid(row=0, column=0, sticky="ew") + + def on_calculate(): + try: + data = calculate( + kelp_price=parse_number_with_suffix(entries["Kelp Price (k/m/b)"].get()), + blaze_rod_price=parse_number_with_suffix(entries["Blaze Rod Price (k/m/b)"].get()), + dried_kelp_price=parse_number_with_suffix(entries["Dried Kelp Price (k/m/b)"].get()), + dried_kelp_block_price=parse_number_with_suffix(entries["Dried Kelp Block Price (k/m/b)"].get()), + smokers=parse_int_with_suffix(entries["Smokers (k/m/b)"].get(), "SMOKERS"), + kelp_amount=parse_kelp_amount(entries["Kelp Amount (items k/m/b or s/ks/ms/bs)"].get()), + ) + except ValueError as exc: + messagebox.showerror("Invalid input", str(exc)) + return + + kpi_1_value.configure(text=money(data["profit_smelt_only"])) + kpi_2_value.configure(text=money(data["profit_craft_only"])) + kpi_3_value.configure(text=money(data["profit_smelt_then_craft"])) + + profits = { + "smelt": data["profit_smelt_only"], + "craft": data["profit_craft_only"], + "hybrid": data["profit_smelt_then_craft"], + } + max_profit = max(profits.values()) + + def kpi_style(value: float) -> str: + if value < 0: + return "KpiValueLoss.TLabel" + if value == max_profit: + return "KpiValueBest.TLabel" + return "KpiValueProfit.TLabel" + + kpi_1_value.configure(style=kpi_style(profits["smelt"])) + kpi_2_value.configure(style=kpi_style(profits["craft"])) + kpi_3_value.configure(style=kpi_style(profits["hybrid"])) + + smelt_time_value.configure(text=format_duration(data["total_seconds"])) + fuel_cost_value.configure(text=money(data["fuel_cost"])) + kelp_cost_value.configure(text=money(data["kelp_cost"])) + + result_text.delete("1.0", tk.END) + result_text.insert(tk.END, format_results(data)) + + for heading in ["===", "---"]: + start = "1.0" + while True: + idx = result_text.search(heading, start, tk.END) + if not idx: + break + line_end = result_text.index(f"{idx} lineend") + result_text.tag_add("section", idx, line_end) + start = line_end + + result_text.tag_remove("profit_best", "1.0", tk.END) + result_text.tag_remove("profit_other", "1.0", tk.END) + result_text.tag_remove("loss", "1.0", tk.END) + + line_rules = [ + ("Profit (smelt -> sell dried kelp)", profits["smelt"]), + ("Profit (craft blocks only)", profits["craft"]), + ("Profit (smelt -> craft -> sell blocks + leftover dried)", profits["hybrid"]), + ] + + for label, value in line_rules: + idx = result_text.search(label, "1.0", tk.END) + if not idx: + continue + line_end = result_text.index(f"{idx} lineend") + if value < 0: + result_text.tag_add("loss", idx, line_end) + elif value == max_profit: + result_text.tag_add("profit_best", idx, line_end) + else: + result_text.tag_add("profit_other", idx, line_end) + + calculate_btn.configure(command=on_calculate) + + on_calculate() + root.mainloop() + +if __name__ == "__main__": + try: + if TK_AVAILABLE: + try: + launch_ui() + except tk.TclError: + main() + else: + print(f"UI unavailable ({TK_IMPORT_ERROR}). Falling back to CLI output.\n") + main() + except KeyboardInterrupt: + print("\nInterrupted by user. Exiting.") \ No newline at end of file