kelp-calculations/kelp_profit_calculator.py

605 lines
24 KiB
Python

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 # per kelp item
BLAZE_ROD_PRICE = 150 # per blaze rod
DRIED_KELP_PRICE = 83.18 # per dried kelp item
DRIED_KELP_BLOCK_PRICE = 750.5 # per dried kelp block
BUY_KELP = True # True = buy kelp, False = use owned kelp
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,
buy_kelp: bool,
) -> 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_market_value = kelp_amount * kelp_price
# In owned mode we still use kelp market value as opportunity cost,
# but label it as raw kelp profit in the UI.
kelp_cost = kelp_market_value
kelp_value_label = "Kelp Cost" if buy_kelp else "Raw Kelp Profit"
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_cost + 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"BUY_KELP: {buy_kelp}")
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,
"buy_kelp": buy_kelp,
"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_market_value": kelp_market_value,
"kelp_value_label": kelp_value_label,
"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"BUY_KELP: {data['buy_kelp']}\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),
buy_kelp=BUY_KELP,
)
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("SourceOn.TButton", font=("Segoe UI", 9, "bold"), padding=7)
style.map(
"SourceOn.TButton",
background=[("active", "#4752c4"), ("!disabled", "#5865f2")],
foreground=[("!disabled", "#ffffff")],
)
style.configure("SourceOff.TButton", font=("Segoe UI", 9, "bold"), padding=7)
style.map(
"SourceOff.TButton",
background=[("active", "#3a3d42"), ("!disabled", "#2b2d31")],
foreground=[("!disabled", "#dbdee1")],
)
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 = {}
buy_kelp_var = tk.BooleanVar(value=BUY_KELP)
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
source_row = ttk.Frame(input_card, style="Card.TFrame")
source_row.grid(row=len(fields) + 1, column=0, columnspan=2, sticky="ew", pady=(10, 2))
source_row.grid_columnconfigure(1, weight=1)
ttk.Label(source_row, text="Kelp Source", style="FieldLabel.TLabel").grid(row=0, column=0, sticky="w", padx=(0, 8))
source_toggle_btn = ttk.Button(source_row)
source_toggle_btn.grid(row=0, column=1, sticky="ew")
def refresh_source_toggle():
if buy_kelp_var.get():
source_toggle_btn.configure(text="Buying Kelp (click to use owned)", style="SourceOn.TButton")
else:
source_toggle_btn.configure(text="Using Owned Kelp (click to buy)", style="SourceOff.TButton")
def toggle_kelp_source():
buy_kelp_var.set(not buy_kelp_var.get())
refresh_source_toggle()
on_calculate()
source_toggle_btn.configure(command=toggle_kelp_source)
refresh_source_toggle()
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")
kelp_cost_label = ttk.Label(kelp_card, text="Kelp Cost", style="KpiLabel.TLabel")
kelp_cost_label.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()),
buy_kelp=buy_kelp_var.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_label.configure(text=data["kelp_value_label"])
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.")