Android build v1.1.34

This commit is contained in:
Warren H
2026-07-07 23:25:40 -04:00
parent 679900e7f5
commit d61cee55ed
14 changed files with 1718 additions and 28 deletions
+344
View File
@@ -0,0 +1,344 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import queue
import subprocess
import threading
from pathlib import Path
try:
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
except ModuleNotFoundError:
print(
"This Python install does not include Tkinter.\n"
"Use the browser GUI instead:\n"
" node scripts/iwt2-pvp-sim-gui-server.mjs\n"
"Then open http://localhost:8787\n"
)
raise SystemExit(1)
ROOT = Path(__file__).resolve().parents[1]
SIM_SCRIPT = ROOT / "scripts" / "iwt2-pvp-roguelike-boss-sim.mjs"
HEALER_CLASSES = ["dawnweaver", "lifebinder", "runesage"]
BOSS_IDS = [
"bulldrome",
"yian-kut-ku",
"great-jaggi",
"khezu",
"rathian",
"barroth",
"tobi-kadachi",
"rimebastion",
"ember-mantis-duelist",
"cinderback-ricochet",
"obsidian-ram-golem",
"stormcoil-wyrm",
"venom-orchid-hydra",
"sandglass-scorpion",
"crystal-bat-matriarch",
"hollowcrown-revenant",
]
class SimGui(tk.Tk):
def __init__(self) -> None:
super().__init__()
self.title("IWT2 PvP Roguelike Sim")
self.geometry("1120x780")
self.minsize(920, 640)
self.output_queue: queue.Queue[tuple[str, str | None]] = queue.Queue()
self.process: subprocess.Popen[str] | None = None
self.last_json = ""
self.class_vars = {name: tk.BooleanVar(value=True) for name in HEALER_CLASSES}
self.boss_vars = {boss_id: tk.BooleanVar(value=True) for boss_id in BOSS_IDS}
self.all_classes_var = tk.BooleanVar(value=True)
self.all_bosses_var = tk.BooleanVar(value=True)
self.gear_level_var = tk.StringVar(value="5")
self.boss_count_var = tk.StringVar(value="2")
self.hp_percent_var = tk.StringVar(value="100")
self.damage_percent_var = tk.StringVar(value="100")
self.stages_var = tk.StringVar(value="1,2")
self.seconds_var = tk.StringVar(value="180")
self.workers_var = tk.StringVar(value="8")
self.top_var = tk.StringVar(value="24")
self.status_var = tk.StringVar(value="Idle")
self._build_ui()
self.after(100, self._drain_output_queue)
def _build_ui(self) -> None:
self.columnconfigure(0, weight=0)
self.columnconfigure(1, weight=1)
self.rowconfigure(0, weight=1)
controls = ttk.Frame(self, padding=12)
controls.grid(row=0, column=0, sticky="nsew")
output_frame = ttk.Frame(self, padding=(0, 12, 12, 12))
output_frame.grid(row=0, column=1, sticky="nsew")
output_frame.columnconfigure(0, weight=1)
output_frame.rowconfigure(1, weight=1)
self._build_controls(controls)
output_header = ttk.Frame(output_frame)
output_header.grid(row=0, column=0, sticky="ew", pady=(0, 8))
output_header.columnconfigure(0, weight=1)
ttk.Label(output_header, textvariable=self.status_var).grid(row=0, column=0, sticky="w")
ttk.Button(output_header, text="Save JSON", command=self._save_json).grid(row=0, column=1, padx=(8, 0))
ttk.Button(output_header, text="Clear", command=self._clear_output).grid(row=0, column=2, padx=(8, 0))
self.output = tk.Text(output_frame, wrap="none", font=("Menlo", 11))
self.output.grid(row=1, column=0, sticky="nsew")
yscroll = ttk.Scrollbar(output_frame, orient="vertical", command=self.output.yview)
yscroll.grid(row=1, column=1, sticky="ns")
xscroll = ttk.Scrollbar(output_frame, orient="horizontal", command=self.output.xview)
xscroll.grid(row=2, column=0, sticky="ew")
self.output.configure(yscrollcommand=yscroll.set, xscrollcommand=xscroll.set)
def _build_controls(self, parent: ttk.Frame) -> None:
parent.columnconfigure(0, weight=1)
ttk.Label(parent, text="Simulation").grid(row=0, column=0, sticky="w")
scalar_grid = ttk.Frame(parent)
scalar_grid.grid(row=1, column=0, sticky="ew", pady=(8, 12))
for column in range(2):
scalar_grid.columnconfigure(column, weight=1)
fields = [
("Gear level", self.gear_level_var),
("Boss count", self.boss_count_var),
("Boss HP %", self.hp_percent_var),
("Boss damage %", self.damage_percent_var),
("Stages", self.stages_var),
("Seconds", self.seconds_var),
("Workers", self.workers_var),
("Top rows", self.top_var),
]
for index, (label, variable) in enumerate(fields):
row = index // 2
column = (index % 2) * 2
ttk.Label(scalar_grid, text=label).grid(row=row, column=column, sticky="w", padx=(0, 6), pady=3)
ttk.Entry(scalar_grid, textvariable=variable, width=10).grid(row=row, column=column + 1, sticky="ew", pady=3)
ttk.Checkbutton(
parent,
text="All classes",
variable=self.all_classes_var,
command=self._toggle_all_classes,
).grid(row=2, column=0, sticky="w")
class_frame = ttk.Frame(parent)
class_frame.grid(row=3, column=0, sticky="ew", pady=(4, 12))
for index, class_id in enumerate(HEALER_CLASSES):
ttk.Checkbutton(
class_frame,
text=class_id,
variable=self.class_vars[class_id],
command=self._sync_all_classes,
).grid(row=0, column=index, sticky="w", padx=(0, 10))
ttk.Checkbutton(
parent,
text="All bosses",
variable=self.all_bosses_var,
command=self._toggle_all_bosses,
).grid(row=4, column=0, sticky="w")
boss_canvas = tk.Canvas(parent, height=300, highlightthickness=0)
boss_scroll = ttk.Scrollbar(parent, orient="vertical", command=boss_canvas.yview)
boss_frame = ttk.Frame(boss_canvas)
boss_frame.bind(
"<Configure>",
lambda _event: boss_canvas.configure(scrollregion=boss_canvas.bbox("all")),
)
boss_canvas.create_window((0, 0), window=boss_frame, anchor="nw")
boss_canvas.configure(yscrollcommand=boss_scroll.set)
boss_canvas.grid(row=5, column=0, sticky="nsew", pady=(4, 12))
boss_scroll.grid(row=5, column=1, sticky="ns", pady=(4, 12))
for index, boss_id in enumerate(BOSS_IDS):
ttk.Checkbutton(
boss_frame,
text=boss_id,
variable=self.boss_vars[boss_id],
command=self._sync_all_bosses,
).grid(row=index, column=0, sticky="w", pady=2)
button_row = ttk.Frame(parent)
button_row.grid(row=6, column=0, sticky="ew")
button_row.columnconfigure(0, weight=1)
self.run_button = ttk.Button(button_row, text="Run Simulation", command=self._start_sim)
self.run_button.grid(row=0, column=0, sticky="ew")
self.stop_button = ttk.Button(button_row, text="Stop", command=self._stop_sim, state="disabled")
self.stop_button.grid(row=0, column=1, padx=(8, 0))
ttk.Button(parent, text="Copy Command", command=self._copy_command).grid(row=7, column=0, sticky="ew", pady=(8, 0))
def _toggle_all_classes(self) -> None:
value = self.all_classes_var.get()
for variable in self.class_vars.values():
variable.set(value)
def _sync_all_classes(self) -> None:
self.all_classes_var.set(all(variable.get() for variable in self.class_vars.values()))
def _toggle_all_bosses(self) -> None:
value = self.all_bosses_var.get()
for variable in self.boss_vars.values():
variable.set(value)
def _sync_all_bosses(self) -> None:
self.all_bosses_var.set(all(variable.get() for variable in self.boss_vars.values()))
def _selected_classes(self) -> str:
if self.all_classes_var.get():
return "all"
selected = [name for name, variable in self.class_vars.items() if variable.get()]
return ",".join(selected) if selected else "all"
def _selected_bosses(self) -> str:
if self.all_bosses_var.get():
return "all"
selected = [boss_id for boss_id, variable in self.boss_vars.items() if variable.get()]
return ",".join(selected) if selected else "all"
def _command(self) -> list[str]:
return [
"node",
str(SIM_SCRIPT),
"--classes",
self._selected_classes(),
"--gear-level",
self.gear_level_var.get().strip() or "5",
"--boss-count",
self.boss_count_var.get().strip() or "2",
"--bosses",
self._selected_bosses(),
"--boss-hp-percent",
self.hp_percent_var.get().strip() or "100",
"--boss-damage-percent",
self.damage_percent_var.get().strip() or "100",
"--stages",
self.stages_var.get().strip() or "1,2",
"--seconds",
self.seconds_var.get().strip() or "180",
"--workers",
self.workers_var.get().strip() or "8",
"--top",
self.top_var.get().strip() or "24",
]
def _start_sim(self) -> None:
if self.process is not None:
return
command = self._command()
self._clear_output()
self._append_output("$ " + " ".join(command) + "\n\n")
self.status_var.set("Running...")
self.run_button.configure(state="disabled")
self.stop_button.configure(state="normal")
thread = threading.Thread(target=self._run_subprocess, args=(command,), daemon=True)
thread.start()
def _run_subprocess(self, command: list[str]) -> None:
try:
self.process = subprocess.Popen(
command,
cwd=ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
assert self.process.stdout is not None
chunks: list[str] = []
for line in self.process.stdout:
chunks.append(line)
self.output_queue.put(("line", line))
exit_code = self.process.wait()
output = "".join(chunks)
self.output_queue.put(("done", json.dumps({"exitCode": exit_code, "output": output})))
except Exception as exc: # noqa: BLE001
self.output_queue.put(("error", str(exc)))
def _drain_output_queue(self) -> None:
try:
while True:
kind, payload = self.output_queue.get_nowait()
if kind == "line" and payload is not None:
self._append_output(payload)
elif kind == "done" and payload is not None:
result = json.loads(payload)
self.last_json = self._extract_json(result["output"])
self.status_var.set(f"Finished: exit {result['exitCode']}")
self.process = None
self.run_button.configure(state="normal")
self.stop_button.configure(state="disabled")
elif kind == "error" and payload is not None:
self.status_var.set("Failed")
self._append_output(f"\nERROR: {payload}\n")
self.process = None
self.run_button.configure(state="normal")
self.stop_button.configure(state="disabled")
except queue.Empty:
pass
self.after(100, self._drain_output_queue)
def _stop_sim(self) -> None:
if self.process is None:
return
self.process.terminate()
self.status_var.set("Stopping...")
def _clear_output(self) -> None:
self.output.delete("1.0", "end")
self.last_json = ""
def _append_output(self, text: str) -> None:
self.output.insert("end", text)
self.output.see("end")
def _copy_command(self) -> None:
command = " ".join(self._command())
self.clipboard_clear()
self.clipboard_append(command)
self.status_var.set("Command copied")
def _save_json(self) -> None:
output = self.last_json or self._extract_json(self.output.get("1.0", "end"))
if not output:
messagebox.showinfo("No JSON", "Run a simulation first.")
return
path = filedialog.asksaveasfilename(
defaultextension=".json",
filetypes=[("JSON", "*.json"), ("All files", "*.*")],
initialfile="iwt2-pvp-roguelike-sim.json",
)
if not path:
return
Path(path).write_text(output, encoding="utf-8")
self.status_var.set(f"Saved {path}")
@staticmethod
def _extract_json(text: str) -> str:
start = text.find("{")
end = text.rfind("}")
if start < 0 or end < start:
return ""
candidate = text[start : end + 1]
try:
parsed = json.loads(candidate)
except json.JSONDecodeError:
return ""
return json.dumps(parsed, indent=2)
if __name__ == "__main__":
SimGui().mainloop()