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

915 lines
24 KiB
Python

"""Slab catalog and effect handlers.
Ported from WhiteDog1004/sephiria
src/features/simulator/config/slabsLists.ts
src/features/simulator/config/getSlabsEffect.ts
src/features/simulator/lib/calculateEffects.ts
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable, Dict, List, Optional, Tuple
GridConfig = List[Dict[str, int]] # [{"rows": int, "cols": int}, ...]
Effects = Dict[str, int]
Flags = Dict[str, Optional[str]]
# ---------------------------------------------------------------------------
# Catalog
# ---------------------------------------------------------------------------
@dataclass
class Slab:
value: str
tier: str
ko_label: str
eng_label: str
image: str # CDN path, e.g. "slabs/chivalry.png"
rotate: bool = False
@property
def image_url(self) -> str:
return f"https://img.sephiria.wiki/{self.image.lstrip('/')}"
# tier -> ordering weight (for display)
TIER_ORDER = {"common": 0, "advanced": 1, "rare": 2, "legend": 3, "solid": 4}
TIER_LABEL = {
"common": "일반",
"advanced": "고급",
"rare": "희귀",
"legend": "전설",
"solid": "결속",
}
# All slabs from slabsLists.ts. image path is stripped of leading "/".
SLABS: List[Slab] = [
# COMMON
Slab("chivalry", "common", "기사도", "chivalry", "slabs/chivalry.png", True),
Slab("dry", "common", "건조", "dry", "slabs/dry.png"),
Slab("approximation", "common", "근사", "approximation", "slabs/approximation.png", True),
Slab("advent", "common", "도래", "advent", "slabs/advent.png", True),
Slab("linear", "common", "선의", "linear", "slabs/linear.png"),
Slab("sight", "common", "시선", "sight", "slabs/sight.png", True),
Slab("handshake", "common", "악수", "handshake", "slabs/handshake.png", True),
Slab("fate", "common", "운명", "fate", "slabs/fate.webp"),
Slab("wit", "common", "재치", "wit", "slabs/wit.png", True),
Slab("exploitation", "common", "착취", "exploitation", "slabs/exploitation.png", True),
Slab("unity", "common", "화합", "unity", "slabs/unity.png", True),
Slab("cheer", "common", "환호", "cheer", "slabs/cheer.webp"),
Slab("hope", "common", "희망", "hope", "slabs/hope.png", True),
# ADVANCED
Slab("compete", "advanced", "경쟁", "compete", "slabs/compete.png", True),
Slab("beating", "advanced", "고동", "beating", "slabs/beating.png", True),
Slab("home_town", "advanced", "고양", "home_town", "slabs/home-town.png", True),
Slab("past", "advanced", "과거", "past", "slabs/past.png", True),
Slab("future", "advanced", "미래", "future", "slabs/future.png", True),
Slab("distribution", "advanced", "분배", "distribution", "slabs/distribution.png"),
Slab("triceps", "advanced", "삼두", "triceps", "slabs/triceps.png"),
Slab("harvesting", "advanced", "수확", "harvesting", "slabs/harvesting.png", True),
Slab("binary_star", "advanced", "쌍성", "binary_star", "slabs/binary_star.png", True),
Slab("nurture", "advanced", "양육", "nurture", "slabs/nurture.png", True),
Slab("yearning", "advanced", "열망", "yearning", "slabs/yearning.png"),
Slab("agglutination", "advanced", "응집", "agglutination", "slabs/agglutination.png", True),
Slab("entrance", "advanced", "입구", "entrance", "slabs/entrance.png"),
Slab("joke", "advanced", "장난", "joke", "slabs/joke.png", True),
Slab("load", "advanced", "적재", "load", "slabs/load.png", True),
Slab("transition", "advanced", "전이", "transition", "slabs/transition.png", True),
Slab("advance", "advanced", "전진", "advance", "slabs/advance.png", True),
Slab("justice", "advanced", "정의", "justice", "slabs/justice.png"),
Slab("preparation", "advanced", "준비", "preparation", "slabs/preparation.png", True),
Slab("exit", "advanced", "출구", "exit", "slabs/exit.png"),
Slab("tide", "advanced", "파도", "tide", "slabs/tide.png", True),
Slab("dedication", "advanced", "헌정", "dedication", "slabs/dedication.png"),
Slab("honor", "advanced", "명예", "honor", "slabs/honor.png", True),
# RARE
Slab("base", "rare", "기반", "base", "slabs/base.png"),
Slab("warrant", "rare", "권능", "warrant", "slabs/warrant.png", True),
Slab("disconnection", "rare", "단절", "disconnection", "slabs/disconnection.png"),
Slab("concurrency", "rare", "동시성", "concurrency", "slabs/concurrency.png"),
Slab("vow", "rare", "맹세", "vow", "slabs/vow.png", True),
Slab("rebellion", "rare", "반항", "rebellion", "slabs/rebellion.png", True),
Slab("connection", "rare", "이음", "connection", "slabs/connection.png", True),
Slab("shade", "rare", "차양", "shade", "slabs/shade.png"),
# LEGEND
Slab("thorn", "legend", "가시", "thorn", "slabs/thorn.png"),
Slab("boundary", "legend", "경계", "boundary", "slabs/boundary.png"),
Slab("sheen", "legend", "광휘", "sheen", "slabs/sheen.png", True),
Slab("miracle", "legend", "기적", "miracle", "slabs/miracle.png"),
Slab("daydream", "legend", "백일몽", "daydream", "slabs/daydream.png", True),
Slab("compression", "legend", "압축", "compression", "slabs/compression.png", True),
Slab("certitude", "legend", "확신", "certitude", "slabs/certitude.png", True),
Slab("hospitality", "legend", "환대", "hospitality", "slabs/hospitality.png"),
Slab("courage", "legend", "용기", "courage", "slabs/courage.png", True),
Slab("peace", "legend", "평화", "peace", "slabs/peace.png", True),
]
SLABS_BY_VALUE: Dict[str, Slab] = {s.value: s for s in SLABS}
# ---------------------------------------------------------------------------
# Grid helpers
# ---------------------------------------------------------------------------
GRID_COLS = 6
def generate_grid_config(total_slots: int = 34) -> GridConfig:
"""Mirror of generateGridConfig() in SlotComponent.tsx."""
if total_slots <= 0:
return []
config: GridConfig = []
full_rows = total_slots // GRID_COLS
last_row_cols = total_slots % GRID_COLS
for i in range(full_rows):
config.append({"rows": i, "cols": GRID_COLS})
if last_row_cols > 0:
config.append({"rows": full_rows, "cols": last_row_cols})
return config
def all_slot_ids(grid: GridConfig) -> List[str]:
return [f"{row['rows']}-{c}" for row in grid for c in range(row["cols"])]
def cols_in_row(grid: GridConfig, y: int) -> int:
for row in grid:
if row["rows"] == y:
return row["cols"]
return 0
# ---------------------------------------------------------------------------
# Rotation helpers
# ---------------------------------------------------------------------------
def rotate_offset(dx: int, dy: int, rotation: int) -> Tuple[int, int]:
if rotation == 1:
return -dy, dx
if rotation == 2:
return -dx, -dy
if rotation == 3:
return dy, -dx
return dx, dy
def apply_offsets(
offsets: List[Dict],
x: int,
y: int,
effects: Effects,
rotation: int = 0,
flags: Optional[Flags] = None,
) -> None:
for o in offsets:
ndx, ndy = rotate_offset(o["dx"], o["dy"], rotation)
target = f"{y + ndy}-{x + ndx}"
if target in effects:
effects[target] += o.get("value", 1)
if flags is not None and o.get("flag") == "ignore" and target in flags:
flags[target] = "ignore"
# ---------------------------------------------------------------------------
# Effect handlers
# Signature: handler(x, y, slot_id, rotation, effects, flags, grid_config)
# ---------------------------------------------------------------------------
Handler = Callable[[int, int, str, int, Effects, Flags, GridConfig], None]
def _no_op(*_args, **_kwargs) -> None:
pass
# COMMON
def _approximation(x, y, _s, rot, e, _f, _g):
apply_offsets([{"dx": 0, "dy": -1}, {"dx": 1, "dy": 0}], x, y, e, rot)
def _dry(x, y, _s, _rot, e, _f, _g):
apply_offsets([{"dx": 0, "dy": -1}, {"dx": 0, "dy": 1}], x, y, e, 0)
def _chivalry(x, y, _s, rot, e, _f, _g):
apply_offsets([{"dx": -1, "dy": -2}], x, y, e, rot)
def _advent(x, y, _s, rot, e, _f, _g):
apply_offsets(
[
{"dx": 0, "dy": -1},
{"dx": 0, "dy": -2},
{"dx": 0, "dy": 1, "value": -1},
{"dx": 0, "dy": 2, "value": -1},
],
x,
y,
e,
rot,
)
def _linear(x, y, _s, _rot, e, _f, g):
if not g:
return
last_row_index = g[-1]["rows"]
if y == last_row_index:
for target in (f"{y}-{x - 1}", f"{y}-{x + 1}"):
if target in e:
e[target] += 1
def _sight(x, y, _s, rot, e, _f, _g):
apply_offsets(
[{"dx": -1, "dy": -1}, {"dx": 1, "dy": 1, "value": -1}], x, y, e, rot
)
def _handshake(x, y, _s, rot, e, _f, _g):
apply_offsets([{"dx": 0, "dy": -1}, {"dx": 0, "dy": 1}], x, y, e, rot)
def _fate(x, y, _s, _rot, e, _f, _g):
apply_offsets([{"dx": 0, "dy": 1}], x, y, e, 0)
def _wit(x, y, _s, rot, e, _f, _g):
apply_offsets([{"dx": -1, "dy": -1}], x, y, e, rot)
def _exploitation(x, y, _s, rot, e, _f, _g):
apply_offsets(
[{"dx": 0, "dy": -1}, {"dx": 0, "dy": 1, "value": -1}], x, y, e, rot
)
def _unity(x, y, _s, rot, e, _f, _g):
apply_offsets(
[
{"dx": 1, "dy": 0},
{"dx": 0, "dy": 1},
{"dx": 0, "dy": -1, "value": -1},
{"dx": -1, "dy": 0, "value": -1},
],
x,
y,
e,
rot,
)
def _cheer(x, y, _s, _rot, e, _f, _g):
apply_offsets([{"dx": 0, "dy": -1}], x, y, e, 0)
def _hope(x, y, _s, rot, e, _f, _g):
apply_offsets([{"dx": 1, "dy": 0}], x, y, e, rot)
# ADVANCED
def _compete(x, y, _s, rot, e, _f, _g):
apply_offsets(
[
{"dx": 0, "dy": 1, "value": 2},
{"dx": 0, "dy": -1, "value": -1},
{"dx": -1, "dy": -1, "value": -1},
],
x,
y,
e,
rot,
)
def _beating(x, y, _s, rot, e, _f, _g):
apply_offsets([{"dx": 0, "dy": -2, "value": 2}], x, y, e, rot)
def _home_town(x, y, _s, rot, _e, f, _g):
ndx, ndy = rotate_offset(1, 0, rot)
target = f"{y + ndy}-{x + ndx}"
if f is not None and target in f:
f[target] = "ignore"
def _past(x, y, _s, rot, e, _f, _g):
apply_offsets(
[
{"dx": -1, "dy": -1},
{"dx": 0, "dy": -1},
{"dx": 1, "dy": -1},
{"dx": 1, "dy": 0},
],
x,
y,
e,
rot,
)
def _future(x, y, _s, rot, e, _f, _g):
apply_offsets(
[
{"dx": -1, "dy": -1},
{"dx": 0, "dy": -1},
{"dx": 1, "dy": -1},
{"dx": -1, "dy": 0},
],
x,
y,
e,
rot,
)
def _distribution(x, y, _s, _rot, e, _f, _g):
apply_offsets(
[
{"dx": 0, "dy": -1},
{"dx": -1, "dy": 0},
{"dx": 1, "dy": 0},
{"dx": 0, "dy": 1},
],
x,
y,
e,
0,
)
def _triceps(x, y, _s, _rot, e, _f, _g):
apply_offsets(
[{"dx": 0, "dy": -1}, {"dx": -1, "dy": 0}, {"dx": 1, "dy": 0}], x, y, e, 0
)
def _harvesting(x, y, _s, rot, e, _f, _g):
apply_offsets(
[{"dx": 0, "dy": 1, "value": 2}, {"dx": 0, "dy": -1, "value": 2}],
x,
y,
e,
rot,
)
def _binary_star(x, y, _s, rot, e, _f, _g):
apply_offsets(
[{"dx": 0, "dy": 2, "value": 2}, {"dx": 0, "dy": -2, "value": 2}],
x,
y,
e,
rot,
)
def _nurture(x, y, _s, rot, e, _f, _g):
apply_offsets(
[
{"dx": -1, "dy": -1},
{"dx": 0, "dy": -1},
{"dx": 1, "dy": -1},
{"dx": 0, "dy": 1, "value": -1},
{"dx": 0, "dy": 2, "value": -1},
],
x,
y,
e,
rot,
)
def _yearning(x, y, _s, rot, e, _f, _g):
apply_offsets([{"dx": 0, "dy": -1, "value": 2}], x, y, e, rot)
def _agglutination(x, y, slot, rot, e, _f, g):
if not g:
return
if rot in (1, 3):
for row in g:
if x < row["cols"]:
target = f"{row['rows']}-{x}"
if target != slot and target in e:
e[target] += -1
else:
c = cols_in_row(g, y)
for i in range(c):
target = f"{y}-{i}"
if target != slot and target in e:
e[target] += -1
apply_offsets([{"dx": 0, "dy": -1, "value": 3}], x, y, e, rot)
def _entrance(x, y, _s, _rot, e, _f, _g):
apply_offsets(
[
{"dx": 0, "dy": -1, "value": 2},
{"dx": -1, "dy": -1, "value": 1},
{"dx": 1, "dy": -1, "value": 1},
],
x,
y,
e,
0,
)
def _joke(x, y, _s, rot, e, _f, _g):
apply_offsets(
[
{"dx": 0, "dy": -1},
{"dx": 1, "dy": -1},
{"dx": -1, "dy": -1},
{"dx": -1, "dy": 0, "value": -1},
{"dx": 1, "dy": 0, "value": -1},
],
x,
y,
e,
rot,
)
def _load(x, y, _s, rot, e, _f, _g):
apply_offsets(
[
{"dx": 0, "dy": -1},
{"dx": -1, "dy": -1},
{"dx": 0, "dy": -2},
{"dx": -1, "dy": -2},
],
x,
y,
e,
rot,
)
def _transition(x, y, slot, rot, e, _f, g):
if not g:
return
if rot in (1, 3):
horizontal_value, vertical_value = -1, 1
else:
horizontal_value, vertical_value = 1, -1
c = cols_in_row(g, y)
for i in range(c):
target = f"{y}-{i}"
if target != slot and target in e:
e[target] += horizontal_value
for row in g:
if x < row["cols"]:
target = f"{row['rows']}-{x}"
if target != slot and target in e:
e[target] += vertical_value
def _advance(x, y, _s, rot, e, _f, _g):
apply_offsets(
[{"dx": 0, "dy": -1}, {"dx": 0, "dy": -2}, {"dx": 0, "dy": -3}], x, y, e, rot
)
def _justice(x, y, slot, _rot, e, _f, g):
if not g:
return
c = cols_in_row(g, y)
is_edge = x == 0 or x == c - 1
if not is_edge:
return
for row in g:
if x < row["cols"]:
target = f"{row['rows']}-{x}"
if target != slot and target in e:
e[target] += 1
def _preparation(x, y, _s, rot, e, _f, _g):
apply_offsets(
[{"dx": -1, "dy": -1}, {"dx": 1, "dy": 1}], x, y, e, rot
)
def _exit(x, y, _s, rot, e, _f, _g):
apply_offsets(
[
{"dx": -1, "dy": 1},
{"dx": 0, "dy": 1, "value": 2},
{"dx": 1, "dy": 1},
],
x,
y,
e,
rot,
)
def _tide(x, y, _s, rot, e, _f, _g):
apply_offsets(
[
{"dx": 1, "dy": -1, "value": 2},
{"dx": 0, "dy": -1, "value": -1},
{"dx": 1, "dy": 0, "value": -1},
],
x,
y,
e,
rot,
)
def _dedication(x, y, _s, rot, e, _f, _g):
apply_offsets(
[
{"dx": 1, "dy": -1},
{"dx": -1, "dy": -1},
{"dx": 1, "dy": 1},
{"dx": -1, "dy": 1},
],
x,
y,
e,
rot,
)
def _honor(x, y, _s, rot, e, _f, _g):
apply_offsets(
[{"dx": 0, "dy": -1, "value": 2}, {"dx": -1, "dy": -2}], x, y, e, rot
)
# RARE
def _base(_x, y, slot, _rot, e, _f, g):
if not g:
return
c = cols_in_row(g, y)
for i in range(c):
target = f"{y}-{i}"
if target != slot and target in e:
e[target] += 1
def _warrant(x, y, _s, rot, e, _f, _g):
apply_offsets([{"dx": 0, "dy": -1, "value": 3}], x, y, e, rot)
def _disconnection(x, y, _s, rot, e, _f, _g):
apply_offsets(
[
{"dx": 0, "dy": -1, "value": 3},
{"dx": 0, "dy": 1, "value": 3},
{"dx": 1, "dy": 0, "value": -1},
{"dx": -1, "dy": 0, "value": -1},
],
x,
y,
e,
rot,
)
def _concurrency(x, _y, slot, _rot, e, _f, g):
if not g:
return
for row in g:
if x < row["cols"]:
target = f"{row['rows']}-{x}"
if target != slot and target in e:
e[target] += 1
def _vow(x, y, _s, rot, e, _f, _g):
apply_offsets(
[
{"dx": 0, "dy": -2, "value": 2},
{"dx": 0, "dy": 1},
{"dx": 0, "dy": -1},
{"dx": -1, "dy": 0},
{"dx": 1, "dy": 0},
],
x,
y,
e,
rot,
)
def _rebellion(x, y, _s, rot, e, _f, _g):
cx = -1 if rot in (1, 3) else 1
directions = [(cx, -1), (-cx, 1)]
for dx, dy in directions:
cur_x, cur_y = x, y
while True:
cur_x += dx
cur_y += dy
target = f"{cur_y}-{cur_x}"
if target in e:
e[target] += 1
else:
break
def _connection(x, y, _s, rot, e, f, _g):
# value
ndx, ndy = rotate_offset(0, -1, rot)
target_v = f"{y + ndy}-{x + ndx}"
if target_v in e:
e[target_v] += 2
# ignore
ndx, ndy = rotate_offset(0, 1, rot)
target_i = f"{y + ndy}-{x + ndx}"
if f is not None and target_i in f:
f[target_i] = "ignore"
def _shade(_x, y, _s, _rot, e, _f, g):
if not g:
return
if y != 0 or len(g) < 2:
return
last_row = g[-1]
second_to_last = g[-2]
last_row_index = last_row["rows"]
cols_last = last_row["cols"]
second_to_last_index = second_to_last["rows"]
cols_second = second_to_last["cols"]
for col_idx in range(cols_last):
target = f"{last_row_index}-{col_idx}"
if target in e:
e[target] += 1
if cols_second > cols_last:
for col_idx in range(cols_last, cols_second):
target = f"{second_to_last_index}-{col_idx}"
if target in e:
e[target] += 1
# LEGEND
def _thorn(x, y, _s, rot, e, _f, _g):
apply_offsets(
[
{"dx": 0, "dy": -1, "value": 2},
{"dx": 0, "dy": 1, "value": 2},
{"dx": -1, "dy": -1},
{"dx": -1, "dy": 0},
{"dx": -1, "dy": 1},
{"dx": 1, "dy": -1},
{"dx": 1, "dy": 0},
{"dx": 1, "dy": 1},
],
x,
y,
e,
rot,
)
def _boundary(_x, _y, _s, _rot, e, _f, g):
if not g:
return
def apply_row(row):
for col_idx in range(row["cols"]):
target = f"{row['rows']}-{col_idx}"
if target in e:
e[target] += 1
apply_row(g[0])
if len(g) > 1:
last = g[-1]
second = g[-2]
apply_row(last)
if second["cols"] > last["cols"]:
for col_idx in range(last["cols"], second["cols"]):
target = f"{second['rows']}-{col_idx}"
if target in e:
e[target] += 1
def _sheen(x, y, slot, rot, e, _f, g):
if not g:
return
if rot in (1, 3):
for row in g:
if x < row["cols"]:
target = f"{row['rows']}-{x}"
if target != slot and target in e:
e[target] += 1
else:
c = cols_in_row(g, y)
for i in range(c):
target = f"{y}-{i}"
if target != slot and target in e:
e[target] += 1
apply_offsets(
[{"dx": 0, "dy": -1, "value": 2}, {"dx": 0, "dy": 1, "value": 2}],
x,
y,
e,
rot,
)
def _miracle(x, y, slot, _rot, e, _f, g):
if not g:
return
for row in g:
if x < row["cols"]:
target = f"{row['rows']}-{x}"
if target != slot and target in e:
e[target] += 1
c = cols_in_row(g, y)
for i in range(c):
target = f"{y}-{i}"
if target != slot and target in e:
e[target] += 1
def _daydream(x, y, _s, rot, e, _f, _g):
apply_offsets(
[
{"dx": -1, "dy": -1},
{"dx": -1, "dy": -2},
{"dx": 1, "dy": -1},
{"dx": 1, "dy": -2},
{"dx": -1, "dy": 1},
{"dx": -1, "dy": 2},
{"dx": 1, "dy": 1},
{"dx": 1, "dy": 2},
],
x,
y,
e,
rot,
)
def _compression(x, y, _s, rot, e, _f, _g):
apply_offsets(
[
{"dx": 0, "dy": -1, "value": 3},
{"dx": 0, "dy": -2, "value": 2},
{"dx": 0, "dy": -3},
],
x,
y,
e,
rot,
)
def _certitude(x, y, _s, rot, e, _f, _g):
apply_offsets([{"dx": 0, "dy": -1, "value": 5}], x, y, e, rot)
def _hospitality(x, y, _s, rot, e, f, _g):
apply_offsets(
[
{"dx": 0, "dy": -1, "value": 1, "flag": "ignore"},
{"dx": -1, "dy": 0, "value": 2, "flag": "ignore"},
],
x,
y,
e,
rot,
f,
)
def _peace(x, y, _s, rot, e, _f, _g):
apply_offsets(
[{"dx": -1, "dy": 0, "value": 3}, {"dx": 1, "dy": 0, "value": 3}], x, y, e, rot
)
def _courage(x, y, _s, rot, e, _f, _g):
apply_offsets(
[
{"dx": -3, "dy": -3},
{"dx": -2, "dy": -2},
{"dx": -1, "dy": -1},
{"dx": 1, "dy": 1},
{"dx": 2, "dy": 2},
{"dx": 1, "dy": -1, "value": 2},
{"dx": -1, "dy": 1, "value": 2},
],
x,
y,
e,
rot,
)
HANDLERS: Dict[str, Handler] = {
# COMMON
"approximation": _approximation,
"dry": _dry,
"chivalry": _chivalry,
"advent": _advent,
"linear": _linear,
"sight": _sight,
"handshake": _handshake,
"fate": _fate,
"wit": _wit,
"exploitation": _exploitation,
"unity": _unity,
"cheer": _cheer,
"hope": _hope,
# ADVANCED
"compete": _compete,
"beating": _beating,
"home_town": _home_town,
"past": _past,
"future": _future,
"distribution": _distribution,
"triceps": _triceps,
"harvesting": _harvesting,
"binary_star": _binary_star,
"nurture": _nurture,
"yearning": _yearning,
"agglutination": _agglutination,
"entrance": _entrance,
"joke": _joke,
"load": _load,
"transition": _transition,
"advance": _advance,
"justice": _justice,
"preparation": _preparation,
"exit": _exit,
"tide": _tide,
"dedication": _dedication,
"honor": _honor,
# RARE
"base": _base,
"warrant": _warrant,
"disconnection": _disconnection,
"concurrency": _concurrency,
"vow": _vow,
"rebellion": _rebellion,
"connection": _connection,
"shade": _shade,
# LEGEND
"thorn": _thorn,
"boundary": _boundary,
"sheen": _sheen,
"miracle": _miracle,
"daydream": _daydream,
"compression": _compression,
"certitude": _certitude,
"hospitality": _hospitality,
"peace": _peace,
"courage": _courage,
}
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
@dataclass
class Placement:
value: str # slab value e.g. "chivalry"
slot_id: str # e.g. "2-3"
rotation: int = 0
def compute_effects(
placements: List[Placement], grid: GridConfig
) -> Tuple[Effects, Flags]:
"""Apply all placements' handlers to the grid and return resulting effects + flags."""
effects: Effects = {sid: 0 for sid in all_slot_ids(grid)}
flags: Flags = {sid: None for sid in all_slot_ids(grid)}
for p in placements:
y, x = (int(v) for v in p.slot_id.split("-"))
handler = HANDLERS.get(p.value)
if handler is None:
continue
handler(x, y, p.slot_id, p.rotation, effects, flags, grid)
return effects, flags
def score(placements: List[Placement], grid: GridConfig) -> int:
"""Score = sum of effects on slab-occupied cells (ignoring "ignore" flagged cells).
This represents the boost an artifact placed on each slab-cell would get.
Higher = better layout. Empty cells don't count.
"""
effects, flags = compute_effects(placements, grid)
total = 0
for p in placements:
if flags.get(p.slot_id) == "ignore":
continue
total += effects.get(p.slot_id, 0)
return total