- 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
915 lines
24 KiB
Python
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
|