Files
tkrmagid 3cb8140cfa 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
2026-05-13 22:12:49 +09:00

138 lines
4.2 KiB
Python

"""Hill-climbing solver for slab placement.
We don't try to enumerate the full N! search space — for a 34-slot grid the
number of placements is enormous. Instead we use multi-start random + first-
improvement hill climbing with swap/rotate/move moves. This converges quickly
to good local optima and is plenty fast for typical inputs (≤ 60 slabs).
"""
from __future__ import annotations
import random
import time
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
from .slabs import (
GRID_COLS,
GridConfig,
Placement,
SLABS_BY_VALUE,
all_slot_ids,
generate_grid_config,
score,
)
@dataclass
class Solution:
placements: List[Placement] = field(default_factory=list)
grid: GridConfig = field(default_factory=list)
score: int = 0
slot_num: int = 34
def _initial(
slab_values: List[str], grid: GridConfig, rng: random.Random
) -> List[Placement]:
slots = all_slot_ids(grid)
rng.shuffle(slots)
placements: List[Placement] = []
for v, sid in zip(slab_values, slots):
slab = SLABS_BY_VALUE.get(v)
rot = rng.randint(0, 3) if slab and slab.rotate else 0
placements.append(Placement(value=v, slot_id=sid, rotation=rot))
return placements
def _hill_climb(
placements: List[Placement],
grid: GridConfig,
rng: random.Random,
max_iters: int,
time_limit: float,
) -> Tuple[List[Placement], int]:
cur = [Placement(p.value, p.slot_id, p.rotation) for p in placements]
cur_score = score(cur, grid)
used = {p.slot_id for p in cur}
free = [sid for sid in all_slot_ids(grid) if sid not in used]
start = time.monotonic()
for _ in range(max_iters):
if time.monotonic() - start > time_limit:
break
move = rng.random()
if move < 0.45 and len(cur) > 1:
# swap two placements
i, j = rng.sample(range(len(cur)), 2)
cur[i].slot_id, cur[j].slot_id = cur[j].slot_id, cur[i].slot_id
ns = score(cur, grid)
if ns > cur_score:
cur_score = ns
else:
cur[i].slot_id, cur[j].slot_id = cur[j].slot_id, cur[i].slot_id
elif move < 0.75 and free:
# move one placement to a free slot
i = rng.randint(0, len(cur) - 1)
old = cur[i].slot_id
new_idx = rng.randint(0, len(free) - 1)
new_slot = free[new_idx]
cur[i].slot_id = new_slot
ns = score(cur, grid)
if ns > cur_score:
cur_score = ns
free[new_idx] = old
else:
cur[i].slot_id = old
else:
# rotate one placement
i = rng.randint(0, len(cur) - 1)
slab = SLABS_BY_VALUE.get(cur[i].value)
if not slab or not slab.rotate:
continue
old_rot = cur[i].rotation
cur[i].rotation = (old_rot + rng.choice([1, 2, 3])) % 4
ns = score(cur, grid)
if ns > cur_score:
cur_score = ns
else:
cur[i].rotation = old_rot
return cur, cur_score
def solve(
slab_values: List[str],
slot_num: int = 34,
*,
restarts: int = 6,
iters_per_restart: int = 8000,
time_limit: float = 4.0,
seed: Optional[int] = None,
) -> Solution:
"""Find a good placement for the given slabs.
`slab_values` is a list of slab `value` strings (duplicates allowed).
Slabs that don't fit (more slabs than slots) are dropped from the tail.
"""
grid = generate_grid_config(slot_num)
capacity = sum(r["cols"] for r in grid)
if len(slab_values) > capacity:
slab_values = slab_values[:capacity]
if not slab_values:
return Solution([], grid, 0, slot_num)
rng = random.Random(seed)
best: List[Placement] = []
best_score = -10**9
per_restart_budget = time_limit / max(restarts, 1)
for _ in range(restarts):
init = _initial(slab_values, grid, rng)
sol, sc = _hill_climb(init, grid, rng, iters_per_restart, per_restart_budget)
if sc > best_score:
best_score = sc
best = [Placement(p.value, p.slot_id, p.rotation) for p in sol]
return Solution(best, grid, best_score, slot_num)