"""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