From 4a7adf10062871c319566147acedbaf7f9be8dab Mon Sep 17 00:00:00 2001 From: ZareMate <0.zaremate@gmail.com> Date: Mon, 23 Mar 2026 15:35:09 +0100 Subject: [PATCH] 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. --- .vscode/settings.json | 3 + .../kelp_profit_calculator.cpython-314.pyc | Bin 0 -> 27539 bytes kelp_profit_calculator.py | 555 ++++++++++++++++++ 3 files changed, 558 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 __pycache__/kelp_profit_calculator.cpython-314.pyc create mode 100644 kelp_profit_calculator.py 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 0000000000000000000000000000000000000000..6a3ea84a15d65709183e77acec505740169cbd38 GIT binary patch literal 27539 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 a-!qU9`<|IZutknwo2mb({JlC7!v7DY+Y!(J literal 0 HcmV?d00001 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