Sephiria inventory optimizer v0.1.0

- Slab catalog and effect handlers ported from WhiteDog1004/sephiria
- Hill-climbing solver maximizes effect sum on slab-occupied cells
- PIL renderer outputs PNG with effects overlay; downloads + caches slab
  images from img.sephiria.wiki on demand
- Tkinter GUI for picking slabs by tier; CLI also available
- Screenshot recognizer (template matching, beta)
- build.bat / build.sh for portable single-file builds via PyInstaller
This commit is contained in:
tkrmagid
2026-05-13 22:12:49 +09:00
parent 88f46eb146
commit 3cb8140cfa
13 changed files with 1940 additions and 0 deletions

113
sephiria_inv/__main__.py Normal file
View File

@@ -0,0 +1,113 @@
"""Entry point: GUI by default, CLI when --cli is passed."""
from __future__ import annotations
import argparse
import sys
from typing import List
from .renderer import save_solution
from .slabs import SLABS, SLABS_BY_VALUE
from .solver import solve
def _cli(argv: List[str]) -> int:
p = argparse.ArgumentParser(
prog="sephiria_inv",
description="Optimize Sephiria slab placement and render the result.",
)
p.add_argument(
"--cli", action="store_true",
help="Run in CLI mode (no GUI).",
)
p.add_argument(
"-s", "--slab", action="append", default=[],
help="Slab value to include. Use value (e.g. 'harvesting') with optional ':N' "
"multiplier (e.g. 'harvesting:3'). Repeat as needed.",
)
p.add_argument(
"--slots", type=int, default=34, help="Inventory slot count (18..60).",
)
p.add_argument("--seed", type=int, default=None)
p.add_argument(
"--time-limit", type=float, default=4.0,
help="Solver time budget in seconds.",
)
p.add_argument(
"-o", "--output", default="sephiria_layout.png",
help="Output PNG path.",
)
p.add_argument(
"--list", action="store_true",
help="List known slab values and exit.",
)
p.add_argument(
"--no-download", action="store_true",
help="Skip CDN download (text-only render).",
)
p.add_argument(
"--screenshot", default=None,
help="Read slab list from a game screenshot (PNG/JPG).",
)
p.add_argument(
"--bbox", default=None,
help="Required with --screenshot. Pixel bbox of the inventory grid as "
"'left,top,right,bottom'.",
)
args = p.parse_args(argv)
if args.list:
for s in SLABS:
print(f" {s.value:<16s} {s.ko_label:<6s} ({s.tier})")
return 0
basket: List[str] = []
if args.screenshot:
if not args.bbox:
print("--screenshot 사용 시 --bbox 'l,t,r,b' 가 필요합니다.", file=sys.stderr)
return 2
try:
l, t, r, b = (int(v) for v in args.bbox.split(","))
except ValueError:
print("--bbox 형식: left,top,right,bottom (정수)", file=sys.stderr)
return 2
from .screenshot import recognize, recognized_values
recs = recognize(args.screenshot, (l, t, r, b), slot_num=args.slots)
basket.extend(recognized_values(recs))
print(f"인식된 석판: {len(basket)}개 from screenshot")
for raw in args.slab:
if ":" in raw:
v, n = raw.split(":", 1)
try:
count = int(n)
except ValueError:
print(f"잘못된 개수: {raw}", file=sys.stderr)
return 2
else:
v, count = raw, 1
if v not in SLABS_BY_VALUE:
print(f"알 수 없는 석판 value: {v} (use --list)", file=sys.stderr)
return 2
basket.extend([v] * count)
if not basket:
print("최소 하나 이상의 --slab 을 지정하세요.", file=sys.stderr)
return 2
sol = solve(basket, slot_num=args.slots, time_limit=args.time_limit, seed=args.seed)
save_solution(sol, args.output, download=not args.no_download)
print(f"score={sol.score} placed={len(sol.placements)}{args.output}")
return 0
def main(argv: List[str] | None = None) -> int:
argv = list(argv if argv is not None else sys.argv[1:])
if "--cli" in argv or "--list" in argv or any(a.startswith("-s") or a.startswith("--slab") for a in argv):
return _cli(argv)
# Fall back to GUI
from .gui import main as gui_main
return gui_main()
if __name__ == "__main__":
raise SystemExit(main())