Spaces:
Running
Running
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +6 -0
- engine/__init__.py +0 -0
- engine/__pycache__/__init__.cpython-312.pyc +0 -0
- engine/data/cards.json +0 -0
- engine/data/cards_compiled.json +0 -0
- engine/game/ACTIONS.md +48 -0
- engine/game/__init__.py +1 -0
- engine/game/__pycache__/__init__.cpython-312.pyc +0 -0
- engine/game/__pycache__/data_loader.cpython-312.pyc +0 -0
- engine/game/__pycache__/deck_utils.cpython-312.pyc +0 -0
- engine/game/__pycache__/desc_utils.cpython-312.pyc +0 -0
- engine/game/__pycache__/enums.cpython-312.pyc +0 -0
- engine/game/__pycache__/fast_logic.cpython-312.pyc +0 -0
- engine/game/__pycache__/game_state.cpython-312.pyc +0 -0
- engine/game/__pycache__/numba_utils.cpython-312.pyc +0 -0
- engine/game/__pycache__/player_state.cpython-312.pyc +0 -0
- engine/game/__pycache__/replay_manager.cpython-312.pyc +0 -0
- engine/game/__pycache__/serializer.cpython-312.pyc +0 -0
- engine/game/__pycache__/state_utils.cpython-312.pyc +0 -0
- engine/game/ai_compat.py +54 -0
- engine/game/data_loader.py +71 -0
- engine/game/deck_utils.py +95 -0
- engine/game/desc_utils.py +466 -0
- engine/game/enums.py +27 -0
- engine/game/fast_logic.py +2209 -0
- engine/game/fast_logic_backup.py +632 -0
- engine/game/game_state.py +3027 -0
- engine/game/mixins/__pycache__/action_mixin.cpython-312.pyc +0 -0
- engine/game/mixins/__pycache__/effect_mixin.cpython-312.pyc +3 -0
- engine/game/mixins/__pycache__/phase_mixin.cpython-312.pyc +0 -0
- engine/game/mixins/action_mixin.py +407 -0
- engine/game/mixins/effect_mixin.py +0 -0
- engine/game/mixins/phase_mixin.py +654 -0
- engine/game/numba_utils.py +71 -0
- engine/game/player_state.py +827 -0
- engine/game/replay_manager.py +302 -0
- engine/game/serializer.py +689 -0
- engine/game/state_utils.py +122 -0
- engine/lovecasim_engine.pyd +3 -0
- engine/models/__pycache__/ability.cpython-312.pyc +0 -0
- engine/models/__pycache__/card.cpython-312.pyc +0 -0
- engine/models/__pycache__/context_indices.cpython-312.pyc +0 -0
- engine/models/__pycache__/enums.cpython-312.pyc +0 -0
- engine/models/__pycache__/opcodes.cpython-312.pyc +0 -0
- engine/models/ability.py +1128 -0
- engine/models/card.py +104 -0
- engine/models/choice_types.py +21 -0
- engine/models/context_indices.py +51 -0
- engine/models/enums.py +161 -0
- engine/models/opcodes.py +129 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,9 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
engine/game/mixins/__pycache__/effect_mixin.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
engine/lovecasim_engine.pyd filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
engine/tests/cards/batches/__pycache__/test_auto_generated_strict_comprehensive.cpython-312-pytest-9.0.2.pyc filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
engine/tests/cards/batches/__pycache__/test_easy_wins_batch_1.cpython-312-pytest-9.0.2.pyc filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
engine/tests/cards/batches/__pycache__/test_easy_wins_batch_2.cpython-312-pytest-9.0.2.pyc filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
engine/tests/scenarios/__pycache__/test_all_qas.cpython-312-pytest-9.0.2.pyc filter=lfs diff=lfs merge=lfs -text
|
engine/__init__.py
ADDED
|
File without changes
|
engine/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (169 Bytes). View file
|
|
|
engine/data/cards.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
engine/data/cards_compiled.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
engine/game/ACTIONS.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Love Live TCG - Action Space Mapping
|
| 2 |
+
|
| 3 |
+
This document defines the 2,000-dimensional action space used by the game engine. Each ID corresponds to a specific atomic action in the game.
|
| 4 |
+
|
| 5 |
+
## Main Map (Sorted by ID)
|
| 6 |
+
|
| 7 |
+
| ID Range | Category | Description | Parameters |
|
| 8 |
+
| :--- | :--- | :--- | :--- |
|
| 9 |
+
| **0** | **System** | Pass / Confirm / Skip / Cancel | N/A |
|
| 10 |
+
| **1 - 180** | **Play Member** | Play a member card from hand to stage | `(Hand Index 0-59) * 3 + (Slot Index 0-2)` |
|
| 11 |
+
| **200 - 202** | **Ability** | Activate a member's 【起動】 ability | `(Slot Index 0-2)` |
|
| 12 |
+
| **300 - 359** | **Mulligan** | Toggle selection of a card in hand for mulligan | `(Hand Index 0-59)` |
|
| 13 |
+
| **400 - 459** | **Live Set** | Set a card from hand face-down in the live zone | `(Hand Index 0-59)` |
|
| 14 |
+
| **500 - 559** | **Select Hand** | Select a card in hand for an effect (e.g. discard) | `(Hand Index 0-59)` |
|
| 15 |
+
| **560 - 562** | **Select Stage** | Select a member or slot on your side of the stage | `(Slot Index 0-2)` |
|
| 16 |
+
| **570 - 579** | **Modal** | Select option N from a modal choice effect | `(Option Index 0-9)` |
|
| 17 |
+
| **580 - 585** | **Color Select** | Select a member color (Pk, Rd, Bl, Gn, Yl, Pr) | `0=Pk, 1=Rd, 2=Bl, 3=Gn, 4=Yl, 5=Pr` |
|
| 18 |
+
| **590 - 599** | **Triggers** | Choose which pending trigger to resolve next | `(Trigger Index 0-9)` |
|
| 19 |
+
| **600 - 659** | **Generic List** | Select from a dynamic list (Look Deck, Deck) | `(List Index 0-59)` |
|
| 20 |
+
| **660 - 719** | **Discard List** | Select a card from the discard pile (Recovery) | `(List Index 0-59)` |
|
| 21 |
+
| **720 - 759** | **Formation** | Formation/Order items | `(Index 0-39)` |
|
| 22 |
+
| **760 - 819** | **Success Lives**| Select from the Success Live (Score) pile | `(List Index 0-59)` |
|
| 23 |
+
| **820 - 822** | **Live Zone** | Select a specific slot in the Live Zone | `(Slot Index 0-2)` |
|
| 24 |
+
| **830 - 849** | **Energy Zone** | Select a specific card in the Energy Zone | `(Index 0-19)` |
|
| 25 |
+
| **850 - 909** | **Removed/Opp** | Select from Removed cards or Opponent's Hand | `(Index 0-59)` |
|
| 26 |
+
| **910 - 912** | **Perform** | Select which Live card to attempt performance | `(Live Zone Index 0-2)` |
|
| 27 |
+
| **1000 - 1999**| **Reserved** | Reserved for future expansion | N/A |
|
| 28 |
+
|
| 29 |
+
## Choice Type Mapping
|
| 30 |
+
|
| 31 |
+
The engine uses `pending_choices` to contextually interpret these IDs.
|
| 32 |
+
|
| 33 |
+
| Choice Type | ID Range Used | Interpretation |
|
| 34 |
+
| :--- | :--- | :--- |
|
| 35 |
+
| `TARGET_HAND` | 500 - 559 | Hand Index |
|
| 36 |
+
| `TARGET_MEMBER` | 560 - 562 | Stage Slot Index |
|
| 37 |
+
| `DISCARD_SELECT` | 500 - 559 | Hand Index |
|
| 38 |
+
| `SELECT_FROM_LIST` | 600 - 659 | Index in provided `cards` list |
|
| 39 |
+
| `SELECT_FROM_DISCARD`| 660 - 719 | Index in discard selection list |
|
| 40 |
+
| `TARGET_OPPONENT_MEMBER`| 600 - 602 | Opponent's Slot Index |
|
| 41 |
+
| `COLOR_SELECT` | 580 - 585 | Color index |
|
| 42 |
+
| `MODAL` | 570 - 579 | Option index |
|
| 43 |
+
|
| 44 |
+
## Design Philosophy
|
| 45 |
+
|
| 46 |
+
1. **Sparsity**: ranges are separated to allow the UI to easily map IDs to visual components without overlap.
|
| 47 |
+
2. **Compatibility**: The fixed size (2,000) is designed for MCTS and RL models to have a consistent output head.
|
| 48 |
+
3. **Extensibility**: Over 50% of the space is reserved for new card types or mechanics.
|
engine/game/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Love Live Card Game - AlphaZero Engine
|
engine/game/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (174 Bytes). View file
|
|
|
engine/game/__pycache__/data_loader.cpython-312.pyc
ADDED
|
Binary file (3.02 kB). View file
|
|
|
engine/game/__pycache__/deck_utils.cpython-312.pyc
ADDED
|
Binary file (3.78 kB). View file
|
|
|
engine/game/__pycache__/desc_utils.cpython-312.pyc
ADDED
|
Binary file (16.6 kB). View file
|
|
|
engine/game/__pycache__/enums.cpython-312.pyc
ADDED
|
Binary file (1.07 kB). View file
|
|
|
engine/game/__pycache__/fast_logic.cpython-312.pyc
ADDED
|
Binary file (53.5 kB). View file
|
|
|
engine/game/__pycache__/game_state.cpython-312.pyc
ADDED
|
Binary file (63.1 kB). View file
|
|
|
engine/game/__pycache__/numba_utils.cpython-312.pyc
ADDED
|
Binary file (1.93 kB). View file
|
|
|
engine/game/__pycache__/player_state.cpython-312.pyc
ADDED
|
Binary file (39.1 kB). View file
|
|
|
engine/game/__pycache__/replay_manager.cpython-312.pyc
ADDED
|
Binary file (10.5 kB). View file
|
|
|
engine/game/__pycache__/serializer.cpython-312.pyc
ADDED
|
Binary file (27.3 kB). View file
|
|
|
engine/game/__pycache__/state_utils.cpython-312.pyc
ADDED
|
Binary file (6.01 kB). View file
|
|
|
engine/game/ai_compat.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
AI Compatibility Layer
|
| 3 |
+
Handles optional dependencies like Numba and Torch gracefully.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import importlib
|
| 7 |
+
import sys
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def check_available(module_name: str) -> bool:
|
| 11 |
+
"""Check if a module is available without importing it fully."""
|
| 12 |
+
try:
|
| 13 |
+
if module_name in sys.modules:
|
| 14 |
+
return True
|
| 15 |
+
return importlib.util.find_spec(module_name) is not None
|
| 16 |
+
except (ImportError, AttributeError):
|
| 17 |
+
return False
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# 1. Numba Availability
|
| 21 |
+
JIT_AVAILABLE = check_available("numba")
|
| 22 |
+
|
| 23 |
+
if JIT_AVAILABLE:
|
| 24 |
+
from numba import njit, prange
|
| 25 |
+
else:
|
| 26 |
+
# No-op decorator fallback
|
| 27 |
+
def njit(*args, **kwargs):
|
| 28 |
+
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
|
| 29 |
+
return args[0] # Used as @njit
|
| 30 |
+
|
| 31 |
+
def decorator(func):
|
| 32 |
+
return func # Used as @njit(cache=True)
|
| 33 |
+
|
| 34 |
+
return decorator
|
| 35 |
+
|
| 36 |
+
prange = range
|
| 37 |
+
|
| 38 |
+
# 2. Torch Availability
|
| 39 |
+
TORCH_AVAILABLE = check_available("torch")
|
| 40 |
+
|
| 41 |
+
# 3. Execution Flags
|
| 42 |
+
# If this is True, the engine will attempt to use JIT/Batching
|
| 43 |
+
GLOBAL_AI_ENABLED = JIT_AVAILABLE
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def report_ai_status():
|
| 47 |
+
"""Print current AI acceleration status."""
|
| 48 |
+
status = "ENABLED" if GLOBAL_AI_ENABLED else "DISABLED (Legacy Mode)"
|
| 49 |
+
print(f"--- AI Acceleration: {status} ---")
|
| 50 |
+
if not JIT_AVAILABLE:
|
| 51 |
+
print(" [Note] Numba not found. Install with 'pip install numba' for 200x speedup.")
|
| 52 |
+
if not TORCH_AVAILABLE:
|
| 53 |
+
print(" [Note] Torch not found. AI training will be unavailable.")
|
| 54 |
+
print("---------------------------------")
|
engine/game/data_loader.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from typing import Any, Dict, Tuple
|
| 4 |
+
|
| 5 |
+
from pydantic import TypeAdapter
|
| 6 |
+
|
| 7 |
+
from engine.models.card import EnergyCard, LiveCard, MemberCard
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class CardDataLoader:
|
| 11 |
+
def __init__(self, json_path: str):
|
| 12 |
+
self.json_path = json_path
|
| 13 |
+
|
| 14 |
+
def load(self) -> Tuple[Dict[int, MemberCard], Dict[int, LiveCard], Dict[int, Any]]:
|
| 15 |
+
# Auto-detect compiled file
|
| 16 |
+
target_path = self.json_path
|
| 17 |
+
if target_path.endswith("cards.json"):
|
| 18 |
+
# Check for compiled file in the same directory, or in data/
|
| 19 |
+
compiled_path = target_path.replace("cards.json", "cards_compiled.json")
|
| 20 |
+
if os.path.exists(compiled_path):
|
| 21 |
+
target_path = compiled_path
|
| 22 |
+
else:
|
| 23 |
+
root_path = os.path.join(os.getcwd(), "data", "cards_compiled.json")
|
| 24 |
+
if os.path.exists(root_path):
|
| 25 |
+
target_path = root_path
|
| 26 |
+
|
| 27 |
+
# Fallback to relative path search if absolute fails (common in tests)
|
| 28 |
+
if not os.path.exists(target_path):
|
| 29 |
+
# Try assuming path is relative to project root
|
| 30 |
+
# But we don't know project root easily.
|
| 31 |
+
pass
|
| 32 |
+
|
| 33 |
+
# print(f"Loading card data from {target_path}...")
|
| 34 |
+
with open(target_path, "r", encoding="utf-8") as f:
|
| 35 |
+
data = json.load(f)
|
| 36 |
+
|
| 37 |
+
members = {}
|
| 38 |
+
lives = {}
|
| 39 |
+
energy = {}
|
| 40 |
+
|
| 41 |
+
if "member_db" in data:
|
| 42 |
+
# Compiled format (v1.0)
|
| 43 |
+
m_adapter = TypeAdapter(MemberCard)
|
| 44 |
+
l_adapter = TypeAdapter(LiveCard)
|
| 45 |
+
e_adapter = TypeAdapter(EnergyCard)
|
| 46 |
+
|
| 47 |
+
for k, v in data["member_db"].items():
|
| 48 |
+
members[int(k)] = m_adapter.validate_python(v)
|
| 49 |
+
|
| 50 |
+
for k, v in data["live_db"].items():
|
| 51 |
+
# print(f"Loading live {k}")
|
| 52 |
+
lives[int(k)] = l_adapter.validate_python(v)
|
| 53 |
+
# print(f"DEBUG: Internal live_db keys: {len(data['live_db'])}, loaded: {len(lives)}")
|
| 54 |
+
|
| 55 |
+
for k, v in data["energy_db"].items():
|
| 56 |
+
energy[int(k)] = e_adapter.validate_python(v)
|
| 57 |
+
|
| 58 |
+
# --- HOTFIXES REMOVED (Parser is now accurate) ---
|
| 59 |
+
for l in lives.values():
|
| 60 |
+
pass
|
| 61 |
+
|
| 62 |
+
else:
|
| 63 |
+
# Legacy raw format
|
| 64 |
+
# Since we removed runtime parsing from the engine to separate concerns,
|
| 65 |
+
# we cannot load raw cards anymore.
|
| 66 |
+
raise RuntimeError(
|
| 67 |
+
"Legacy cards.json format detected. Runtime parsing is disabled. "
|
| 68 |
+
"Please run 'uv run compiler/main.py' to generate 'data/cards_compiled.json'."
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
return members, lives, energy
|
engine/game/deck_utils.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
from collections import Counter
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def extract_deck_data(content: str, card_db: dict):
|
| 7 |
+
"""
|
| 8 |
+
Parses deck content (HTML or various text formats) to extract card IDs and quantities.
|
| 9 |
+
Returns (main_deck, energy_deck, type_counts, errors)
|
| 10 |
+
"""
|
| 11 |
+
# 1. Try HTML Structure (Deck Log)
|
| 12 |
+
# pattern matches: title="PL!xxx-yyy-zzz : NAME" ... <span class="num">N</span>
|
| 13 |
+
pattern_html = r'title="([^"]+?) :[^"]*"[^>]*>.*?class="num">(\d+)</span>'
|
| 14 |
+
matches = re.findall(pattern_html, content, re.DOTALL)
|
| 15 |
+
|
| 16 |
+
if not matches:
|
| 17 |
+
# Fallback 1: Text format "QTY x ID" (e.g., "4 x LL-bp3-001-R+")
|
| 18 |
+
text_pattern_1 = r"(\d+)\s*[xX]\s*([A-Za-z0-9!+\-+]+)"
|
| 19 |
+
matches_1 = re.findall(text_pattern_1, content)
|
| 20 |
+
if matches_1:
|
| 21 |
+
# Swap to (ID, Qty) format
|
| 22 |
+
matches = [(m[1], m[0]) for m in matches_1]
|
| 23 |
+
else:
|
| 24 |
+
# Fallback 2: Text format "ID x QTY" (e.g., "PL!S-bp2-022-L x 2")
|
| 25 |
+
text_pattern_2 = r"([A-Za-z0-9!+\-+]+)\s*[xX]\s*(\d+)"
|
| 26 |
+
matches_2 = re.findall(text_pattern_2, content)
|
| 27 |
+
if matches_2:
|
| 28 |
+
matches = matches_2
|
| 29 |
+
else:
|
| 30 |
+
# Fallback 3: Simple list of IDs (one per line)
|
| 31 |
+
# Matches strings like "PL!S-bp1-001-M" but avoids common words
|
| 32 |
+
# This is risky but useful for simple text files.
|
| 33 |
+
# Let's use a more specific regex for ID patterns.
|
| 34 |
+
id_pattern = r"([PL!|LL\-E][A-Za-z0-9!+\-+]+-[A-Za-z0-9!+\-+]+-[A-Za-z0-9!+\-+]+[A-Za-z0-9!+\-+]*)"
|
| 35 |
+
matches_3 = re.findall(id_pattern, content)
|
| 36 |
+
if matches_3:
|
| 37 |
+
# Count occurrences
|
| 38 |
+
counts = Counter(matches_3)
|
| 39 |
+
matches = [(cid, str(cnt)) for cid, cnt in counts.items()]
|
| 40 |
+
|
| 41 |
+
if not matches:
|
| 42 |
+
return [], [], {}, ["No recognizable card data found in content."]
|
| 43 |
+
|
| 44 |
+
main_deck = []
|
| 45 |
+
energy_deck = []
|
| 46 |
+
type_counts = {"Member": 0, "Live": 0, "Energy": 0, "Unknown": 0}
|
| 47 |
+
errors = []
|
| 48 |
+
|
| 49 |
+
for card_id, qty_str in matches:
|
| 50 |
+
try:
|
| 51 |
+
qty = int(qty_str)
|
| 52 |
+
except ValueError:
|
| 53 |
+
continue
|
| 54 |
+
|
| 55 |
+
card_id = card_id.strip()
|
| 56 |
+
|
| 57 |
+
# Determine Type from database
|
| 58 |
+
cdata = card_db.get(card_id, {})
|
| 59 |
+
ctype = cdata.get("type", "")
|
| 60 |
+
|
| 61 |
+
if "メンバー" in ctype or "Member" in ctype:
|
| 62 |
+
type_counts["Member"] += qty
|
| 63 |
+
elif "ライブ" in ctype or "Live" in ctype:
|
| 64 |
+
type_counts["Live"] += qty
|
| 65 |
+
elif "エネルギー" in ctype or "Energy" in ctype:
|
| 66 |
+
type_counts["Energy"] += qty
|
| 67 |
+
else:
|
| 68 |
+
type_counts["Unknown"] += qty
|
| 69 |
+
|
| 70 |
+
for _ in range(qty):
|
| 71 |
+
if "エネルギー" in ctype or "Energy" in ctype or card_id.startswith("LL-E"):
|
| 72 |
+
energy_deck.append(card_id)
|
| 73 |
+
else:
|
| 74 |
+
main_deck.append(card_id)
|
| 75 |
+
|
| 76 |
+
# Basic Validation
|
| 77 |
+
all_counts = Counter(main_deck + energy_deck)
|
| 78 |
+
for cid, count in all_counts.items():
|
| 79 |
+
if count > 4 and not cid.startswith("LL-E"):
|
| 80 |
+
# Some formats might have duplicates if listing line-by-line + QTY.
|
| 81 |
+
# We don't block here, just warn.
|
| 82 |
+
errors.append(f"Card limit exceeded: {cid} x{count} (Max 4)")
|
| 83 |
+
|
| 84 |
+
return main_deck, energy_deck, type_counts, errors
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def load_deck_from_file(file_path: str, card_db: dict):
|
| 88 |
+
"""Helper to read a file and parse it."""
|
| 89 |
+
if not os.path.exists(file_path):
|
| 90 |
+
return None, None, {}, [f"File {file_path} not found."]
|
| 91 |
+
|
| 92 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 93 |
+
content = f.read()
|
| 94 |
+
|
| 95 |
+
return extract_deck_data(content, card_db)
|
engine/game/desc_utils.py
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from engine.game.enums import Phase
|
| 2 |
+
from engine.game.state_utils import get_base_id
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def get_action_desc(a, gs):
|
| 6 |
+
"""
|
| 7 |
+
Generate clear, informative action descriptions.
|
| 8 |
+
Shows card names, costs, and ability sources for better user understanding.
|
| 9 |
+
"""
|
| 10 |
+
if gs is None:
|
| 11 |
+
return f"Action {a}"
|
| 12 |
+
|
| 13 |
+
# Handle both Python and Rust engine (PyGameState)
|
| 14 |
+
if hasattr(gs, "get_player"):
|
| 15 |
+
p_idx = gs.current_player
|
| 16 |
+
p = gs.get_player(p_idx)
|
| 17 |
+
else:
|
| 18 |
+
p = gs.active_player
|
| 19 |
+
p_idx = gs.current_player
|
| 20 |
+
|
| 21 |
+
member_db = gs.member_db
|
| 22 |
+
live_db = gs.live_db
|
| 23 |
+
|
| 24 |
+
# Helper to get from DB, handling int/str keys
|
| 25 |
+
def get_from_db(db, key, default=None):
|
| 26 |
+
if not db:
|
| 27 |
+
return default
|
| 28 |
+
if hasattr(db, "get"):
|
| 29 |
+
res = db.get(key)
|
| 30 |
+
if res is not None:
|
| 31 |
+
return res
|
| 32 |
+
return db.get(str(key), default)
|
| 33 |
+
try:
|
| 34 |
+
if key in db:
|
| 35 |
+
return db[key]
|
| 36 |
+
if str(key) in db:
|
| 37 |
+
return db[str(key)]
|
| 38 |
+
except:
|
| 39 |
+
pass
|
| 40 |
+
return default
|
| 41 |
+
|
| 42 |
+
# Helper to get card name
|
| 43 |
+
def get_card_name(cid, gs_override=None):
|
| 44 |
+
_gs = gs_override or gs
|
| 45 |
+
if cid < 0:
|
| 46 |
+
return "なし"
|
| 47 |
+
|
| 48 |
+
base_id = get_base_id(int(cid))
|
| 49 |
+
|
| 50 |
+
# Try all DBs with the helper
|
| 51 |
+
m = get_from_db(member_db, base_id)
|
| 52 |
+
if m:
|
| 53 |
+
name = get_v(m, "name", f"メンバー #{base_id}")
|
| 54 |
+
card_no = get_v(m, "card_no", "??")
|
| 55 |
+
return f"{name} ({card_no})"
|
| 56 |
+
|
| 57 |
+
l = get_from_db(live_db, base_id)
|
| 58 |
+
if l:
|
| 59 |
+
name = get_v(l, "name", f"ライブ #{base_id}")
|
| 60 |
+
card_no = get_v(l, "card_no", "??")
|
| 61 |
+
return f"{name} ({card_no})"
|
| 62 |
+
|
| 63 |
+
e = get_from_db(getattr(_gs, "energy_db", None), base_id)
|
| 64 |
+
if e:
|
| 65 |
+
name = get_v(e, "name", f"エネルギー #{base_id}")
|
| 66 |
+
card_no = get_v(e, "card_no", "??")
|
| 67 |
+
return f"{name} ({card_no})"
|
| 68 |
+
|
| 69 |
+
return f"カード #{cid}"
|
| 70 |
+
|
| 71 |
+
# Helpers for general property access
|
| 72 |
+
def get_v(obj, key, default=None):
|
| 73 |
+
if obj is None:
|
| 74 |
+
return default
|
| 75 |
+
if isinstance(obj, dict):
|
| 76 |
+
return obj.get(key, default)
|
| 77 |
+
return getattr(obj, key, default)
|
| 78 |
+
|
| 79 |
+
# Helper for pending choices
|
| 80 |
+
def get_top_pending():
|
| 81 |
+
if not gs.pending_choices:
|
| 82 |
+
return None, {}
|
| 83 |
+
choice_type, params = gs.pending_choices[0]
|
| 84 |
+
if isinstance(params, str):
|
| 85 |
+
import json
|
| 86 |
+
|
| 87 |
+
try:
|
| 88 |
+
return choice_type, json.loads(params)
|
| 89 |
+
except:
|
| 90 |
+
return choice_type, {}
|
| 91 |
+
return choice_type, params
|
| 92 |
+
|
| 93 |
+
# --- ACTION HANDLERS (Order Matters: Specific to General) ---
|
| 94 |
+
# The order of these blocks determines priority. Specific contextual handlers
|
| 95 |
+
# (like Color selection or Discard/Recover) must come BEFORE broad ranges
|
| 96 |
+
# to avoid being shadowed by more generic messages.
|
| 97 |
+
|
| 98 |
+
# 1. SPECIAL & UNIVERSAL ACTIONS
|
| 99 |
+
# --------------------------------------------------------------------------
|
| 100 |
+
|
| 101 |
+
# Action 0: Pass / Confirm / Skip
|
| 102 |
+
# Used for ending phases (Main, LiveSet, Mulligan) or skipping optional effects.
|
| 103 |
+
if a == 0:
|
| 104 |
+
if int(gs.phase) == int(Phase.MAIN):
|
| 105 |
+
return "【終了】メインフェイズを終了する"
|
| 106 |
+
if int(gs.phase) == int(Phase.LIVE_SET):
|
| 107 |
+
return "【確認】ライブカードをセットして続行"
|
| 108 |
+
if int(gs.phase) == int(Phase.LIVE_RESULT):
|
| 109 |
+
return "【進む】次へ進む"
|
| 110 |
+
if int(gs.phase) in (int(Phase.MULLIGAN_P1), int(Phase.MULLIGAN_P2)):
|
| 111 |
+
return "【確認】マリガンを実行"
|
| 112 |
+
choice_type, params = get_top_pending()
|
| 113 |
+
if choice_type:
|
| 114 |
+
source_name = params.get("source_member", "アビリティ")
|
| 115 |
+
return f"【スキップ】{source_name}の効果を使用しない"
|
| 116 |
+
return "【パス】何もしない"
|
| 117 |
+
|
| 118 |
+
# 2. SPECIFIC INTERACTIVE SELECTIONS (Must precede broad ranges)
|
| 119 |
+
# --------------------------------------------------------------------------
|
| 120 |
+
|
| 121 |
+
# 580-585: Color Selection
|
| 122 |
+
# Triggered by O_COLOR_SELECT. Maps to Pink, Red, Yellow, Green, Blue, Purple.
|
| 123 |
+
if 580 <= a <= 585:
|
| 124 |
+
colors = ["赤", "青", "緑", "黄", "紫", "ピンク"]
|
| 125 |
+
return f"【色選択】 {colors[a - 580]}"
|
| 126 |
+
|
| 127 |
+
# 560-562: Stage Slot Selection (Targeting/Wait)
|
| 128 |
+
# Triggered when choosing a slot on the player's own stage for an effect.
|
| 129 |
+
if 560 <= a <= 562:
|
| 130 |
+
idx = a - 560
|
| 131 |
+
areas = ["左", "センター", "右"]
|
| 132 |
+
cid = p.stage[idx]
|
| 133 |
+
name = "空エリア"
|
| 134 |
+
base_id = get_base_id(int(cid))
|
| 135 |
+
if cid >= 0:
|
| 136 |
+
m = get_from_db(member_db, base_id)
|
| 137 |
+
if m:
|
| 138 |
+
name = get_v(m, "name", "メン��ー")
|
| 139 |
+
|
| 140 |
+
desc = "選択"
|
| 141 |
+
choice_type, params = get_top_pending()
|
| 142 |
+
if choice_type:
|
| 143 |
+
if choice_type == "MOVE_MEMBER":
|
| 144 |
+
desc = "移動元"
|
| 145 |
+
elif choice_type == "TAP_MEMBER":
|
| 146 |
+
desc = "ウェイト"
|
| 147 |
+
elif choice_type in ("PLAY_MEMBER_FROM_HAND", "PLAY_MEMBER_FROM_DISCARD"):
|
| 148 |
+
desc = "に置く"
|
| 149 |
+
return f"【ステージ選択】 {areas[idx]}: {name}を{desc}"
|
| 150 |
+
|
| 151 |
+
# 500-509: Hand Card Selection (Discard/Recover)
|
| 152 |
+
# Triggered when specifically selecting a card from hand as an effect cost or target.
|
| 153 |
+
# Note: Normal playing of cards (1-180) is handled separately.
|
| 154 |
+
if 500 <= a <= 509:
|
| 155 |
+
idx = a - 500
|
| 156 |
+
if idx < len(p.hand):
|
| 157 |
+
cid = p.hand[idx]
|
| 158 |
+
name = get_card_name(cid)
|
| 159 |
+
desc = "選択"
|
| 160 |
+
choice_type, params = get_top_pending()
|
| 161 |
+
if choice_type:
|
| 162 |
+
if choice_type == "RECOVER_MEMBER":
|
| 163 |
+
desc = "回収"
|
| 164 |
+
elif choice_type == "DISCARD":
|
| 165 |
+
desc = "捨てる"
|
| 166 |
+
return f"【手札選択】 {name}を{desc}"
|
| 167 |
+
|
| 168 |
+
# 570-579: Mode Selection (Branching Effects)
|
| 169 |
+
# Triggered by O_SELECT_MODE when a card offers multiple choices (e.g., Mode A/B).
|
| 170 |
+
if 570 <= a <= 579:
|
| 171 |
+
mode_label = f"モード {a - 570 + 1}"
|
| 172 |
+
choice_type, params = get_top_pending()
|
| 173 |
+
if choice_type:
|
| 174 |
+
options = params.get("options", [])
|
| 175 |
+
if a - 570 < len(options):
|
| 176 |
+
mode_label = options[a - 570]
|
| 177 |
+
return f"【モード選択】 {mode_label}"
|
| 178 |
+
|
| 179 |
+
# 590-599: Ability Trigger/Resolution Order
|
| 180 |
+
# Triggered when multiple abilities are pending (e.g., [OnPlay] triggers).
|
| 181 |
+
if 590 <= a <= 599:
|
| 182 |
+
idx = a - 590
|
| 183 |
+
if idx < len(gs.triggered_abilities):
|
| 184 |
+
t = gs.triggered_abilities[idx]
|
| 185 |
+
if len(t) >= 2:
|
| 186 |
+
cid = getattr(t[2], "card_id", -1) if len(t) > 2 else -1
|
| 187 |
+
src_name = get_card_name(cid) if cid >= 0 else "不明"
|
| 188 |
+
return f"【能力解決】 {src_name}の効果を発動 ({idx + 1}/{len(gs.triggered_abilities)})"
|
| 189 |
+
return f"【能力解決】 ({idx + 1}/{len(gs.triggered_abilities)})"
|
| 190 |
+
|
| 191 |
+
# 820-822: Live Zone Targeting
|
| 192 |
+
if 820 <= a <= 822:
|
| 193 |
+
idx = a - 820
|
| 194 |
+
areas = ["左", "センター", "右"]
|
| 195 |
+
cid = p.live_zone[idx] if idx < len(p.live_zone) else -1
|
| 196 |
+
name = "なし"
|
| 197 |
+
if cid >= 0:
|
| 198 |
+
name = get_card_name(cid)
|
| 199 |
+
return f"【ライブ選択】 {areas[idx]}: {name}"
|
| 200 |
+
|
| 201 |
+
# 900-902: Performance Execution
|
| 202 |
+
# Standard action to clear a Live card in the Performance phase.
|
| 203 |
+
if 900 <= a <= 902:
|
| 204 |
+
idx = a - 900
|
| 205 |
+
areas = ["左", "センター", "右"]
|
| 206 |
+
cid = p.live_zone[idx] if idx < len(p.live_zone) else -1
|
| 207 |
+
name = "なし"
|
| 208 |
+
summary = "パフォーマンス"
|
| 209 |
+
if cid >= 0:
|
| 210 |
+
name = get_card_name(cid)
|
| 211 |
+
base_id = get_base_id(cid)
|
| 212 |
+
live = get_from_db(live_db, base_id)
|
| 213 |
+
if live:
|
| 214 |
+
abilities = get_v(live, "abilities", [])
|
| 215 |
+
if abilities:
|
| 216 |
+
raw_text = get_v(abilities[0], "raw_text", "").strip()
|
| 217 |
+
if raw_text:
|
| 218 |
+
summary = raw_text.split("\n")[0][:40]
|
| 219 |
+
if len(raw_text) > 40:
|
| 220 |
+
summary += "..."
|
| 221 |
+
return f"【パフォーマンス】 {areas[idx]}: {name} ({summary})"
|
| 222 |
+
|
| 223 |
+
# 600-719: Broad Choice Range (General Targets/Opponent)
|
| 224 |
+
if 600 <= a <= 719:
|
| 225 |
+
idx = a - 600
|
| 226 |
+
choice_type, params = get_top_pending()
|
| 227 |
+
if choice_type == "ORDER_DECK":
|
| 228 |
+
cards = params.get("cards", [])
|
| 229 |
+
if idx < len(cards):
|
| 230 |
+
return f"【並べ替え】 {get_card_name(cards[idx])}を一番上へ"
|
| 231 |
+
return "【確定】 並び替えを終了"
|
| 232 |
+
|
| 233 |
+
if 0 <= idx <= 2:
|
| 234 |
+
if choice_type in ("SELECT_MEMBER", "TARGET_OPPONENT_MEMBER"):
|
| 235 |
+
areas = ["左", "センター", "右"]
|
| 236 |
+
opp = gs.get_player(1 - p_idx) if hasattr(gs, "get_player") else gs.inactive_player
|
| 237 |
+
cid = opp.stage[idx]
|
| 238 |
+
name = "空エリア"
|
| 239 |
+
if cid >= 0:
|
| 240 |
+
base_id = get_base_id(int(cid))
|
| 241 |
+
m = get_from_db(member_db, base_id)
|
| 242 |
+
if m:
|
| 243 |
+
name = get_v(m, "name", "メンバー")
|
| 244 |
+
return f"【ターゲット】 相手のステージ ({areas[idx]}: {name}) を選択"
|
| 245 |
+
|
| 246 |
+
if choice_type == "SELECT_FROM_LIST":
|
| 247 |
+
cards = params.get("cards", [])
|
| 248 |
+
if idx < len(cards):
|
| 249 |
+
return f"【リスト選択】 {get_card_name(cards[idx])}"
|
| 250 |
+
elif choice_type == "SELECT_MODE":
|
| 251 |
+
options = params.get("options", [])
|
| 252 |
+
if idx < len(options):
|
| 253 |
+
return f"【選択】 {options[idx]}"
|
| 254 |
+
elif choice_type == "SELECT_SUCCESS_LIVE":
|
| 255 |
+
cards = params.get("cards", [])
|
| 256 |
+
if idx < len(cards):
|
| 257 |
+
return f"【獲得選択】 {get_card_name(cards[idx])}"
|
| 258 |
+
elif choice_type == "SELECT_FROM_DISCARD":
|
| 259 |
+
cards = params.get("cards", [])
|
| 260 |
+
if idx < len(cards):
|
| 261 |
+
return f"【控え室回収】 {get_card_name(cards[idx])}"
|
| 262 |
+
|
| 263 |
+
return f"【選択】 項目 {idx}"
|
| 264 |
+
|
| 265 |
+
# 550-849: Complex Choice Resolution (Consolidated)
|
| 266 |
+
# Used for choices that require specific card/slot context (e.g., Order Deck, Color Choose).
|
| 267 |
+
if 550 <= a <= 849:
|
| 268 |
+
adj = a - 550
|
| 269 |
+
area_idx = adj // 100
|
| 270 |
+
ab_idx = (adj % 100) // 10
|
| 271 |
+
choice_idx = adj % 10
|
| 272 |
+
|
| 273 |
+
area_name = ["左", "中", "右"][area_idx] if area_idx < 3 else f"Slot {area_idx}"
|
| 274 |
+
cid = p.stage[area_idx] if area_idx < 3 else -1
|
| 275 |
+
card_name = "メンバー"
|
| 276 |
+
if cid >= 0:
|
| 277 |
+
base_id = get_base_id(cid)
|
| 278 |
+
m = get_from_db(member_db, base_id)
|
| 279 |
+
if m:
|
| 280 |
+
card_name = get_v(m, "name", "メンバー")
|
| 281 |
+
|
| 282 |
+
choice_type, params = get_top_pending()
|
| 283 |
+
choice_label = f"選択 {choice_idx}"
|
| 284 |
+
if choice_type == "ORDER_DECK":
|
| 285 |
+
cards = params.get("cards", [])
|
| 286 |
+
if choice_idx < len(cards):
|
| 287 |
+
choice_label = f"デッキトップ: {get_card_name(cards[choice_idx])}"
|
| 288 |
+
else:
|
| 289 |
+
choice_label = "[確定]"
|
| 290 |
+
elif choice_type == "COLOR_SELECT":
|
| 291 |
+
colors = ["赤", "青", "緑", "黄", "紫", "ピンク"]
|
| 292 |
+
if choice_idx < len(colors):
|
| 293 |
+
choice_label = f"色選択: {colors[choice_idx]}"
|
| 294 |
+
elif choice_type == "SELECT_MODE":
|
| 295 |
+
options = params.get("options", [])
|
| 296 |
+
if choice_idx < len(options):
|
| 297 |
+
choice_label = options[choice_idx]
|
| 298 |
+
else:
|
| 299 |
+
choice_label = f"モード {choice_idx + 1}"
|
| 300 |
+
elif choice_type == "SELECT_FROM_LIST":
|
| 301 |
+
cards = params.get("cards", [])
|
| 302 |
+
if choice_idx < len(cards):
|
| 303 |
+
choice_label = f"選択: {get_card_name(cards[choice_idx])}"
|
| 304 |
+
|
| 305 |
+
return f"[{card_name}] {choice_label} ({area_name})"
|
| 306 |
+
|
| 307 |
+
# 3. PHASE-SPECIFIC CORE ACTIONS (Main Range)
|
| 308 |
+
# --------------------------------------------------------------------------
|
| 309 |
+
|
| 310 |
+
# 1-180: Playing Members (Main Phase)
|
| 311 |
+
# Each hand index has 3 target slots: aid = 1 + (hand_idx * 3) + slot_idx
|
| 312 |
+
if 1 <= a <= 180 and int(gs.phase) == int(Phase.MAIN):
|
| 313 |
+
idx = (a - 1) // 3
|
| 314 |
+
area_idx = (a - 1) % 3
|
| 315 |
+
areas = ["左", "センター", "右"]
|
| 316 |
+
area_name = areas[area_idx]
|
| 317 |
+
card_name = f"カード[{idx}]"
|
| 318 |
+
new_card_cost = 0
|
| 319 |
+
suffix = ""
|
| 320 |
+
if idx < len(p.hand):
|
| 321 |
+
cid = p.hand[idx]
|
| 322 |
+
base_cid = get_base_id(int(cid))
|
| 323 |
+
m = get_from_db(member_db, base_cid)
|
| 324 |
+
if m:
|
| 325 |
+
card_name = get_v(m, "name", "メンバー")
|
| 326 |
+
new_card_cost = get_v(m, "cost", 0)
|
| 327 |
+
abilities = get_v(m, "abilities", [])
|
| 328 |
+
if any(get_v(ab, "trigger", 0) == 1 for ab in abilities):
|
| 329 |
+
suffix = " [登場]"
|
| 330 |
+
|
| 331 |
+
stage_cid = p.stage[area_idx]
|
| 332 |
+
if stage_cid >= 0:
|
| 333 |
+
base_stage_cid = get_base_id(int(stage_cid))
|
| 334 |
+
old_card = get_from_db(member_db, base_stage_cid)
|
| 335 |
+
if old_card:
|
| 336 |
+
old_name = get_v(old_card, "name", "メンバー")
|
| 337 |
+
old_cost = get_v(old_card, "cost", 0)
|
| 338 |
+
actual_cost = max(0, new_card_cost - old_cost)
|
| 339 |
+
return f"【{area_name}に置く】 {card_name}{suffix} (バトンタッチ: {old_name}退場, 支払:{actual_cost})"
|
| 340 |
+
return f"【{area_name}に置く】 {card_name}{suffix} (コスト {new_card_cost})"
|
| 341 |
+
|
| 342 |
+
# 100-159: Energy Charge Selection
|
| 343 |
+
if 100 <= a <= 159 and int(gs.phase) == int(Phase.ENERGY):
|
| 344 |
+
idx = a - 100
|
| 345 |
+
card_name = f"手札[{idx}]"
|
| 346 |
+
if idx < len(p.hand):
|
| 347 |
+
card_name = get_card_name(p.hand[idx])
|
| 348 |
+
return f"【エネルギー】 {card_name}をチャージ"
|
| 349 |
+
|
| 350 |
+
# 300-359: Mulligan Selection
|
| 351 |
+
# Toggles cards to be shuffled back during the mulligan phase.
|
| 352 |
+
if 300 <= a <= 359:
|
| 353 |
+
idx = a - 300
|
| 354 |
+
card_name = f"手札[{idx}]"
|
| 355 |
+
if idx < len(p.hand):
|
| 356 |
+
card_name = get_card_name(p.hand[idx])
|
| 357 |
+
return f"【マリガン】 {card_name}を選択/解除"
|
| 358 |
+
|
| 359 |
+
# 400-459: Live Set Selection
|
| 360 |
+
# Sets a Live card from hand to face-up or face-down zone.
|
| 361 |
+
if 400 <= a <= 459:
|
| 362 |
+
idx = a - 400
|
| 363 |
+
card_name = f"手札[{idx}]"
|
| 364 |
+
score_text = ""
|
| 365 |
+
if idx < len(p.hand):
|
| 366 |
+
cid = p.hand[idx]
|
| 367 |
+
card_name = get_card_name(cid)
|
| 368 |
+
base_id = get_base_id(cid)
|
| 369 |
+
live = get_from_db(live_db, base_id)
|
| 370 |
+
return f"【ライブセット】 {card_name}{score_text}"
|
| 371 |
+
|
| 372 |
+
# 200-299: Activated Ability on Stage
|
| 373 |
+
# [起動] abilities triggered manually by the player.
|
| 374 |
+
if 200 <= a <= 299:
|
| 375 |
+
adj = a - 200
|
| 376 |
+
area_idx = adj // 10
|
| 377 |
+
ab_idx = adj % 10
|
| 378 |
+
areas = ["左", "センター", "右"]
|
| 379 |
+
area_name = areas[area_idx] if area_idx < 3 else f"Slot {area_idx}"
|
| 380 |
+
cid = p.stage[area_idx] if area_idx < 3 else -1
|
| 381 |
+
if cid >= 0:
|
| 382 |
+
base_cid = get_base_id(int(cid))
|
| 383 |
+
member = get_from_db(member_db, base_cid)
|
| 384 |
+
if member:
|
| 385 |
+
card_name = get_v(member, "name", "メンバー")
|
| 386 |
+
abilities = get_v(member, "abilities", [])
|
| 387 |
+
summary = "アビリティ"
|
| 388 |
+
if len(abilities) > ab_idx:
|
| 389 |
+
ab = abilities[ab_idx]
|
| 390 |
+
raw_text = get_v(ab, "raw_text", "").strip()
|
| 391 |
+
if raw_text:
|
| 392 |
+
summary = raw_text.split("\n")[0][:40]
|
| 393 |
+
if len(raw_text) > 40:
|
| 394 |
+
summary += "..."
|
| 395 |
+
return f"【起動】{card_name}: {summary} ({area_name})"
|
| 396 |
+
return f"Ability ({area_name})"
|
| 397 |
+
|
| 398 |
+
# 2000-2999: Discard Pile Activation
|
| 399 |
+
# Playing or activating effects of cards currently in the discard zone.
|
| 400 |
+
if 2000 <= a <= 2999:
|
| 401 |
+
adj = a - 2000
|
| 402 |
+
discard_idx = adj // 10
|
| 403 |
+
ab_idx = adj % 10
|
| 404 |
+
card_name = f"控え室[{discard_idx}]"
|
| 405 |
+
if discard_idx < len(p.discard):
|
| 406 |
+
cid = p.discard[discard_idx]
|
| 407 |
+
card_name = get_card_name(cid)
|
| 408 |
+
base_id = get_base_id(cid)
|
| 409 |
+
member = get_from_db(member_db, base_id)
|
| 410 |
+
if member:
|
| 411 |
+
abilities = get_v(member, "abilities", [])
|
| 412 |
+
if len(abilities) > ab_idx:
|
| 413 |
+
ab = abilities[ab_idx]
|
| 414 |
+
raw_text = get_v(ab, "raw_text", "").strip()
|
| 415 |
+
summary = raw_text.split("\n")[0][:40] if raw_text else "効果"
|
| 416 |
+
return f"【控え召喚】 {card_name}: {summary}"
|
| 417 |
+
return f"【控え召喚】 {card_name}"
|
| 418 |
+
|
| 419 |
+
# 1000-1999: OnPlay Sub-Choices
|
| 420 |
+
# Options offered immediately during/after a member is played.
|
| 421 |
+
if 1000 <= a <= 1999:
|
| 422 |
+
adj = a - 1000
|
| 423 |
+
hand_idx = adj // 100
|
| 424 |
+
slot_idx = (adj % 100) // 10
|
| 425 |
+
choice_idx = adj % 10
|
| 426 |
+
|
| 427 |
+
choice_label = f"選択 {choice_idx}"
|
| 428 |
+
|
| 429 |
+
choice_type, params = get_top_pending()
|
| 430 |
+
if choice_type == "ORDER_DECK":
|
| 431 |
+
cards = params.get("cards", [])
|
| 432 |
+
if choice_idx < len(cards):
|
| 433 |
+
choice_label = f"{get_card_name(cards[choice_idx])}をトップへ"
|
| 434 |
+
else:
|
| 435 |
+
choice_label = "[確定]"
|
| 436 |
+
elif choice_type == "COLOR_SELECT":
|
| 437 |
+
colors = ["赤", "青", "緑", "黄", "紫", "ピンク"]
|
| 438 |
+
if choice_idx < len(colors):
|
| 439 |
+
choice_label = f"{colors[choice_idx]}を選択"
|
| 440 |
+
elif choice_type == "SELECT_MODE":
|
| 441 |
+
options = params.get("options", [])
|
| 442 |
+
if choice_idx < len(options):
|
| 443 |
+
choice_label = options[choice_idx]
|
| 444 |
+
else:
|
| 445 |
+
choice_label = f"モード {choice_idx + 1}"
|
| 446 |
+
elif choice_type == "SELECT_FROM_LIST":
|
| 447 |
+
cards = params.get("cards", [])
|
| 448 |
+
if choice_idx < len(cards):
|
| 449 |
+
choice_label = f"{get_card_name(cards[choice_idx])}を選択"
|
| 450 |
+
|
| 451 |
+
return choice_label
|
| 452 |
+
|
| 453 |
+
# 4. FALLBACKS
|
| 454 |
+
# --------------------------------------------------------------------------
|
| 455 |
+
|
| 456 |
+
# 510-559: Generic Hand Selection Fallback
|
| 457 |
+
if 510 <= a <= 559:
|
| 458 |
+
idx = a - 500 # Keep idx logic consistent with 500-509
|
| 459 |
+
card_name = f"手札[{idx}]"
|
| 460 |
+
if idx < len(p.hand):
|
| 461 |
+
card_name = get_card_name(p.hand[idx])
|
| 462 |
+
return f"【手札選択】 {card_name}"
|
| 463 |
+
|
| 464 |
+
return f"Action {a}"
|
| 465 |
+
|
| 466 |
+
return f"Action {a}"
|
engine/game/enums.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from enum import IntEnum
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class Phase(IntEnum):
|
| 5 |
+
"""Game phases within a turn (Rule 7).
|
| 6 |
+
|
| 7 |
+
Flow: MULLIGAN_P1 -> MULLIGAN_P2 -> ACTIVE -> ENERGY -> DRAW -> MAIN
|
| 8 |
+
-> LIVE_SET -> PERFORMANCE_P1 -> PERFORMANCE_P2 -> LIVE_RESULT
|
| 9 |
+
-> ACTIVE (next turn)
|
| 10 |
+
|
| 11 |
+
Note: SETUP (-2) is reserved for potential future use (pregame setup).
|
| 12 |
+
Games currently start directly at MULLIGAN_P1.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
SETUP = -2
|
| 16 |
+
MULLIGAN_P1 = -1
|
| 17 |
+
MULLIGAN_P2 = 0
|
| 18 |
+
ACTIVE = 1
|
| 19 |
+
ENERGY = 2
|
| 20 |
+
DRAW = 3
|
| 21 |
+
MAIN = 4
|
| 22 |
+
LIVE_SET = 5
|
| 23 |
+
PERFORMANCE_P1 = 6
|
| 24 |
+
PERFORMANCE_P2 = 7
|
| 25 |
+
LIVE_RESULT = 8
|
| 26 |
+
TERMINAL = 9
|
| 27 |
+
RESPONSE = 10
|
engine/game/fast_logic.py
ADDED
|
@@ -0,0 +1,2209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import warnings
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
from numba import njit, prange
|
| 5 |
+
|
| 6 |
+
# Suppress Numba cache warnings (harmless in testing environments)
|
| 7 |
+
warnings.filterwarnings("ignore", message=".*'cache' is set for njit and is ignored.*")
|
| 8 |
+
|
| 9 |
+
# =============================================================================
|
| 10 |
+
# HYPER-OPTIMIZED VM CORE (Production Version)
|
| 11 |
+
# =============================================================================
|
| 12 |
+
|
| 13 |
+
# ContextIndex Mappings (Raw Ints)
|
| 14 |
+
CV = 20
|
| 15 |
+
AT = 22
|
| 16 |
+
TS = 12
|
| 17 |
+
TI = 13
|
| 18 |
+
SZ = 7
|
| 19 |
+
CH = 15
|
| 20 |
+
SID = 5
|
| 21 |
+
|
| 22 |
+
# Backward compatibility aliases
|
| 23 |
+
CTX_VALUE = CV
|
| 24 |
+
CTX_ATTR = AT
|
| 25 |
+
CTX_TARGET_SLOT = TS
|
| 26 |
+
CTX_TARGET_PLAYER_ID = TI
|
| 27 |
+
CTX_SOURCE_ZONE_IDX = SZ
|
| 28 |
+
CTX_CHOICE_INDEX = CH
|
| 29 |
+
|
| 30 |
+
# GlobalContext Mappings
|
| 31 |
+
SC = 0
|
| 32 |
+
OS = 1
|
| 33 |
+
TR = 2
|
| 34 |
+
HD = 3
|
| 35 |
+
DI = 4
|
| 36 |
+
EN = 5
|
| 37 |
+
DK = 6
|
| 38 |
+
OT = 7
|
| 39 |
+
OT = 7
|
| 40 |
+
PH = 8
|
| 41 |
+
LS = 110 # Live Score Bonus (Temporary, moved to 110 to avoid conflict with OD)
|
| 42 |
+
EH = 111 # Excess Hearts (Current Live Performance)
|
| 43 |
+
PREV_CID_IDX = 60 # Previous Card ID for Baton Pass (Index 60)
|
| 44 |
+
|
| 45 |
+
# Unique ID (UID) System
|
| 46 |
+
BASE_ID_MASK = 0xFFFFF
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@njit(inline="always")
|
| 50 |
+
def get_base_id(uid: int) -> int:
|
| 51 |
+
"""Extract the base card definition ID (0-1999) from a UID."""
|
| 52 |
+
return uid & BASE_ID_MASK
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
# Opcodes
|
| 56 |
+
O_DRAW = 10
|
| 57 |
+
O_BLADES = 11
|
| 58 |
+
O_HEARTS = 12
|
| 59 |
+
O_REDUCE_COST = 13
|
| 60 |
+
O_LOOK_DECK = 14
|
| 61 |
+
O_RECOV_L = 15
|
| 62 |
+
O_BOOST = 16
|
| 63 |
+
O_RECOV_M = 17
|
| 64 |
+
O_BUFF = 18
|
| 65 |
+
O_MOVE_MEMBER = 20
|
| 66 |
+
O_SWAP_CARDS = 21
|
| 67 |
+
O_SEARCH_DECK = 22
|
| 68 |
+
O_CHARGE = 23
|
| 69 |
+
O_ORDER_DECK = 28
|
| 70 |
+
O_SET_BLADES = 24
|
| 71 |
+
O_SELECT_MODE = 30
|
| 72 |
+
O_TAP_O = 32
|
| 73 |
+
O_PLACE_UNDER = 33
|
| 74 |
+
O_LOOK_AND_CHOOSE = 41
|
| 75 |
+
O_ACTIVATE_MEMBER = 43
|
| 76 |
+
O_ADD_H = 44
|
| 77 |
+
O_REPLACE_EFFECT = 46
|
| 78 |
+
O_TRIGGER_REMOTE = 47
|
| 79 |
+
O_TRANSFORM_COLOR = 39
|
| 80 |
+
O_TAP_M = 53
|
| 81 |
+
O_REDUCE_HEART_REQ = 48
|
| 82 |
+
O_REVEAL_CARDS = 26
|
| 83 |
+
O_MOVE_TO_DISCARD = 58
|
| 84 |
+
O_RETURN = 1
|
| 85 |
+
O_JUMP = 2
|
| 86 |
+
O_JUMP_F = 3
|
| 87 |
+
O_REVEAL_HAND_ALL = 6 # Cost Type (Internal reminder)
|
| 88 |
+
|
| 89 |
+
# Conditions
|
| 90 |
+
C_TR1 = 200
|
| 91 |
+
C_CLR = 202
|
| 92 |
+
C_STG = 203
|
| 93 |
+
C_HND = 204
|
| 94 |
+
C_CTR = 206
|
| 95 |
+
C_LLD = 207
|
| 96 |
+
C_GRP = 208
|
| 97 |
+
C_OPH = 210
|
| 98 |
+
C_ENR = 213
|
| 99 |
+
C_OPH = 210
|
| 100 |
+
C_ENR = 213
|
| 101 |
+
C_CMP = 220
|
| 102 |
+
C_BLD = 224
|
| 103 |
+
C_HRT = 223
|
| 104 |
+
C_HAS_CHOICE = 221
|
| 105 |
+
C_OPPONENT_CHOICE = 222
|
| 106 |
+
C_BATON = 231 # Baton Pass condition (ID 231)
|
| 107 |
+
|
| 108 |
+
# Conditions
|
| 109 |
+
C_TR1 = 200
|
| 110 |
+
C_CLR = 202
|
| 111 |
+
C_STG = 203
|
| 112 |
+
C_HND = 204
|
| 113 |
+
C_CTR = 206
|
| 114 |
+
C_LLD = 207
|
| 115 |
+
C_GRP = 208
|
| 116 |
+
C_OPH = 210
|
| 117 |
+
C_ENR = 213
|
| 118 |
+
C_OPH = 210
|
| 119 |
+
C_ENR = 213
|
| 120 |
+
C_CMP = 220
|
| 121 |
+
C_BLD = 224
|
| 122 |
+
C_HRT = 223
|
| 123 |
+
C_HAS_CHOICE = 221
|
| 124 |
+
C_OPPONENT_CHOICE = 222
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
@njit(nopython=True, cache=True, fastmath=True)
|
| 128 |
+
def resolve_bytecode(
|
| 129 |
+
bytecode,
|
| 130 |
+
flat_ctx,
|
| 131 |
+
global_ctx,
|
| 132 |
+
player_id,
|
| 133 |
+
p_hand,
|
| 134 |
+
p_deck,
|
| 135 |
+
p_stage,
|
| 136 |
+
p_energy_vec,
|
| 137 |
+
p_energy_count,
|
| 138 |
+
p_cont_vec,
|
| 139 |
+
out_cptr, # Modified: Pass by ref array (size 1)
|
| 140 |
+
p_tapped,
|
| 141 |
+
p_live,
|
| 142 |
+
opp_tapped,
|
| 143 |
+
p_trash, # Added
|
| 144 |
+
b_map,
|
| 145 |
+
b_idx,
|
| 146 |
+
out_bonus, # Modified: Pass by ref array (size 1)
|
| 147 |
+
card_stats, # Added: NumPy array for card properties
|
| 148 |
+
opp_stage, # Added
|
| 149 |
+
opp_tapped_count=None, # Optional tracking
|
| 150 |
+
):
|
| 151 |
+
# Manual Stack Unrolling (Depth 4) - Eliminates allocations in hot loop
|
| 152 |
+
stack_ptr = 0
|
| 153 |
+
|
| 154 |
+
bc0 = bytecode
|
| 155 |
+
ip0 = 0
|
| 156 |
+
bc1 = bytecode
|
| 157 |
+
ip1 = 0
|
| 158 |
+
bc2 = bytecode
|
| 159 |
+
ip2 = 0
|
| 160 |
+
bc3 = bytecode
|
| 161 |
+
ip3 = 0
|
| 162 |
+
|
| 163 |
+
cptr = out_cptr[0]
|
| 164 |
+
cond = True
|
| 165 |
+
safety_counter = 0
|
| 166 |
+
|
| 167 |
+
while stack_ptr >= 0 and safety_counter < 1000:
|
| 168 |
+
safety_counter += 1
|
| 169 |
+
|
| 170 |
+
# Current Frame Selection (Type-stable for Numba)
|
| 171 |
+
if stack_ptr == 0:
|
| 172 |
+
cur_bc = bc0
|
| 173 |
+
ip = ip0
|
| 174 |
+
elif stack_ptr == 1:
|
| 175 |
+
cur_bc = bc1
|
| 176 |
+
ip = ip1
|
| 177 |
+
elif stack_ptr == 2:
|
| 178 |
+
cur_bc = bc2
|
| 179 |
+
ip = ip2
|
| 180 |
+
else:
|
| 181 |
+
cur_bc = bc3
|
| 182 |
+
ip = ip3
|
| 183 |
+
|
| 184 |
+
blen = cur_bc.shape[0]
|
| 185 |
+
|
| 186 |
+
if ip >= blen:
|
| 187 |
+
stack_ptr -= 1
|
| 188 |
+
continue
|
| 189 |
+
|
| 190 |
+
op = cur_bc[ip, 0]
|
| 191 |
+
v = cur_bc[ip, 1]
|
| 192 |
+
a = cur_bc[ip, 2]
|
| 193 |
+
s = cur_bc[ip, 3]
|
| 194 |
+
|
| 195 |
+
# Advance IP logic: Pre-increment local copy
|
| 196 |
+
next_ip = ip + 1
|
| 197 |
+
if stack_ptr == 0:
|
| 198 |
+
ip0 = next_ip
|
| 199 |
+
elif stack_ptr == 1:
|
| 200 |
+
ip1 = next_ip
|
| 201 |
+
elif stack_ptr == 2:
|
| 202 |
+
ip2 = next_ip
|
| 203 |
+
else:
|
| 204 |
+
ip3 = next_ip
|
| 205 |
+
|
| 206 |
+
if op == 0:
|
| 207 |
+
continue
|
| 208 |
+
if op == O_RETURN:
|
| 209 |
+
stack_ptr -= 1
|
| 210 |
+
continue
|
| 211 |
+
|
| 212 |
+
# Negation Flag Handling
|
| 213 |
+
is_negated = False
|
| 214 |
+
if op >= 1000:
|
| 215 |
+
is_negated = True
|
| 216 |
+
op -= 1000
|
| 217 |
+
|
| 218 |
+
# Dynamic Target Handling (MEMBER_SELECT)
|
| 219 |
+
if s == 10:
|
| 220 |
+
s = int(flat_ctx[TS])
|
| 221 |
+
|
| 222 |
+
if op == O_JUMP:
|
| 223 |
+
new_ip = ip + v
|
| 224 |
+
if stack_ptr == 0:
|
| 225 |
+
ip0 = new_ip if 0 <= new_ip < blen else blen
|
| 226 |
+
elif stack_ptr == 1:
|
| 227 |
+
ip1 = new_ip if 0 <= new_ip < blen else blen
|
| 228 |
+
elif stack_ptr == 2:
|
| 229 |
+
ip2 = new_ip if 0 <= new_ip < blen else blen
|
| 230 |
+
else:
|
| 231 |
+
ip3 = new_ip if 0 <= new_ip < blen else blen
|
| 232 |
+
continue
|
| 233 |
+
|
| 234 |
+
if op == O_JUMP_F:
|
| 235 |
+
if not cond:
|
| 236 |
+
new_ip = ip + v
|
| 237 |
+
if stack_ptr == 0:
|
| 238 |
+
ip0 = new_ip if 0 <= new_ip < blen else blen
|
| 239 |
+
elif stack_ptr == 1:
|
| 240 |
+
ip1 = new_ip if 0 <= new_ip < blen else blen
|
| 241 |
+
elif stack_ptr == 2:
|
| 242 |
+
ip2 = new_ip if 0 <= new_ip < blen else blen
|
| 243 |
+
else:
|
| 244 |
+
ip3 = new_ip if 0 <= new_ip < blen else blen
|
| 245 |
+
continue
|
| 246 |
+
continue
|
| 247 |
+
|
| 248 |
+
if op == O_SELECT_MODE:
|
| 249 |
+
choice = int(flat_ctx[CH])
|
| 250 |
+
jumped = False
|
| 251 |
+
if 0 <= choice < v:
|
| 252 |
+
jump_ip = ip + 1 + choice
|
| 253 |
+
if jump_ip < blen:
|
| 254 |
+
offset = cur_bc[jump_ip, 1]
|
| 255 |
+
new_ip = jump_ip + offset
|
| 256 |
+
if 0 <= new_ip < blen:
|
| 257 |
+
if stack_ptr == 0:
|
| 258 |
+
ip0 = new_ip
|
| 259 |
+
elif stack_ptr == 1:
|
| 260 |
+
ip1 = new_ip
|
| 261 |
+
elif stack_ptr == 2:
|
| 262 |
+
ip2 = new_ip
|
| 263 |
+
else:
|
| 264 |
+
ip3 = new_ip
|
| 265 |
+
jumped = True
|
| 266 |
+
if not jumped:
|
| 267 |
+
new_ip = ip + (v + 1)
|
| 268 |
+
if stack_ptr == 0:
|
| 269 |
+
ip0 = new_ip
|
| 270 |
+
elif stack_ptr == 1:
|
| 271 |
+
ip1 = new_ip
|
| 272 |
+
elif stack_ptr == 2:
|
| 273 |
+
ip2 = new_ip
|
| 274 |
+
else:
|
| 275 |
+
ip3 = new_ip
|
| 276 |
+
continue
|
| 277 |
+
|
| 278 |
+
if op >= 200:
|
| 279 |
+
if op == C_TR1:
|
| 280 |
+
cond = global_ctx[TR] == 1
|
| 281 |
+
elif op == C_STG:
|
| 282 |
+
ct = 0
|
| 283 |
+
for i in range(3):
|
| 284 |
+
if p_stage[i] != -1:
|
| 285 |
+
ct += 1
|
| 286 |
+
cond = ct >= v
|
| 287 |
+
elif op == C_HND:
|
| 288 |
+
cond = global_ctx[HD] >= v
|
| 289 |
+
elif op == C_LLD:
|
| 290 |
+
cond = global_ctx[SC] > global_ctx[OS]
|
| 291 |
+
elif op == C_CLR:
|
| 292 |
+
if 0 <= a <= 5:
|
| 293 |
+
cond = global_ctx[10 + a] > 0
|
| 294 |
+
else:
|
| 295 |
+
cond = False
|
| 296 |
+
elif op == C_GRP:
|
| 297 |
+
# C_GRP: Count cards of a group on stage or live zone
|
| 298 |
+
# v = min count, a = group ID, s = zone (0=stage, 1=live)
|
| 299 |
+
target_group = a
|
| 300 |
+
grp_count = 0
|
| 301 |
+
if s == 0: # Stage
|
| 302 |
+
for slot_k in range(3):
|
| 303 |
+
cid = p_stage[slot_k]
|
| 304 |
+
if cid >= 0:
|
| 305 |
+
bid = get_base_id(cid)
|
| 306 |
+
if bid < card_stats.shape[0]:
|
| 307 |
+
# Groups are stored at index 6 (primary group)
|
| 308 |
+
if card_stats[bid, 6] == target_group:
|
| 309 |
+
grp_count += 1
|
| 310 |
+
elif s == 1: # Live Zone
|
| 311 |
+
for lz_k in range(p_live.shape[0]):
|
| 312 |
+
cid = p_live[lz_k]
|
| 313 |
+
if cid >= 0:
|
| 314 |
+
bid = get_base_id(cid)
|
| 315 |
+
if bid < card_stats.shape[0]:
|
| 316 |
+
if card_stats[bid, 6] == target_group:
|
| 317 |
+
grp_count += 1
|
| 318 |
+
cond = grp_count >= v
|
| 319 |
+
elif op == C_BLD:
|
| 320 |
+
# C_BLD: Check if total blades on stage meets requirement
|
| 321 |
+
# Rule 9.9: Tapped (Resting) members do not contribute.
|
| 322 |
+
total_blades_check = 0
|
| 323 |
+
for slot_k in range(3):
|
| 324 |
+
cid = p_stage[slot_k]
|
| 325 |
+
if cid >= 0 and p_tapped[slot_k] == 0:
|
| 326 |
+
bid = get_base_id(cid)
|
| 327 |
+
if bid < card_stats.shape[0]:
|
| 328 |
+
total_blades_check += card_stats[bid, 1] # Blades at index 1
|
| 329 |
+
|
| 330 |
+
# Decode slot/comparison
|
| 331 |
+
real_slot = s & 0x0F
|
| 332 |
+
comp = (s >> 4) & 0x0F
|
| 333 |
+
if comp == 0:
|
| 334 |
+
cond = total_blades_check >= v
|
| 335 |
+
elif comp == 1:
|
| 336 |
+
cond = total_blades_check <= v
|
| 337 |
+
elif comp == 2:
|
| 338 |
+
cond = total_blades_check > v
|
| 339 |
+
elif comp == 3:
|
| 340 |
+
cond = total_blades_check < v
|
| 341 |
+
else:
|
| 342 |
+
cond = total_blades_check == v
|
| 343 |
+
elif op == C_HRT:
|
| 344 |
+
# C_HRT: Check if total hearts of color `a` on stage meets requirement
|
| 345 |
+
total_hearts_check = 0
|
| 346 |
+
|
| 347 |
+
# Decode slot/comparison
|
| 348 |
+
real_slot = s & 0x0F
|
| 349 |
+
comp = (s >> 4) & 0x0F
|
| 350 |
+
|
| 351 |
+
if real_slot == 2: # Live Result / Excess Hearts
|
| 352 |
+
total_hearts_check = global_ctx[EH]
|
| 353 |
+
else:
|
| 354 |
+
for slot_k in range(3):
|
| 355 |
+
cid = p_stage[slot_k]
|
| 356 |
+
# Rule 9.9: Tapped members do not contribute hearts either.
|
| 357 |
+
if cid >= 0 and p_tapped[slot_k] == 0:
|
| 358 |
+
bid = get_base_id(cid)
|
| 359 |
+
if bid < card_stats.shape[0]:
|
| 360 |
+
if 0 <= a <= 6:
|
| 361 |
+
total_hearts_check += card_stats[bid, 12 + a] # Hearts at 12-18
|
| 362 |
+
else:
|
| 363 |
+
# Sum all hearts
|
| 364 |
+
for h_k in range(7):
|
| 365 |
+
total_hearts_check += card_stats[bid, 12 + h_k]
|
| 366 |
+
|
| 367 |
+
if comp == 0:
|
| 368 |
+
cond = total_hearts_check >= v
|
| 369 |
+
elif comp == 1:
|
| 370 |
+
cond = total_hearts_check <= v
|
| 371 |
+
elif comp == 2:
|
| 372 |
+
cond = total_hearts_check > v
|
| 373 |
+
elif comp == 3:
|
| 374 |
+
cond = total_hearts_check < v
|
| 375 |
+
else:
|
| 376 |
+
cond = total_hearts_check == v
|
| 377 |
+
elif op == C_ENR:
|
| 378 |
+
cond = global_ctx[EN] >= v
|
| 379 |
+
elif op == C_CTR:
|
| 380 |
+
cond = flat_ctx[SZ] == 1
|
| 381 |
+
elif op == C_CMP:
|
| 382 |
+
if v > 0:
|
| 383 |
+
cond = global_ctx[SC] >= v
|
| 384 |
+
else:
|
| 385 |
+
cond = global_ctx[SC] > global_ctx[OS]
|
| 386 |
+
elif op == C_OPH:
|
| 387 |
+
ct = global_ctx[OT]
|
| 388 |
+
if v > 0:
|
| 389 |
+
cond = ct >= v
|
| 390 |
+
else:
|
| 391 |
+
cond = ct > 0
|
| 392 |
+
elif op == 230: # C_LIVE_ZONE
|
| 393 |
+
# Placeholder: correctly count cards in successful live zone if needed
|
| 394 |
+
cond = True
|
| 395 |
+
elif op == 212: # C_MODAL_ANSWER
|
| 396 |
+
cond = global_ctx[CH] == v
|
| 397 |
+
elif op == C_HAS_CHOICE:
|
| 398 |
+
cond = True
|
| 399 |
+
elif op == C_OPPONENT_CHOICE:
|
| 400 |
+
# Check if opponent tapped ANY card this turn (safe approximation)
|
| 401 |
+
# We check the passed `opp_tapped` array.
|
| 402 |
+
cond = False
|
| 403 |
+
for k in range(3):
|
| 404 |
+
if opp_tapped[k] > 0:
|
| 405 |
+
cond = True
|
| 406 |
+
break
|
| 407 |
+
elif op == C_BATON:
|
| 408 |
+
# Baton Pass condition: Check if the previous card's Character ID matches target
|
| 409 |
+
prev_cid = global_ctx[PREV_CID_IDX]
|
| 410 |
+
if prev_cid >= 0:
|
| 411 |
+
prev_bid = get_base_id(prev_cid)
|
| 412 |
+
if prev_bid < card_stats.shape[0]:
|
| 413 |
+
# Check if the previous card's Character ID (stored in stats index 19) matches the target
|
| 414 |
+
cond = card_stats[prev_bid, 19] == v
|
| 415 |
+
else:
|
| 416 |
+
cond = False
|
| 417 |
+
else:
|
| 418 |
+
cond = False
|
| 419 |
+
|
| 420 |
+
if is_negated:
|
| 421 |
+
cond = not cond
|
| 422 |
+
|
| 423 |
+
if not cond:
|
| 424 |
+
stack_ptr -= 1
|
| 425 |
+
continue
|
| 426 |
+
else:
|
| 427 |
+
if cond:
|
| 428 |
+
if op == O_DRAW:
|
| 429 |
+
drawn = 0
|
| 430 |
+
for d_idx in range(60):
|
| 431 |
+
if p_deck[d_idx] > 0:
|
| 432 |
+
card_id = p_deck[d_idx]
|
| 433 |
+
p_deck[d_idx] = 0
|
| 434 |
+
global_ctx[DK] -= 1
|
| 435 |
+
for h_idx in range(60):
|
| 436 |
+
if p_hand[h_idx] == 0:
|
| 437 |
+
p_hand[h_idx] = card_id
|
| 438 |
+
global_ctx[HD] += 1
|
| 439 |
+
break
|
| 440 |
+
drawn += 1
|
| 441 |
+
if drawn >= v:
|
| 442 |
+
break
|
| 443 |
+
elif op == O_TRANSFORM_COLOR:
|
| 444 |
+
# Rule 11.12: Transformation effects.
|
| 445 |
+
# Add to continuous effects buffer.
|
| 446 |
+
# Entry: [type, value, attr, slot, p_id, duration, ...]
|
| 447 |
+
# and TRANSFORM_COLOR usually lasts until LIVE_END (duration=2 in this implementation?)
|
| 448 |
+
# attr is target_color index
|
| 449 |
+
if (
|
| 450 |
+
cptr < p_cont_vec.shape[0]
|
| 451 |
+
): # Changed from p_ptr[0] to cptr, and p_cont_vec.shape[1] to p_cont_vec.shape[0]
|
| 452 |
+
idx = cptr
|
| 453 |
+
p_cont_vec[idx, 0] = O_TRANSFORM_COLOR
|
| 454 |
+
p_cont_vec[idx, 1] = v
|
| 455 |
+
p_cont_vec[idx, 2] = a
|
| 456 |
+
p_cont_vec[idx, 3] = s
|
| 457 |
+
p_cont_vec[idx, 4] = player_id
|
| 458 |
+
p_cont_vec[idx, 5] = 2 # Duration: LIVE_END
|
| 459 |
+
cptr += 1 # Changed from p_ptr[0] += 1 to cptr += 1
|
| 460 |
+
elif op == O_CHARGE:
|
| 461 |
+
charged = 0
|
| 462 |
+
for d_idx in range(60):
|
| 463 |
+
if p_deck[d_idx] > 0:
|
| 464 |
+
card_id = p_deck[d_idx]
|
| 465 |
+
p_deck[d_idx] = 0
|
| 466 |
+
global_ctx[DK] -= 1
|
| 467 |
+
if 0 <= s < 3:
|
| 468 |
+
for e_idx in range(32):
|
| 469 |
+
if p_energy_vec[s, e_idx] == 0:
|
| 470 |
+
p_energy_vec[s, e_idx] = card_id
|
| 471 |
+
p_energy_count[s] += 1
|
| 472 |
+
# Rule 10.6: Energy under members does NOT count as global energy
|
| 473 |
+
# global_ctx[EN] += 1 <-- REMOVED
|
| 474 |
+
break
|
| 475 |
+
charged += 1
|
| 476 |
+
if charged >= v:
|
| 477 |
+
break
|
| 478 |
+
elif op == O_BLADES:
|
| 479 |
+
if s >= 0 and cptr < 32:
|
| 480 |
+
p_cont_vec[cptr, 0] = 1
|
| 481 |
+
p_cont_vec[cptr, 1] = v
|
| 482 |
+
p_cont_vec[cptr, 2] = 4
|
| 483 |
+
p_cont_vec[cptr, 3] = s
|
| 484 |
+
p_cont_vec[cptr, 8] = int(flat_ctx[SID])
|
| 485 |
+
p_cont_vec[cptr, 9] = 1
|
| 486 |
+
cptr += 1
|
| 487 |
+
elif op == O_HEARTS:
|
| 488 |
+
if cptr < 32:
|
| 489 |
+
p_cont_vec[cptr, 0] = 2
|
| 490 |
+
p_cont_vec[cptr, 1] = v
|
| 491 |
+
p_cont_vec[cptr, 5] = a
|
| 492 |
+
p_cont_vec[cptr, 8] = int(flat_ctx[SID])
|
| 493 |
+
p_cont_vec[cptr, 9] = 1
|
| 494 |
+
cptr += 1
|
| 495 |
+
if 10 + a < 128:
|
| 496 |
+
global_ctx[10 + a] += v
|
| 497 |
+
elif op == O_REDUCE_COST:
|
| 498 |
+
if cptr < 32:
|
| 499 |
+
p_cont_vec[cptr, 0] = 3
|
| 500 |
+
p_cont_vec[cptr, 1] = v
|
| 501 |
+
p_cont_vec[cptr, 2] = s
|
| 502 |
+
p_cont_vec[cptr, 8] = int(flat_ctx[SID])
|
| 503 |
+
p_cont_vec[cptr, 9] = 1
|
| 504 |
+
cptr += 1
|
| 505 |
+
elif op == O_REDUCE_HEART_REQ:
|
| 506 |
+
if cptr < 32:
|
| 507 |
+
p_cont_vec[cptr, 0] = 48
|
| 508 |
+
p_cont_vec[cptr, 1] = v
|
| 509 |
+
p_cont_vec[cptr, 2] = s
|
| 510 |
+
p_cont_vec[cptr, 8] = int(flat_ctx[SID])
|
| 511 |
+
p_cont_vec[cptr, 9] = 1
|
| 512 |
+
cptr += 1
|
| 513 |
+
elif op == O_SET_BLADES:
|
| 514 |
+
if s >= 0 and cptr < 32:
|
| 515 |
+
p_cont_vec[cptr, 0] = 24
|
| 516 |
+
p_cont_vec[cptr, 1] = v
|
| 517 |
+
p_cont_vec[cptr, 2] = s
|
| 518 |
+
p_cont_vec[cptr, 8] = int(flat_ctx[SID])
|
| 519 |
+
p_cont_vec[cptr, 9] = 1
|
| 520 |
+
cptr += 1
|
| 521 |
+
elif op == O_REPLACE_EFFECT:
|
| 522 |
+
if cptr < 32:
|
| 523 |
+
p_cont_vec[cptr, 0] = 46
|
| 524 |
+
p_cont_vec[cptr, 1] = v
|
| 525 |
+
p_cont_vec[cptr, 2] = s
|
| 526 |
+
p_cont_vec[cptr, 9] = 1
|
| 527 |
+
cptr += 1
|
| 528 |
+
elif op == O_LOOK_DECK:
|
| 529 |
+
# Look deck in VM just processes the look (Revealing cards to a buffer)
|
| 530 |
+
# For now, we skip if it's purely for selection (handled by 41)
|
| 531 |
+
# Implementation: No-op for state, but placeholder for side-effects
|
| 532 |
+
continue
|
| 533 |
+
elif op == O_REVEAL_CARDS:
|
| 534 |
+
# Reveal cards (placeholder for state-based effects)
|
| 535 |
+
continue
|
| 536 |
+
elif op == O_RECOV_L:
|
| 537 |
+
# Heuristic: Recover highest ID Live Card
|
| 538 |
+
best_idx = -1
|
| 539 |
+
best_id = -1
|
| 540 |
+
for tr_k in range(p_trash.shape[0]):
|
| 541 |
+
tid = p_trash[tr_k]
|
| 542 |
+
if tid > 0:
|
| 543 |
+
tbid = get_base_id(tid)
|
| 544 |
+
if tbid < card_stats.shape[0] and card_stats[tbid, 10] == 2: # Live
|
| 545 |
+
if tid > best_id:
|
| 546 |
+
best_id = tid
|
| 547 |
+
best_idx = tr_k
|
| 548 |
+
|
| 549 |
+
if best_idx != -1:
|
| 550 |
+
# Move to hand
|
| 551 |
+
moved = False
|
| 552 |
+
for h_idx in range(60):
|
| 553 |
+
if p_hand[h_idx] == 0:
|
| 554 |
+
p_hand[h_idx] = best_id
|
| 555 |
+
global_ctx[HD] += 1
|
| 556 |
+
p_trash[best_idx] = 0
|
| 557 |
+
global_ctx[4] -= 1 # TR
|
| 558 |
+
moved = True
|
| 559 |
+
break
|
| 560 |
+
if not moved:
|
| 561 |
+
# Hand full: Fallback to discard or stay in trash (which it already is)
|
| 562 |
+
continue
|
| 563 |
+
|
| 564 |
+
elif op == O_RECOV_M:
|
| 565 |
+
# Heuristic: Recover highest ID Member Card
|
| 566 |
+
best_idx = -1
|
| 567 |
+
best_id = -1
|
| 568 |
+
for tr_k in range(p_trash.shape[0]):
|
| 569 |
+
tid = p_trash[tr_k]
|
| 570 |
+
if tid > 0:
|
| 571 |
+
tbid = get_base_id(tid)
|
| 572 |
+
if tbid < card_stats.shape[0] and card_stats[tbid, 10] == 1: # Member
|
| 573 |
+
# Optional: Add Cost/Power check if needed, for now Max ID is decent proxy
|
| 574 |
+
if tid > best_id:
|
| 575 |
+
best_id = tid
|
| 576 |
+
best_idx = tr_k
|
| 577 |
+
|
| 578 |
+
if best_idx != -1:
|
| 579 |
+
# Move to hand
|
| 580 |
+
moved = False
|
| 581 |
+
for h_idx in range(60):
|
| 582 |
+
if p_hand[h_idx] == 0:
|
| 583 |
+
p_hand[h_idx] = best_id
|
| 584 |
+
global_ctx[HD] += 1
|
| 585 |
+
p_trash[best_idx] = 0
|
| 586 |
+
global_ctx[4] -= 1 # TR
|
| 587 |
+
moved = True
|
| 588 |
+
break
|
| 589 |
+
elif op == O_ACTIVATE_MEMBER:
|
| 590 |
+
if 0 <= s < 3:
|
| 591 |
+
p_tapped[s] = 0
|
| 592 |
+
elif op == O_SWAP_CARDS:
|
| 593 |
+
removed = 0
|
| 594 |
+
for h_idx in range(60):
|
| 595 |
+
if p_hand[h_idx] > 0:
|
| 596 |
+
cid = p_hand[h_idx]
|
| 597 |
+
p_hand[h_idx] = 0
|
| 598 |
+
global_ctx[HD] -= 1
|
| 599 |
+
removed += 1
|
| 600 |
+
if removed >= v:
|
| 601 |
+
break
|
| 602 |
+
|
| 603 |
+
if s == 0: # Only draw if mode is 0 (Swap). s=1 implies Discard Only cost.
|
| 604 |
+
drawn = 0
|
| 605 |
+
for d_idx in range(60):
|
| 606 |
+
if p_deck[d_idx] > 0:
|
| 607 |
+
card_id = p_deck[d_idx]
|
| 608 |
+
p_deck[d_idx] = 0
|
| 609 |
+
for h_idx in range(60):
|
| 610 |
+
if p_hand[h_idx] == 0:
|
| 611 |
+
p_hand[h_idx] = card_id
|
| 612 |
+
break
|
| 613 |
+
global_ctx[DK] -= 1
|
| 614 |
+
global_ctx[HD] += 1
|
| 615 |
+
drawn += 1
|
| 616 |
+
if drawn >= v:
|
| 617 |
+
break
|
| 618 |
+
|
| 619 |
+
elif op == O_PLACE_UNDER:
|
| 620 |
+
placed = 0
|
| 621 |
+
if a == 1: # From Energy Zone
|
| 622 |
+
for _ in range(v):
|
| 623 |
+
if global_ctx[EN] > 0:
|
| 624 |
+
global_ctx[EN] -= 1
|
| 625 |
+
if 0 <= s < 3:
|
| 626 |
+
for e_idx in range(32):
|
| 627 |
+
if p_energy_vec[s, e_idx] == 0:
|
| 628 |
+
p_energy_vec[s, e_idx] = 2000 # Dummy energy card
|
| 629 |
+
p_energy_count[s] += 1
|
| 630 |
+
break
|
| 631 |
+
placed += 1
|
| 632 |
+
if placed >= v:
|
| 633 |
+
break
|
| 634 |
+
else: # From Hand (Default)
|
| 635 |
+
for h_idx in range(59, -1, -1):
|
| 636 |
+
if p_hand[h_idx] > 0:
|
| 637 |
+
cid = p_hand[h_idx]
|
| 638 |
+
p_hand[h_idx] = 0
|
| 639 |
+
global_ctx[HD] -= 1
|
| 640 |
+
if 0 <= s < 3:
|
| 641 |
+
for e_idx in range(32):
|
| 642 |
+
if p_energy_vec[s, e_idx] == 0:
|
| 643 |
+
p_energy_vec[s, e_idx] = cid
|
| 644 |
+
p_energy_count[s] += 1
|
| 645 |
+
break
|
| 646 |
+
placed += 1
|
| 647 |
+
if placed >= v:
|
| 648 |
+
break
|
| 649 |
+
|
| 650 |
+
elif op == O_MOVE_MEMBER:
|
| 651 |
+
dest_slot = int(flat_ctx[TS])
|
| 652 |
+
if 0 <= s < 3 and 0 <= dest_slot < 3 and s != dest_slot:
|
| 653 |
+
temp_id = p_stage[s]
|
| 654 |
+
p_stage[s] = p_stage[dest_slot]
|
| 655 |
+
p_stage[dest_slot] = temp_id
|
| 656 |
+
temp_tap = p_tapped[s]
|
| 657 |
+
p_tapped[s] = p_tapped[dest_slot]
|
| 658 |
+
p_tapped[dest_slot] = temp_tap
|
| 659 |
+
for e_idx in range(32):
|
| 660 |
+
temp_e = p_energy_vec[s, e_idx]
|
| 661 |
+
p_energy_vec[s, e_idx] = p_energy_vec[dest_slot, e_idx]
|
| 662 |
+
p_energy_vec[dest_slot, e_idx] = temp_e
|
| 663 |
+
temp_ec = p_energy_count[s]
|
| 664 |
+
p_energy_count[s] = p_energy_count[dest_slot]
|
| 665 |
+
p_energy_count[dest_slot] = temp_ec
|
| 666 |
+
|
| 667 |
+
elif op == O_TAP_M:
|
| 668 |
+
# Tap self or other member (usually based on TargetSlot/s)
|
| 669 |
+
if 0 <= s < 3:
|
| 670 |
+
p_tapped[s] = True
|
| 671 |
+
elif (
|
| 672 |
+
s == 10
|
| 673 |
+
): # TargetSlot 10 usually means select manually, but in Numba we might just tap all if instructed
|
| 674 |
+
p_tapped[:] = True
|
| 675 |
+
|
| 676 |
+
elif op == O_TAP_O:
|
| 677 |
+
is_all = (a & 0x80) != 0
|
| 678 |
+
c_max = v
|
| 679 |
+
b_max = a & 0x7F
|
| 680 |
+
# Decode real slot
|
| 681 |
+
real_slot = s & 0x0F
|
| 682 |
+
|
| 683 |
+
for slot_k in range(3):
|
| 684 |
+
if not is_all and slot_k != real_slot:
|
| 685 |
+
continue
|
| 686 |
+
|
| 687 |
+
cid = opp_stage[slot_k]
|
| 688 |
+
if cid >= 0:
|
| 689 |
+
bid = get_base_id(cid)
|
| 690 |
+
if bid < card_stats.shape[0]:
|
| 691 |
+
# Filter checks
|
| 692 |
+
if c_max != 99 and card_stats[bid, 0] > c_max:
|
| 693 |
+
continue
|
| 694 |
+
if b_max != 99 and card_stats[bid, 1] > b_max:
|
| 695 |
+
continue
|
| 696 |
+
|
| 697 |
+
opp_tapped[slot_k] = 1
|
| 698 |
+
elif op == O_BUFF:
|
| 699 |
+
if cptr < 32:
|
| 700 |
+
p_cont_vec[cptr, 0] = 8
|
| 701 |
+
p_cont_vec[cptr, 1] = v
|
| 702 |
+
p_cont_vec[cptr, 2] = s
|
| 703 |
+
p_cont_vec[cptr, 8] = int(flat_ctx[SID])
|
| 704 |
+
p_cont_vec[cptr, 9] = 1
|
| 705 |
+
cptr += 1
|
| 706 |
+
elif op == O_BOOST:
|
| 707 |
+
# out_bonus[0] += v # WRONG: This was adding to Permanent Score
|
| 708 |
+
global_ctx[LS] += v # CORRECT: Add to Temporary Live Score Context
|
| 709 |
+
elif op == O_LOOK_AND_CHOOSE:
|
| 710 |
+
choice_idx = int(flat_ctx[CH])
|
| 711 |
+
if choice_idx < 0 or choice_idx >= v:
|
| 712 |
+
choice_idx = 0
|
| 713 |
+
indices = np.full(v, -1, dtype=np.int32)
|
| 714 |
+
ptr = 0
|
| 715 |
+
for d_idx in range(60):
|
| 716 |
+
if p_deck[d_idx] > 0:
|
| 717 |
+
indices[ptr] = d_idx
|
| 718 |
+
ptr += 1
|
| 719 |
+
if ptr >= v:
|
| 720 |
+
break
|
| 721 |
+
if ptr > 0:
|
| 722 |
+
if choice_idx >= ptr:
|
| 723 |
+
choice_idx = 0
|
| 724 |
+
real_idx = indices[choice_idx]
|
| 725 |
+
chosen_card = -1
|
| 726 |
+
if real_idx != -1:
|
| 727 |
+
chosen_card = p_deck[real_idx]
|
| 728 |
+
if chosen_card > 0:
|
| 729 |
+
for h_idx in range(60):
|
| 730 |
+
if p_hand[h_idx] == 0:
|
| 731 |
+
p_hand[h_idx] = chosen_card
|
| 732 |
+
global_ctx[HD] += 1
|
| 733 |
+
break
|
| 734 |
+
for k in range(ptr):
|
| 735 |
+
rid = indices[k]
|
| 736 |
+
if rid != -1:
|
| 737 |
+
cid = p_deck[rid]
|
| 738 |
+
p_deck[rid] = 0
|
| 739 |
+
global_ctx[DK] -= 1
|
| 740 |
+
if rid != real_idx: # Fix: Don't mill the card added to hand
|
| 741 |
+
move_to_trash(0, cid, p_trash.reshape(1, -1), global_ctx.reshape(1, -1), 2)
|
| 742 |
+
|
| 743 |
+
elif op == O_ORDER_DECK:
|
| 744 |
+
indices = np.full(v, -1, dtype=np.int32)
|
| 745 |
+
vals = np.full(v, 0, dtype=np.int32)
|
| 746 |
+
ptr = 0
|
| 747 |
+
for d_idx in range(60):
|
| 748 |
+
if p_deck[d_idx] > 0:
|
| 749 |
+
indices[ptr] = d_idx
|
| 750 |
+
vals[ptr] = p_deck[d_idx]
|
| 751 |
+
ptr += 1
|
| 752 |
+
if ptr >= v:
|
| 753 |
+
break
|
| 754 |
+
if ptr > 1:
|
| 755 |
+
for k in range(ptr // 2):
|
| 756 |
+
temp = vals[k]
|
| 757 |
+
vals[k] = vals[ptr - 1 - k]
|
| 758 |
+
vals[ptr - 1 - k] = temp
|
| 759 |
+
for k in range(ptr):
|
| 760 |
+
p_deck[indices[k]] = vals[k]
|
| 761 |
+
|
| 762 |
+
elif op == O_ADD_H:
|
| 763 |
+
drawn = 0
|
| 764 |
+
for d_idx in range(60):
|
| 765 |
+
if p_deck[d_idx] > 0:
|
| 766 |
+
card_id = p_deck[d_idx]
|
| 767 |
+
p_deck[d_idx] = 0
|
| 768 |
+
global_ctx[DK] -= 1
|
| 769 |
+
for h_idx in range(60):
|
| 770 |
+
if p_hand[h_idx] == 0:
|
| 771 |
+
p_hand[h_idx] = card_id
|
| 772 |
+
global_ctx[HD] += 1
|
| 773 |
+
break
|
| 774 |
+
drawn += 1
|
| 775 |
+
if drawn >= v:
|
| 776 |
+
break
|
| 777 |
+
elif op == O_SEARCH_DECK:
|
| 778 |
+
target_idx = int(flat_ctx[TS])
|
| 779 |
+
if 0 <= target_idx < 60 and p_deck[target_idx] > 0:
|
| 780 |
+
card_to_move = p_deck[target_idx]
|
| 781 |
+
p_deck[target_idx] = 0
|
| 782 |
+
for h_idx in range(60):
|
| 783 |
+
if p_hand[h_idx] == 0:
|
| 784 |
+
p_hand[h_idx] = card_to_move
|
| 785 |
+
global_ctx[HD] += 1
|
| 786 |
+
global_ctx[DK] -= 1
|
| 787 |
+
break
|
| 788 |
+
else:
|
| 789 |
+
for d_idx in range(60):
|
| 790 |
+
if p_deck[d_idx] > 0:
|
| 791 |
+
card_to_move = p_deck[d_idx]
|
| 792 |
+
p_deck[d_idx] = 0
|
| 793 |
+
for h_idx in range(60):
|
| 794 |
+
if p_hand[h_idx] == 0:
|
| 795 |
+
p_hand[h_idx] = card_to_move
|
| 796 |
+
global_ctx[HD] += 1
|
| 797 |
+
global_ctx[DK] -= 1
|
| 798 |
+
break
|
| 799 |
+
break
|
| 800 |
+
|
| 801 |
+
elif op == O_MOVE_TO_DISCARD:
|
| 802 |
+
# v=count, a=source(1=deck,2=hand,3=energy), s=target(0=self)
|
| 803 |
+
if a == 1: # Deck
|
| 804 |
+
moved = 0
|
| 805 |
+
for d_idx in range(60):
|
| 806 |
+
if p_deck[d_idx] > 0:
|
| 807 |
+
cid = p_deck[d_idx]
|
| 808 |
+
p_deck[d_idx] = 0
|
| 809 |
+
global_ctx[DK] -= 1
|
| 810 |
+
# Add to trash inline
|
| 811 |
+
for tr_k in range(60):
|
| 812 |
+
if p_trash[tr_k] == 0:
|
| 813 |
+
p_trash[tr_k] = cid
|
| 814 |
+
global_ctx[DI] += 1
|
| 815 |
+
break
|
| 816 |
+
moved += 1
|
| 817 |
+
if moved >= v:
|
| 818 |
+
break
|
| 819 |
+
elif a == 2: # Hand
|
| 820 |
+
moved = 0
|
| 821 |
+
for h_idx in range(59, -1, -1):
|
| 822 |
+
if p_hand[h_idx] > 0:
|
| 823 |
+
cid = p_hand[h_idx]
|
| 824 |
+
p_hand[h_idx] = 0
|
| 825 |
+
global_ctx[HD] -= 1
|
| 826 |
+
for tr_k in range(60):
|
| 827 |
+
if p_trash[tr_k] == 0:
|
| 828 |
+
p_trash[tr_k] = cid
|
| 829 |
+
global_ctx[DI] += 1
|
| 830 |
+
break
|
| 831 |
+
moved += 1
|
| 832 |
+
if moved >= v:
|
| 833 |
+
break
|
| 834 |
+
# a=3 Energy not fully supported in fast_logic yet (requires attached energy iter)
|
| 835 |
+
elif s == 0: # Self (Stage)
|
| 836 |
+
scid = int(flat_ctx[SID])
|
| 837 |
+
for s_k in range(3):
|
| 838 |
+
if p_stage[s_k] == scid:
|
| 839 |
+
p_stage[s_k] = -1
|
| 840 |
+
p_tapped[s_k] = 0
|
| 841 |
+
for tr_k in range(60):
|
| 842 |
+
if p_trash[tr_k] == 0:
|
| 843 |
+
p_trash[tr_k] = scid
|
| 844 |
+
global_ctx[DI] += 1
|
| 845 |
+
break
|
| 846 |
+
break
|
| 847 |
+
|
| 848 |
+
elif op == O_TRIGGER_REMOTE:
|
| 849 |
+
target_slot = s
|
| 850 |
+
target_card_id = -1
|
| 851 |
+
if 0 <= target_slot < 3:
|
| 852 |
+
target_card_id = p_stage[target_slot]
|
| 853 |
+
|
| 854 |
+
if target_card_id >= 0:
|
| 855 |
+
target_bid = get_base_id(target_card_id)
|
| 856 |
+
if target_bid < b_idx.shape[0]:
|
| 857 |
+
map_idx = b_idx[target_bid, 0]
|
| 858 |
+
if map_idx >= 0 and stack_ptr < 3:
|
| 859 |
+
stack_ptr += 1
|
| 860 |
+
if stack_ptr == 1:
|
| 861 |
+
bc1 = b_map[map_idx]
|
| 862 |
+
ip1 = 0
|
| 863 |
+
elif stack_ptr == 2:
|
| 864 |
+
bc2 = b_map[map_idx]
|
| 865 |
+
ip2 = 0
|
| 866 |
+
else:
|
| 867 |
+
bc3 = b_map[map_idx]
|
| 868 |
+
ip3 = 0
|
| 869 |
+
continue
|
| 870 |
+
|
| 871 |
+
out_cptr[0] = cptr
|
| 872 |
+
|
| 873 |
+
|
| 874 |
+
@njit(nopython=True, parallel=True, cache=True, fastmath=True)
|
| 875 |
+
def batch_resolve_bytecode(
|
| 876 |
+
batch_bytecode,
|
| 877 |
+
batch_flat_ctx,
|
| 878 |
+
batch_global_ctx,
|
| 879 |
+
player_id,
|
| 880 |
+
p_hand,
|
| 881 |
+
p_deck,
|
| 882 |
+
p_stage,
|
| 883 |
+
p_energy_vec,
|
| 884 |
+
p_energy_count,
|
| 885 |
+
p_cont_vec,
|
| 886 |
+
p_cont_ptr,
|
| 887 |
+
p_tapped,
|
| 888 |
+
p_live,
|
| 889 |
+
opp_tapped,
|
| 890 |
+
p_trash,
|
| 891 |
+
b_map,
|
| 892 |
+
b_idx,
|
| 893 |
+
card_stats, # Added: NumPy array for card properties
|
| 894 |
+
opp_stage, # Added
|
| 895 |
+
):
|
| 896 |
+
num_envs = batch_bytecode.shape[0]
|
| 897 |
+
# Pre-allocate bonus array to avoid allocations in prange
|
| 898 |
+
batch_bonus = np.zeros(num_envs, dtype=np.int32)
|
| 899 |
+
for i in prange(num_envs):
|
| 900 |
+
cptr_slice = p_cont_ptr[i : i + 1]
|
| 901 |
+
out_bonus_slice = batch_bonus[i : i + 1]
|
| 902 |
+
|
| 903 |
+
resolve_bytecode(
|
| 904 |
+
batch_bytecode[i],
|
| 905 |
+
batch_flat_ctx[i],
|
| 906 |
+
batch_global_ctx[i],
|
| 907 |
+
player_id,
|
| 908 |
+
p_hand[i],
|
| 909 |
+
p_deck[i],
|
| 910 |
+
p_stage[i],
|
| 911 |
+
p_energy_vec[i],
|
| 912 |
+
p_energy_count[i],
|
| 913 |
+
p_cont_vec[i],
|
| 914 |
+
cptr_slice,
|
| 915 |
+
p_tapped[i],
|
| 916 |
+
p_live[i],
|
| 917 |
+
opp_tapped[i],
|
| 918 |
+
p_trash[i],
|
| 919 |
+
b_map,
|
| 920 |
+
b_idx,
|
| 921 |
+
out_bonus_slice,
|
| 922 |
+
card_stats,
|
| 923 |
+
opp_stage[i], # Pass opponent stage
|
| 924 |
+
)
|
| 925 |
+
|
| 926 |
+
|
| 927 |
+
@njit(nopython=True, cache=True, inline="always", fastmath=True)
|
| 928 |
+
def copy_state(s_stg, s_ev, s_ec, s_cv, d_stg, d_ev, d_ec, d_cv):
|
| 929 |
+
d_stg[:] = s_stg[:]
|
| 930 |
+
d_ev[:] = s_ev[:]
|
| 931 |
+
d_ec[:] = s_ec[:]
|
| 932 |
+
d_cv[:] = s_cv[:]
|
| 933 |
+
|
| 934 |
+
|
| 935 |
+
@njit(nopython=True, cache=True, inline="always", fastmath=True)
|
| 936 |
+
def move_to_trash(i, card_id, batch_trash, batch_global_ctx, TR_idx):
|
| 937 |
+
if card_id <= 0:
|
| 938 |
+
return
|
| 939 |
+
for k in range(batch_trash.shape[1]):
|
| 940 |
+
if batch_trash[i, k] == 0:
|
| 941 |
+
batch_trash[i, k] = card_id
|
| 942 |
+
batch_global_ctx[i, TR_idx] += 1
|
| 943 |
+
break
|
| 944 |
+
|
| 945 |
+
|
| 946 |
+
@njit(nopython=True, cache=True, fastmath=True)
|
| 947 |
+
def resolve_live_single(
|
| 948 |
+
i,
|
| 949 |
+
live_id,
|
| 950 |
+
batch_stage,
|
| 951 |
+
batch_live,
|
| 952 |
+
batch_scores,
|
| 953 |
+
batch_global_ctx,
|
| 954 |
+
batch_deck,
|
| 955 |
+
batch_hand,
|
| 956 |
+
batch_trash,
|
| 957 |
+
card_stats,
|
| 958 |
+
p_cont_vec,
|
| 959 |
+
p_cont_ptr,
|
| 960 |
+
b_map,
|
| 961 |
+
b_idx,
|
| 962 |
+
p_tapped,
|
| 963 |
+
):
|
| 964 |
+
# Rule 8.3.4: Non-Live cards are discarded before performance
|
| 965 |
+
live_bid = get_base_id(live_id)
|
| 966 |
+
if live_bid >= card_stats.shape[0] or card_stats[live_bid, 10] != 2:
|
| 967 |
+
# Find and remove from live zone
|
| 968 |
+
for j in range(batch_live.shape[1]):
|
| 969 |
+
if batch_live[i, j] == live_id:
|
| 970 |
+
move_to_trash(i, live_id, batch_trash, batch_global_ctx, 2)
|
| 971 |
+
batch_live[i, j] = 0
|
| 972 |
+
break
|
| 973 |
+
return 0
|
| 974 |
+
|
| 975 |
+
# 1. Verify availability in Live Zone
|
| 976 |
+
live_idx = -1
|
| 977 |
+
for j in range(batch_live.shape[1]):
|
| 978 |
+
if batch_live[i, j] == live_id:
|
| 979 |
+
live_idx = j
|
| 980 |
+
break
|
| 981 |
+
|
| 982 |
+
if live_idx == -1:
|
| 983 |
+
return 0
|
| 984 |
+
|
| 985 |
+
batch_global_ctx[i, LS] = 0 # Ensure reset score bonus
|
| 986 |
+
batch_global_ctx[i, EH] = 0 # Reset excess hearts
|
| 987 |
+
|
| 988 |
+
# 1. Trigger ON_LIVE_START (TriggerType = 2)
|
| 989 |
+
for ab_idx in range(4):
|
| 990 |
+
trigger_off = 20 if ab_idx == 0 else (20 + 16 + (ab_idx - 1) * 12)
|
| 991 |
+
if card_stats[live_bid, trigger_off] == 2: # ON_LIVE_START
|
| 992 |
+
map_idx = b_idx[live_bid, ab_idx]
|
| 993 |
+
if map_idx >= 0:
|
| 994 |
+
# Execution context
|
| 995 |
+
flat_ctx = np.zeros(64, dtype=np.int32)
|
| 996 |
+
dummy_opp_tapped = np.zeros(3, dtype=np.int32)
|
| 997 |
+
out_bonus = np.zeros(1, dtype=np.int32)
|
| 998 |
+
p_tapped_dummy = np.zeros(16, dtype=np.int32)
|
| 999 |
+
|
| 1000 |
+
resolve_bytecode(
|
| 1001 |
+
b_map[map_idx],
|
| 1002 |
+
flat_ctx,
|
| 1003 |
+
batch_global_ctx[i],
|
| 1004 |
+
0,
|
| 1005 |
+
batch_hand[i],
|
| 1006 |
+
batch_deck[i],
|
| 1007 |
+
batch_stage[i],
|
| 1008 |
+
np.zeros((3, 32), dtype=np.int32),
|
| 1009 |
+
np.zeros(3, dtype=np.int32),
|
| 1010 |
+
p_cont_vec[i],
|
| 1011 |
+
p_cont_ptr[i : i + 1],
|
| 1012 |
+
p_tapped,
|
| 1013 |
+
batch_live[i],
|
| 1014 |
+
dummy_opp_tapped,
|
| 1015 |
+
batch_trash[i],
|
| 1016 |
+
b_map,
|
| 1017 |
+
b_idx,
|
| 1018 |
+
out_bonus,
|
| 1019 |
+
card_stats,
|
| 1020 |
+
batch_stage[i],
|
| 1021 |
+
)
|
| 1022 |
+
|
| 1023 |
+
# 2. Check Requirements
|
| 1024 |
+
# Get Live Stats (indices 12-18 for requirements)
|
| 1025 |
+
req_pink = card_stats[live_bid, 12]
|
| 1026 |
+
req_red = card_stats[live_bid, 13]
|
| 1027 |
+
req_yel = card_stats[live_bid, 14]
|
| 1028 |
+
req_grn = card_stats[live_bid, 15]
|
| 1029 |
+
req_blu = card_stats[live_bid, 16]
|
| 1030 |
+
req_pur = card_stats[live_bid, 17]
|
| 1031 |
+
|
| 1032 |
+
# Sum Stage Stats
|
| 1033 |
+
stage_hearts = np.zeros(7, dtype=np.int32)
|
| 1034 |
+
stage_all = 0
|
| 1035 |
+
total_blades = 0
|
| 1036 |
+
|
| 1037 |
+
for slot in range(3):
|
| 1038 |
+
# Rule 9.9: Resting members (tapped) do not contribute to performance.
|
| 1039 |
+
cid = batch_stage[i, slot]
|
| 1040 |
+
if cid >= 0 and p_tapped[slot] == 0:
|
| 1041 |
+
bid = get_base_id(cid)
|
| 1042 |
+
if bid < card_stats.shape[0]:
|
| 1043 |
+
slot_blades = card_stats[bid, 1]
|
| 1044 |
+
slot_hearts = np.zeros(7, dtype=np.int32)
|
| 1045 |
+
for cidx in range(6):
|
| 1046 |
+
slot_hearts[cidx] = card_stats[bid, 12 + cidx]
|
| 1047 |
+
slot_all = card_stats[bid, 18]
|
| 1048 |
+
|
| 1049 |
+
# Apply continuous effects
|
| 1050 |
+
for ce_idx in range(p_cont_ptr[i]):
|
| 1051 |
+
op = p_cont_vec[i, ce_idx, 0]
|
| 1052 |
+
val = p_cont_vec[i, ce_idx, 1]
|
| 1053 |
+
target_slot = p_cont_vec[i, ce_idx, 2]
|
| 1054 |
+
active = p_cont_vec[i, ce_idx, 9]
|
| 1055 |
+
|
| 1056 |
+
if active == 1 and (target_slot == -1 or target_slot == slot):
|
| 1057 |
+
if op == 1 or op == 8: # BLADES / BUFF
|
| 1058 |
+
slot_blades += val
|
| 1059 |
+
elif op == 24: # SET_BLADES
|
| 1060 |
+
slot_blades = val
|
| 1061 |
+
elif op == 2: # HEARTS
|
| 1062 |
+
a_color = p_cont_vec[i, ce_idx, 5]
|
| 1063 |
+
if 0 <= a_color <= 5:
|
| 1064 |
+
slot_hearts[a_color] += val
|
| 1065 |
+
elif a_color == 6: # Any/Rainbow
|
| 1066 |
+
slot_all += val
|
| 1067 |
+
|
| 1068 |
+
total_blades += max(0, slot_blades)
|
| 1069 |
+
for cidx in range(6):
|
| 1070 |
+
stage_hearts[cidx] += slot_hearts[cidx]
|
| 1071 |
+
stage_all += slot_all
|
| 1072 |
+
|
| 1073 |
+
# Apply Heart Requirement Reductions
|
| 1074 |
+
for ce_idx in range(p_cont_ptr[i]):
|
| 1075 |
+
op = p_cont_vec[i, ce_idx, 0]
|
| 1076 |
+
val = p_cont_vec[i, ce_idx, 1]
|
| 1077 |
+
active = p_cont_vec[i, ce_idx, 9]
|
| 1078 |
+
if active == 1 and op == 48: # REDUCE_HEART_REQ
|
| 1079 |
+
target_color = p_cont_vec[i, ce_idx, 2] # 0-5 or 6 (Any)
|
| 1080 |
+
if target_color == 0:
|
| 1081 |
+
req_pink = max(0, req_pink - val)
|
| 1082 |
+
elif target_color == 1:
|
| 1083 |
+
req_red = max(0, req_red - val)
|
| 1084 |
+
elif target_color == 2:
|
| 1085 |
+
req_yel = max(0, req_yel - val)
|
| 1086 |
+
elif target_color == 3:
|
| 1087 |
+
req_grn = max(0, req_grn - val)
|
| 1088 |
+
elif target_color == 4:
|
| 1089 |
+
req_blu = max(0, req_blu - val)
|
| 1090 |
+
elif target_color == 5:
|
| 1091 |
+
req_pur = max(0, req_pur - val)
|
| 1092 |
+
elif target_color == 6: # Reduction for 'Any' requirement
|
| 1093 |
+
# In this simplified model, card_stats[live_bid, 18] might be 'Any'
|
| 1094 |
+
# but it's not being checked yet. Let's assume for now
|
| 1095 |
+
# that we don't have a specific index for 'Any' req in card_stats.
|
| 1096 |
+
# Actually, let's look for where 'any' might be.
|
| 1097 |
+
pass
|
| 1098 |
+
|
| 1099 |
+
# --- RULE ACCURACY: Yells (Rule 8.3) ---
|
| 1100 |
+
# Draw 'total_blades' cards from deck, apply their blade_hearts and volume/draw icons.
|
| 1101 |
+
volume_bonus = 0
|
| 1102 |
+
draw_bonus = 0
|
| 1103 |
+
yells_processed = 0
|
| 1104 |
+
|
| 1105 |
+
# Use a while loop to handle potential deck refreshes
|
| 1106 |
+
while yells_processed < total_blades:
|
| 1107 |
+
# Check if we need to refresh (deck empty/exhausted)
|
| 1108 |
+
# Scan for next valid card
|
| 1109 |
+
found_card = False
|
| 1110 |
+
deck_len = batch_deck.shape[1]
|
| 1111 |
+
d_idx = -1
|
| 1112 |
+
|
| 1113 |
+
for k in range(deck_len):
|
| 1114 |
+
if batch_deck[i, k] > 0:
|
| 1115 |
+
d_idx = k
|
| 1116 |
+
found_card = True
|
| 1117 |
+
break
|
| 1118 |
+
|
| 1119 |
+
if not found_card:
|
| 1120 |
+
# Deck is empty, trigger refresh logic
|
| 1121 |
+
check_deck_refresh(i, batch_deck, batch_trash, batch_global_ctx, 6, 2)
|
| 1122 |
+
# Try to find again
|
| 1123 |
+
for k in range(deck_len):
|
| 1124 |
+
if batch_deck[i, k] > 0:
|
| 1125 |
+
d_idx = k
|
| 1126 |
+
found_card = True
|
| 1127 |
+
break
|
| 1128 |
+
|
| 1129 |
+
# If still no card (Deck + Trash were empty), we stop yelling.
|
| 1130 |
+
if not found_card:
|
| 1131 |
+
break
|
| 1132 |
+
|
| 1133 |
+
# Process the found card
|
| 1134 |
+
if d_idx >= 0:
|
| 1135 |
+
yid = batch_deck[i, d_idx]
|
| 1136 |
+
if yid > 0 and yid < card_stats.shape[0]:
|
| 1137 |
+
# Extract blade_hearts [40:47]
|
| 1138 |
+
bh = np.zeros(7, dtype=np.int32)
|
| 1139 |
+
for cidx in range(7):
|
| 1140 |
+
bh[cidx] = card_stats[yid, 40 + cidx]
|
| 1141 |
+
|
| 1142 |
+
# Apply TRANSFORM_COLOR (Rule 11.12)
|
| 1143 |
+
for ce_idx in range(p_cont_ptr[i]):
|
| 1144 |
+
if p_cont_vec[i, ce_idx, 0] == O_TRANSFORM_COLOR:
|
| 1145 |
+
target_idx = p_cont_vec[i, ce_idx, 2]
|
| 1146 |
+
if 0 <= target_idx <= 5:
|
| 1147 |
+
total_affected = 0
|
| 1148 |
+
# Transform Pink, Red, Yellow, Green, Blue, (and All if applicable)
|
| 1149 |
+
# Rule for Dazzling Game: 0,1,2,3,4,6 -> target_idx
|
| 1150 |
+
for src_idx in [0, 1, 2, 3, 4, 6]:
|
| 1151 |
+
total_affected += bh[src_idx]
|
| 1152 |
+
bh[src_idx] = 0
|
| 1153 |
+
bh[target_idx] += total_affected
|
| 1154 |
+
|
| 1155 |
+
# Add to totals
|
| 1156 |
+
for cidx in range(7):
|
| 1157 |
+
stage_hearts[cidx] += bh[cidx]
|
| 1158 |
+
|
| 1159 |
+
# Bonus Icons
|
| 1160 |
+
volume_bonus += card_stats[yid, 4]
|
| 1161 |
+
draw_bonus += card_stats[yid, 5]
|
| 1162 |
+
|
| 1163 |
+
# Discard (Remove from deck)
|
| 1164 |
+
batch_deck[i, d_idx] = 0
|
| 1165 |
+
if batch_global_ctx[i, 6] > 0:
|
| 1166 |
+
batch_global_ctx[i, 6] -= 1 # DK
|
| 1167 |
+
move_to_trash(i, yid, batch_trash, batch_global_ctx, 2)
|
| 1168 |
+
yells_processed += 1
|
| 1169 |
+
else:
|
| 1170 |
+
# Invalid card ID or padding
|
| 1171 |
+
batch_deck[i, d_idx] = 0
|
| 1172 |
+
yells_processed += 1
|
| 1173 |
+
else:
|
| 1174 |
+
break
|
| 1175 |
+
|
| 1176 |
+
# Apply Draw Bonus
|
| 1177 |
+
cards_drawn = 0
|
| 1178 |
+
while cards_drawn < draw_bonus:
|
| 1179 |
+
found_card = False
|
| 1180 |
+
d_idx = -1
|
| 1181 |
+
deck_len = batch_deck.shape[1]
|
| 1182 |
+
|
| 1183 |
+
for k in range(deck_len):
|
| 1184 |
+
if batch_deck[i, k] > 0:
|
| 1185 |
+
d_idx = k
|
| 1186 |
+
found_card = True
|
| 1187 |
+
break
|
| 1188 |
+
|
| 1189 |
+
if not found_card:
|
| 1190 |
+
check_deck_refresh(i, batch_deck, batch_trash, batch_global_ctx, 6, 2)
|
| 1191 |
+
for k in range(deck_len):
|
| 1192 |
+
if batch_deck[i, k] > 0:
|
| 1193 |
+
d_idx = k
|
| 1194 |
+
found_card = True
|
| 1195 |
+
break
|
| 1196 |
+
if not found_card:
|
| 1197 |
+
break
|
| 1198 |
+
|
| 1199 |
+
if found_card and d_idx != -1:
|
| 1200 |
+
# Draw the card
|
| 1201 |
+
top_c = batch_deck[i, d_idx]
|
| 1202 |
+
# Move to Hand
|
| 1203 |
+
placed = False
|
| 1204 |
+
for h_ptr in range(batch_hand.shape[1]):
|
| 1205 |
+
if batch_hand[i, h_ptr] == 0:
|
| 1206 |
+
batch_hand[i, h_ptr] = top_c
|
| 1207 |
+
placed = True
|
| 1208 |
+
break
|
| 1209 |
+
|
| 1210 |
+
# Remove from Deck regardless (it left the deck)
|
| 1211 |
+
batch_deck[i, d_idx] = 0
|
| 1212 |
+
if batch_global_ctx[i, 6] > 0:
|
| 1213 |
+
batch_global_ctx[i, 6] -= 1 # DK
|
| 1214 |
+
|
| 1215 |
+
if placed:
|
| 1216 |
+
batch_global_ctx[i, 3] += 1 # HD
|
| 1217 |
+
else:
|
| 1218 |
+
# Hand full, discard to trash
|
| 1219 |
+
move_to_trash(i, top_c, batch_trash, batch_global_ctx, 2)
|
| 1220 |
+
else:
|
| 1221 |
+
break
|
| 1222 |
+
|
| 1223 |
+
cards_drawn += 1
|
| 1224 |
+
|
| 1225 |
+
# Apply Heart Requirement Reductions
|
| 1226 |
+
req_any = card_stats[live_bid, 18]
|
| 1227 |
+
for ce_idx in range(p_cont_ptr[i]):
|
| 1228 |
+
op = p_cont_vec[i, ce_idx, 0]
|
| 1229 |
+
val = p_cont_vec[i, ce_idx, 1]
|
| 1230 |
+
active = p_cont_vec[i, ce_idx, 9]
|
| 1231 |
+
if active == 1 and op == 48: # REDUCE_HEART_REQ
|
| 1232 |
+
target_color = p_cont_vec[i, ce_idx, 2] # 0-5 or 6 (Any)
|
| 1233 |
+
if target_color == 0:
|
| 1234 |
+
req_pink = max(0, req_pink - val)
|
| 1235 |
+
elif target_color == 1:
|
| 1236 |
+
req_red = max(0, req_red - val)
|
| 1237 |
+
elif target_color == 2:
|
| 1238 |
+
req_yel = max(0, req_yel - val)
|
| 1239 |
+
elif target_color == 3:
|
| 1240 |
+
req_grn = max(0, req_grn - val)
|
| 1241 |
+
elif target_color == 4:
|
| 1242 |
+
req_blu = max(0, req_blu - val)
|
| 1243 |
+
elif target_color == 5:
|
| 1244 |
+
req_pur = max(0, req_pur - val)
|
| 1245 |
+
elif target_color == 6:
|
| 1246 |
+
req_any = max(0, req_any - val)
|
| 1247 |
+
|
| 1248 |
+
# Verify Requirements (Greedy points matching)
|
| 1249 |
+
met = True
|
| 1250 |
+
temp_all = stage_all
|
| 1251 |
+
req_list = [req_pink, req_red, req_yel, req_grn, req_blu, req_pur]
|
| 1252 |
+
|
| 1253 |
+
for cidx in range(6):
|
| 1254 |
+
needed = req_list[cidx]
|
| 1255 |
+
have = stage_hearts[cidx]
|
| 1256 |
+
if have < needed:
|
| 1257 |
+
deficit = needed - have
|
| 1258 |
+
if temp_all >= deficit:
|
| 1259 |
+
temp_all -= deficit
|
| 1260 |
+
else:
|
| 1261 |
+
met = False
|
| 1262 |
+
break
|
| 1263 |
+
|
| 1264 |
+
if met:
|
| 1265 |
+
remaining = temp_all
|
| 1266 |
+
for cidx in range(6):
|
| 1267 |
+
remaining += max(0, stage_hearts[cidx] - req_list[cidx])
|
| 1268 |
+
if remaining < req_any:
|
| 1269 |
+
met = False
|
| 1270 |
+
|
| 1271 |
+
# 3. Apply Result (Defer to Battle Phase)
|
| 1272 |
+
if met and total_blades > 0:
|
| 1273 |
+
# SUCCESS PENDING
|
| 1274 |
+
|
| 1275 |
+
# --- RULE ACCURACY: Excess Hearts (Rule 8.3) ---
|
| 1276 |
+
total_p_hearts = (
|
| 1277 |
+
stage_hearts[0]
|
| 1278 |
+
+ stage_hearts[1]
|
| 1279 |
+
+ stage_hearts[2]
|
| 1280 |
+
+ stage_hearts[3]
|
| 1281 |
+
+ stage_hearts[4]
|
| 1282 |
+
+ stage_hearts[5]
|
| 1283 |
+
+ stage_all
|
| 1284 |
+
)
|
| 1285 |
+
req_total = req_pink + req_red + req_yel + req_grn + req_blu + req_pur
|
| 1286 |
+
batch_global_ctx[i, EH] = max(0, total_p_hearts - req_total)
|
| 1287 |
+
|
| 1288 |
+
# 4. Trigger ON_LIVE_SUCCESS (TriggerType = 3)
|
| 1289 |
+
for ab_idx in range(4):
|
| 1290 |
+
trigger_off = 20 if ab_idx == 0 else (20 + 16 + (ab_idx - 1) * 12)
|
| 1291 |
+
if card_stats[live_id, trigger_off] == 3: # ON_LIVE_SUCCESS
|
| 1292 |
+
map_idx = b_idx[live_id, ab_idx]
|
| 1293 |
+
if map_idx >= 0:
|
| 1294 |
+
# Execution context
|
| 1295 |
+
flat_ctx = np.zeros(64, dtype=np.int32)
|
| 1296 |
+
dummy_opp_tapped = np.zeros(3, dtype=np.int32)
|
| 1297 |
+
out_bonus_ab = np.zeros(1, dtype=np.int32)
|
| 1298 |
+
p_tapped_dummy = np.zeros(16, dtype=np.int32)
|
| 1299 |
+
|
| 1300 |
+
resolve_bytecode(
|
| 1301 |
+
b_map[map_idx],
|
| 1302 |
+
flat_ctx,
|
| 1303 |
+
batch_global_ctx[i],
|
| 1304 |
+
0,
|
| 1305 |
+
batch_hand[i],
|
| 1306 |
+
batch_deck[i],
|
| 1307 |
+
batch_stage[i],
|
| 1308 |
+
np.zeros((3, 32), dtype=np.int32),
|
| 1309 |
+
np.zeros(3, dtype=np.int32),
|
| 1310 |
+
p_cont_vec[i],
|
| 1311 |
+
p_cont_ptr[i : i + 1],
|
| 1312 |
+
p_tapped_dummy,
|
| 1313 |
+
batch_live[i],
|
| 1314 |
+
dummy_opp_tapped,
|
| 1315 |
+
batch_trash[i],
|
| 1316 |
+
b_map,
|
| 1317 |
+
b_idx,
|
| 1318 |
+
out_bonus_ab,
|
| 1319 |
+
card_stats,
|
| 1320 |
+
opp_stage[i], # Opponent stage
|
| 1321 |
+
)
|
| 1322 |
+
|
| 1323 |
+
base_score = card_stats[live_id, 38]
|
| 1324 |
+
if base_score <= 0:
|
| 1325 |
+
base_score = 1 # Safety
|
| 1326 |
+
|
| 1327 |
+
total_score = base_score + volume_bonus + batch_global_ctx[i, LS]
|
| 1328 |
+
batch_global_ctx[i, LS] = 0 # Reset after applying
|
| 1329 |
+
batch_global_ctx[i, EH] = 0 # Reset after triggering success
|
| 1330 |
+
|
| 1331 |
+
# Clear Stage MEMBERS (Rule 8.3.17) - Members are cleared after performance regardless of battle result?
|
| 1332 |
+
# Rule 8.4.8 says "Clear ... remaining cards". Rule 8.3.17 says "Clear stage".
|
| 1333 |
+
# Assume stage clear happens here.
|
| 1334 |
+
for slot in range(3):
|
| 1335 |
+
cid = batch_stage[i, slot]
|
| 1336 |
+
if cid > 0:
|
| 1337 |
+
move_to_trash(i, cid, batch_trash, batch_global_ctx, 2)
|
| 1338 |
+
batch_stage[i, slot] = -1
|
| 1339 |
+
|
| 1340 |
+
# We DO NOT remove the live card yet. It waits for battle.
|
| 1341 |
+
return total_score
|
| 1342 |
+
else:
|
| 1343 |
+
# FAILURE - Cards in zone are discarded after performance (Rule 8.3.16)
|
| 1344 |
+
if live_idx >= 0:
|
| 1345 |
+
move_to_trash(i, live_id, batch_trash, batch_global_ctx, 2)
|
| 1346 |
+
batch_live[i, live_idx] = 0
|
| 1347 |
+
|
| 1348 |
+
# Also clear stage on failure
|
| 1349 |
+
for slot in range(3):
|
| 1350 |
+
cid = batch_stage[i, slot]
|
| 1351 |
+
if cid > 0:
|
| 1352 |
+
move_to_trash(i, cid, batch_trash, batch_global_ctx, 2)
|
| 1353 |
+
batch_stage[i, slot] = -1
|
| 1354 |
+
|
| 1355 |
+
return 0
|
| 1356 |
+
|
| 1357 |
+
|
| 1358 |
+
@njit(nopython=True)
|
| 1359 |
+
def p_deck_len_helper(batch_deck, i):
|
| 1360 |
+
return batch_deck.shape[1]
|
| 1361 |
+
|
| 1362 |
+
|
| 1363 |
+
@njit(nopython=True)
|
| 1364 |
+
def p_hand_len_helper(batch_hand, i):
|
| 1365 |
+
return batch_hand.shape[1]
|
| 1366 |
+
|
| 1367 |
+
|
| 1368 |
+
@njit(nopython=True, cache=True)
|
| 1369 |
+
def check_deck_refresh(i, p_deck, p_trash, g_ctx, DK_idx, TR_idx):
|
| 1370 |
+
# Rule 10.2: If deck empty and trash has cards, refresh.
|
| 1371 |
+
if g_ctx[i, DK_idx] <= 0 and g_ctx[i, TR_idx] > 0:
|
| 1372 |
+
# Move trash to deck
|
| 1373 |
+
d_ptr = 0
|
| 1374 |
+
for k in range(p_trash.shape[1]):
|
| 1375 |
+
cid = p_trash[i, k]
|
| 1376 |
+
if cid > 0:
|
| 1377 |
+
p_deck[i, d_ptr] = cid
|
| 1378 |
+
p_trash[i, k] = 0
|
| 1379 |
+
d_ptr += 1
|
| 1380 |
+
|
| 1381 |
+
g_ctx[i, DK_idx] = d_ptr
|
| 1382 |
+
g_ctx[i, TR_idx] = 0
|
| 1383 |
+
# Shuffle Deck
|
| 1384 |
+
if d_ptr > 1:
|
| 1385 |
+
for k in range(d_ptr - 1, 0, -1):
|
| 1386 |
+
j = np.random.randint(0, k + 1)
|
| 1387 |
+
tmp = p_deck[i, k]
|
| 1388 |
+
p_deck[i, k] = p_deck[i, j]
|
| 1389 |
+
p_deck[i, j] = tmp
|
| 1390 |
+
|
| 1391 |
+
|
| 1392 |
+
@njit(nopython=True, cache=True, fastmath=True)
|
| 1393 |
+
def resolve_live_performance(
|
| 1394 |
+
num_envs: int,
|
| 1395 |
+
action_ids: np.ndarray,
|
| 1396 |
+
batch_stage: np.ndarray,
|
| 1397 |
+
batch_live: np.ndarray,
|
| 1398 |
+
batch_scores: np.ndarray,
|
| 1399 |
+
batch_global_ctx: np.ndarray,
|
| 1400 |
+
batch_deck: np.ndarray,
|
| 1401 |
+
batch_hand: np.ndarray,
|
| 1402 |
+
batch_trash: np.ndarray,
|
| 1403 |
+
card_stats: np.ndarray,
|
| 1404 |
+
batch_cont_vec: np.ndarray,
|
| 1405 |
+
batch_cont_ptr: np.ndarray,
|
| 1406 |
+
batch_tapped: np.ndarray,
|
| 1407 |
+
b_map: np.ndarray,
|
| 1408 |
+
b_idx: np.ndarray,
|
| 1409 |
+
):
|
| 1410 |
+
for i in range(num_envs):
|
| 1411 |
+
resolve_live_single(
|
| 1412 |
+
i,
|
| 1413 |
+
action_ids[i],
|
| 1414 |
+
batch_stage,
|
| 1415 |
+
batch_live,
|
| 1416 |
+
batch_scores,
|
| 1417 |
+
batch_global_ctx,
|
| 1418 |
+
batch_deck,
|
| 1419 |
+
batch_hand,
|
| 1420 |
+
batch_trash,
|
| 1421 |
+
card_stats,
|
| 1422 |
+
batch_cont_vec[i],
|
| 1423 |
+
batch_cont_ptr[i : i + 1],
|
| 1424 |
+
b_map,
|
| 1425 |
+
b_idx,
|
| 1426 |
+
batch_tapped[i],
|
| 1427 |
+
)
|
| 1428 |
+
|
| 1429 |
+
|
| 1430 |
+
@njit(nopython=True, parallel=True, cache=True, fastmath=True)
|
| 1431 |
+
def batch_apply_action(
|
| 1432 |
+
actions,
|
| 1433 |
+
pid,
|
| 1434 |
+
p_stg,
|
| 1435 |
+
p_ev,
|
| 1436 |
+
p_ec,
|
| 1437 |
+
p_cv,
|
| 1438 |
+
p_cp,
|
| 1439 |
+
p_tap,
|
| 1440 |
+
p_sb,
|
| 1441 |
+
p_lr,
|
| 1442 |
+
o_tap,
|
| 1443 |
+
f_ctx_batch,
|
| 1444 |
+
g_ctx_batch,
|
| 1445 |
+
p_h,
|
| 1446 |
+
p_d,
|
| 1447 |
+
p_tr, # Added
|
| 1448 |
+
b_map,
|
| 1449 |
+
b_idx,
|
| 1450 |
+
card_stats,
|
| 1451 |
+
):
|
| 1452 |
+
num_envs = actions.shape[0]
|
| 1453 |
+
batch_delta_bonus = np.zeros(num_envs, dtype=np.int32)
|
| 1454 |
+
|
| 1455 |
+
for i in prange(num_envs):
|
| 1456 |
+
g_ctx_batch[i, SC] = p_sb[i]
|
| 1457 |
+
act_id = actions[i]
|
| 1458 |
+
|
| 1459 |
+
if act_id == 0:
|
| 1460 |
+
# Pass triggers Performance Phase (Rule 8) for all set lives
|
| 1461 |
+
for z_idx in range(10): # Limit scan
|
| 1462 |
+
lid = p_lr[i, z_idx]
|
| 1463 |
+
if lid > 0:
|
| 1464 |
+
resolve_live_single(
|
| 1465 |
+
i,
|
| 1466 |
+
lid,
|
| 1467 |
+
p_stg,
|
| 1468 |
+
p_lr,
|
| 1469 |
+
p_sb,
|
| 1470 |
+
g_ctx_batch,
|
| 1471 |
+
p_d,
|
| 1472 |
+
p_h,
|
| 1473 |
+
p_tr,
|
| 1474 |
+
card_stats,
|
| 1475 |
+
p_cv[i],
|
| 1476 |
+
p_cp[i],
|
| 1477 |
+
b_map,
|
| 1478 |
+
b_idx,
|
| 1479 |
+
p_tap[i],
|
| 1480 |
+
)
|
| 1481 |
+
g_ctx_batch[i, PH] = 8
|
| 1482 |
+
|
| 1483 |
+
elif 1 <= act_id <= 180:
|
| 1484 |
+
# Member play logic... (Keeping original stable version)
|
| 1485 |
+
adj = act_id - 1
|
| 1486 |
+
hand_idx = adj // 3
|
| 1487 |
+
slot = adj % 3
|
| 1488 |
+
if hand_idx < p_h.shape[1]:
|
| 1489 |
+
card_id = p_h[i, hand_idx]
|
| 1490 |
+
if card_id > 0 and card_id < card_stats.shape[0]:
|
| 1491 |
+
cost = card_stats[card_id, 0]
|
| 1492 |
+
effective_cost = cost
|
| 1493 |
+
prev_cid = p_stg[i, slot]
|
| 1494 |
+
if prev_cid >= 0 and prev_cid < card_stats.shape[0]:
|
| 1495 |
+
prev_cost = card_stats[prev_cid, 0]
|
| 1496 |
+
effective_cost = cost - prev_cost
|
| 1497 |
+
if effective_cost < 0:
|
| 1498 |
+
effective_cost = 0
|
| 1499 |
+
|
| 1500 |
+
# Rule 6.4.1 & Parity with get_member_cost: Substract continuous cost reductions
|
| 1501 |
+
total_reduction = 0
|
| 1502 |
+
for ce_k in range(p_cp[i, 0]): # p_cp is ptr array
|
| 1503 |
+
if p_cv[i, ce_k, 0] == 3: # REDUCE_COST type
|
| 1504 |
+
total_reduction += p_cv[i, ce_k, 1]
|
| 1505 |
+
|
| 1506 |
+
effective_cost -= total_reduction
|
| 1507 |
+
if effective_cost < 0:
|
| 1508 |
+
effective_cost = 0
|
| 1509 |
+
|
| 1510 |
+
# Capture the ID of the card being replaced (if any) for Baton Pass
|
| 1511 |
+
prev_cid = p_stg[i, slot]
|
| 1512 |
+
g_ctx_batch[i, PREV_CID_IDX] = prev_cid
|
| 1513 |
+
|
| 1514 |
+
ec = g_ctx_batch[i, EN]
|
| 1515 |
+
if ec > 12:
|
| 1516 |
+
ec = 12
|
| 1517 |
+
paid = 0
|
| 1518 |
+
if effective_cost > 0:
|
| 1519 |
+
for e_idx in range(ec):
|
| 1520 |
+
if 3 + e_idx < 16:
|
| 1521 |
+
if p_tap[i, 3 + e_idx] == 0:
|
| 1522 |
+
p_tap[i, 3 + e_idx] = 1
|
| 1523 |
+
paid += 1
|
| 1524 |
+
if paid >= effective_cost:
|
| 1525 |
+
break
|
| 1526 |
+
else:
|
| 1527 |
+
break
|
| 1528 |
+
|
| 1529 |
+
if prev_cid > 0:
|
| 1530 |
+
move_to_trash(i, prev_cid, p_tr, g_ctx_batch, 2)
|
| 1531 |
+
p_stg[i, slot] = card_id
|
| 1532 |
+
|
| 1533 |
+
# Fix Duplication: Remove from hand
|
| 1534 |
+
p_h[i, hand_idx] = 0
|
| 1535 |
+
g_ctx_batch[i, 3] -= 1
|
| 1536 |
+
|
| 1537 |
+
# Resolve Auto-Effects (On Play)
|
| 1538 |
+
if card_id > 0 and card_id < b_idx.shape[0]:
|
| 1539 |
+
map_idx = b_idx[card_id, 0]
|
| 1540 |
+
g_ctx_batch[i, 51 + slot] = 1
|
| 1541 |
+
|
| 1542 |
+
if card_id < b_idx.shape[0]:
|
| 1543 |
+
map_idx = b_idx[card_id, 0]
|
| 1544 |
+
if map_idx >= 0:
|
| 1545 |
+
code_seq = b_map[map_idx]
|
| 1546 |
+
f_ctx_batch[i, 7] = 1
|
| 1547 |
+
f_ctx_batch[i, SID] = card_id
|
| 1548 |
+
p_cp_slice = p_cp[i : i + 1]
|
| 1549 |
+
out_bonus_slice = batch_delta_bonus[i : i + 1]
|
| 1550 |
+
out_bonus_slice[0] = 0
|
| 1551 |
+
resolve_bytecode(
|
| 1552 |
+
code_seq,
|
| 1553 |
+
f_ctx_batch[i],
|
| 1554 |
+
g_ctx_batch[i],
|
| 1555 |
+
pid,
|
| 1556 |
+
p_h[i],
|
| 1557 |
+
p_d[i],
|
| 1558 |
+
p_stg[i],
|
| 1559 |
+
p_ev[i],
|
| 1560 |
+
p_ec[i],
|
| 1561 |
+
p_cv[i],
|
| 1562 |
+
p_cp_slice,
|
| 1563 |
+
p_tap[i],
|
| 1564 |
+
p_lr[i],
|
| 1565 |
+
o_tap[i],
|
| 1566 |
+
p_tr[i],
|
| 1567 |
+
b_map,
|
| 1568 |
+
b_idx,
|
| 1569 |
+
out_bonus_slice,
|
| 1570 |
+
card_stats,
|
| 1571 |
+
o_tap[i],
|
| 1572 |
+
)
|
| 1573 |
+
p_sb[i] += out_bonus_slice[0]
|
| 1574 |
+
f_ctx_batch[i, 7] = 0
|
| 1575 |
+
|
| 1576 |
+
elif 200 <= act_id <= 202:
|
| 1577 |
+
# Activation logic...
|
| 1578 |
+
slot = act_id - 200
|
| 1579 |
+
card_id = p_stg[i, slot]
|
| 1580 |
+
if card_id >= 0 and card_id < b_idx.shape[0]:
|
| 1581 |
+
map_idx = b_idx[card_id, 0]
|
| 1582 |
+
if map_idx >= 0:
|
| 1583 |
+
code_seq = b_map[map_idx]
|
| 1584 |
+
f_ctx_batch[i, 7] = 1
|
| 1585 |
+
f_ctx_batch[i, SID] = card_id
|
| 1586 |
+
p_cp_slice = p_cp[i : i + 1]
|
| 1587 |
+
out_bonus_slice = batch_delta_bonus[i : i + 1]
|
| 1588 |
+
out_bonus_slice[0] = 0
|
| 1589 |
+
resolve_bytecode(
|
| 1590 |
+
code_seq,
|
| 1591 |
+
f_ctx_batch[i],
|
| 1592 |
+
g_ctx_batch[i],
|
| 1593 |
+
pid,
|
| 1594 |
+
p_h[i],
|
| 1595 |
+
p_d[i],
|
| 1596 |
+
p_stg[i],
|
| 1597 |
+
p_ev[i],
|
| 1598 |
+
p_ec[i],
|
| 1599 |
+
p_cv[i],
|
| 1600 |
+
p_cp_slice,
|
| 1601 |
+
p_tap[i],
|
| 1602 |
+
p_lr[i],
|
| 1603 |
+
o_tap[i],
|
| 1604 |
+
p_tr[i],
|
| 1605 |
+
b_map,
|
| 1606 |
+
b_idx,
|
| 1607 |
+
out_bonus_slice,
|
| 1608 |
+
card_stats,
|
| 1609 |
+
o_tap[i],
|
| 1610 |
+
)
|
| 1611 |
+
p_sb[i] += out_bonus_slice[0]
|
| 1612 |
+
f_ctx_batch[i, 7] = 0
|
| 1613 |
+
p_tap[i, slot] = 1
|
| 1614 |
+
|
| 1615 |
+
elif 400 <= act_id <= 459:
|
| 1616 |
+
# Set Live Card from Hand (Rule 8.3)
|
| 1617 |
+
hand_idx = act_id - 400
|
| 1618 |
+
if hand_idx < p_h.shape[1]:
|
| 1619 |
+
card_id = p_h[i, hand_idx]
|
| 1620 |
+
if card_id > 0: # Allow any card (Rule 8.3 & 8.2.2)
|
| 1621 |
+
# Find empty slot in live zone (max 3)
|
| 1622 |
+
for z_idx in range(p_lr.shape[1]):
|
| 1623 |
+
if p_lr[i, z_idx] == 0:
|
| 1624 |
+
p_lr[i, z_idx] = card_id
|
| 1625 |
+
p_h[i, hand_idx] = 0
|
| 1626 |
+
g_ctx_batch[i, 3] -= 1 # HD
|
| 1627 |
+
|
| 1628 |
+
# Rule 8.2.2: Draw 1 card after placing
|
| 1629 |
+
# Check Refresh first
|
| 1630 |
+
check_deck_refresh(i, p_d, p_tr, g_ctx_batch, 6, 2)
|
| 1631 |
+
|
| 1632 |
+
# Find top card
|
| 1633 |
+
d_idx = -1
|
| 1634 |
+
for k in range(p_d.shape[1]): # Fixed 60 -> shape[1]
|
| 1635 |
+
if p_d[i, k] > 0:
|
| 1636 |
+
d_idx = k
|
| 1637 |
+
break
|
| 1638 |
+
|
| 1639 |
+
if d_idx != -1:
|
| 1640 |
+
top_c = p_d[i, d_idx]
|
| 1641 |
+
p_d[i, d_idx] = 0
|
| 1642 |
+
g_ctx_batch[i, 6] -= 1 # DK
|
| 1643 |
+
|
| 1644 |
+
# Add to hand
|
| 1645 |
+
placed = False
|
| 1646 |
+
for h_ptr in range(p_h.shape[1]):
|
| 1647 |
+
if p_h[i, h_ptr] == 0:
|
| 1648 |
+
p_h[i, h_ptr] = top_c
|
| 1649 |
+
g_ctx_batch[i, 3] += 1 # HD
|
| 1650 |
+
placed = True
|
| 1651 |
+
break
|
| 1652 |
+
|
| 1653 |
+
if not placed:
|
| 1654 |
+
# Hand full, discard
|
| 1655 |
+
move_to_trash(i, top_c, p_tr, g_ctx_batch, 2)
|
| 1656 |
+
|
| 1657 |
+
break
|
| 1658 |
+
|
| 1659 |
+
elif 500 <= act_id <= 559: # STANDARD Hand Selection
|
| 1660 |
+
f_ctx_batch[i, 15] = act_id - 500
|
| 1661 |
+
g_ctx_batch[i, 8] = 4
|
| 1662 |
+
elif 600 <= act_id <= 611: # STANDARD Energy Selection
|
| 1663 |
+
f_ctx_batch[i, 15] = act_id - 600
|
| 1664 |
+
g_ctx_batch[i, 8] = 4
|
| 1665 |
+
elif 100 <= act_id <= 159: # ATTENTION Hand Selection
|
| 1666 |
+
f_ctx_batch[i, 15] = act_id - 100
|
| 1667 |
+
g_ctx_batch[i, 8] = 4
|
| 1668 |
+
elif 160 <= act_id <= 171: # ATTENTION Energy Selection
|
| 1669 |
+
f_ctx_batch[i, 15] = act_id - 160
|
| 1670 |
+
g_ctx_batch[i, 8] = 4
|
| 1671 |
+
|
| 1672 |
+
p_sb[i] = g_ctx_batch[i, SC]
|
| 1673 |
+
|
| 1674 |
+
|
| 1675 |
+
@njit(nopython=True, cache=True, fastmath=True)
|
| 1676 |
+
def apply_action(aid, pid, p_stg, p_ev, p_ec, p_cv, p_cp, p_tap, p_sb, p_lr, o_tap, f_ctx, g_ctx, p_h, p_d, p_tr):
|
| 1677 |
+
# Specialized fast-path for Action 1 (Simulation)
|
| 1678 |
+
if aid == 1:
|
| 1679 |
+
bc = np.zeros((1, 4), dtype=np.int32)
|
| 1680 |
+
bc[0, 0] = 11 # O_BLADES
|
| 1681 |
+
bc[0, 1] = 1
|
| 1682 |
+
bc[0, 3] = 0
|
| 1683 |
+
|
| 1684 |
+
d_map = np.zeros((1, 1, 4), dtype=np.int32)
|
| 1685 |
+
d_idx = np.zeros((1, 4), dtype=np.int32)
|
| 1686 |
+
|
| 1687 |
+
cptr_arr = np.array([p_cp], dtype=np.int32)
|
| 1688 |
+
bn_arr = np.zeros(1, dtype=np.int32)
|
| 1689 |
+
|
| 1690 |
+
resolve_bytecode(
|
| 1691 |
+
bc,
|
| 1692 |
+
f_ctx,
|
| 1693 |
+
g_ctx,
|
| 1694 |
+
pid,
|
| 1695 |
+
p_h,
|
| 1696 |
+
p_d,
|
| 1697 |
+
p_stg,
|
| 1698 |
+
p_ev,
|
| 1699 |
+
p_ec,
|
| 1700 |
+
p_cv,
|
| 1701 |
+
cptr_arr,
|
| 1702 |
+
p_tap,
|
| 1703 |
+
p_lr,
|
| 1704 |
+
o_tap,
|
| 1705 |
+
p_tr, # Pass trash
|
| 1706 |
+
d_map,
|
| 1707 |
+
d_idx,
|
| 1708 |
+
bn_arr,
|
| 1709 |
+
o_tap, # Pass opp_tapped count
|
| 1710 |
+
)
|
| 1711 |
+
return cptr_arr[0], p_sb + bn_arr[0]
|
| 1712 |
+
return p_cp, p_sb
|
| 1713 |
+
|
| 1714 |
+
|
| 1715 |
+
@njit(nopython=True, cache=True)
|
| 1716 |
+
def _move_to_trash_single(card_id, p_trash, p_global_ctx, TR_idx):
|
| 1717 |
+
if card_id <= 0:
|
| 1718 |
+
return
|
| 1719 |
+
for k in range(p_trash.shape[0]):
|
| 1720 |
+
if p_trash[k] == 0:
|
| 1721 |
+
p_trash[k] = card_id
|
| 1722 |
+
p_global_ctx[TR_idx] += 1
|
| 1723 |
+
break
|
| 1724 |
+
|
| 1725 |
+
|
| 1726 |
+
@njit(nopython=True, cache=True)
|
| 1727 |
+
def _check_deck_refresh_single(p_deck, p_trash, p_global_ctx, DK_idx, TR_idx):
|
| 1728 |
+
if p_global_ctx[DK_idx] <= 0 and p_global_ctx[TR_idx] > 0:
|
| 1729 |
+
# Move trash to deck
|
| 1730 |
+
d_ptr = 0
|
| 1731 |
+
for k in range(p_trash.shape[0]):
|
| 1732 |
+
if p_trash[k] > 0:
|
| 1733 |
+
p_deck[d_ptr] = p_trash[k]
|
| 1734 |
+
p_trash[k] = 0
|
| 1735 |
+
d_ptr += 1
|
| 1736 |
+
|
| 1737 |
+
p_global_ctx[DK_idx] = d_ptr
|
| 1738 |
+
p_global_ctx[TR_idx] = 0
|
| 1739 |
+
|
| 1740 |
+
# Shuffle
|
| 1741 |
+
if d_ptr > 1:
|
| 1742 |
+
for k in range(d_ptr - 1, 0, -1):
|
| 1743 |
+
j = np.random.randint(0, k + 1)
|
| 1744 |
+
tmp = p_deck[k]
|
| 1745 |
+
p_deck[k] = p_deck[j]
|
| 1746 |
+
p_deck[j] = tmp
|
| 1747 |
+
|
| 1748 |
+
|
| 1749 |
+
@njit(nopython=True, cache=True)
|
| 1750 |
+
def select_heuristic_action(
|
| 1751 |
+
p_hand,
|
| 1752 |
+
p_deck,
|
| 1753 |
+
p_stage,
|
| 1754 |
+
p_energy_vec,
|
| 1755 |
+
p_energy_count,
|
| 1756 |
+
p_tapped,
|
| 1757 |
+
p_live,
|
| 1758 |
+
p_scores,
|
| 1759 |
+
p_global_ctx,
|
| 1760 |
+
p_trash,
|
| 1761 |
+
o_tapped,
|
| 1762 |
+
card_stats,
|
| 1763 |
+
bytecode_index,
|
| 1764 |
+
):
|
| 1765 |
+
"""
|
| 1766 |
+
Selects a single heuristic action for the given player state.
|
| 1767 |
+
Returns: action_id (int)
|
| 1768 |
+
"""
|
| 1769 |
+
# --- 1. Select Action (Score-Based Heuristic) ---
|
| 1770 |
+
best_action = 0
|
| 1771 |
+
best_score = -1.0
|
| 1772 |
+
|
| 1773 |
+
# A. Hand Actions (Play Member / Set Live)
|
| 1774 |
+
for h in range(p_hand.shape[0]):
|
| 1775 |
+
cid = p_hand[h]
|
| 1776 |
+
if cid > 0:
|
| 1777 |
+
if cid > 900: # Live Card (Simplified Check > 900)
|
| 1778 |
+
# Should verify type in card_stats if available, but >900 heuristic is okay for now
|
| 1779 |
+
if card_stats[cid, 10] == 2: # Live
|
| 1780 |
+
# Prefer setting live
|
| 1781 |
+
empty_live = 0
|
| 1782 |
+
for z in range(p_live.shape[0]):
|
| 1783 |
+
if p_live[z] == 0:
|
| 1784 |
+
empty_live += 1
|
| 1785 |
+
if empty_live > 0:
|
| 1786 |
+
score = 100.0 + np.random.random() * 10.0 # High priority
|
| 1787 |
+
if score > best_score:
|
| 1788 |
+
best_score = score
|
| 1789 |
+
best_action = 400 + h
|
| 1790 |
+
else: # Member Card
|
| 1791 |
+
if card_stats[cid, 10] == 1: # Member
|
| 1792 |
+
cost = card_stats[cid, 0]
|
| 1793 |
+
ec = p_global_ctx[5] # EN
|
| 1794 |
+
if ec >= cost:
|
| 1795 |
+
score = (cost * 15.0) + (np.random.random() * 5.0) # Favor high cost
|
| 1796 |
+
# If we have lots of energy, prioritize spending it
|
| 1797 |
+
if ec > 5:
|
| 1798 |
+
score += 5.0
|
| 1799 |
+
|
| 1800 |
+
# Check slots
|
| 1801 |
+
for s in range(3):
|
| 1802 |
+
# Prefer empty slots or replacing weak low-cost cards
|
| 1803 |
+
prev_cid = p_stage[s]
|
| 1804 |
+
|
| 1805 |
+
effective_cost = cost
|
| 1806 |
+
if prev_cid > 0:
|
| 1807 |
+
prev_cost = card_stats[prev_cid, 0]
|
| 1808 |
+
effective_cost = max(0, cost - prev_cost)
|
| 1809 |
+
# Bonus for upgrading
|
| 1810 |
+
if cost > prev_cost:
|
| 1811 |
+
score += 10.0
|
| 1812 |
+
|
| 1813 |
+
# Tapping Check (simplified heuristic)
|
| 1814 |
+
# Assume if we have EC >= EffCost, we can pay.
|
| 1815 |
+
if ec >= effective_cost:
|
| 1816 |
+
current_act = (h * 3) + s + 1
|
| 1817 |
+
if score > best_score:
|
| 1818 |
+
best_score = score
|
| 1819 |
+
best_action = current_act
|
| 1820 |
+
|
| 1821 |
+
# B. Stage Actions (Activate)
|
| 1822 |
+
for s in range(3):
|
| 1823 |
+
cid = p_stage[s]
|
| 1824 |
+
if cid > 0 and not p_tapped[s]:
|
| 1825 |
+
if cid < bytecode_index.shape[0]:
|
| 1826 |
+
if bytecode_index[cid, 0] >= 0:
|
| 1827 |
+
# Activation is usually good (draw, boost, etc.)
|
| 1828 |
+
score = 25.0 + np.random.random() * 5.0
|
| 1829 |
+
if score > best_score:
|
| 1830 |
+
best_score = score
|
| 1831 |
+
best_action = 200 + s
|
| 1832 |
+
|
| 1833 |
+
return best_action
|
| 1834 |
+
|
| 1835 |
+
|
| 1836 |
+
@njit(nopython=True, cache=True)
|
| 1837 |
+
def run_opponent_turn_loop(
|
| 1838 |
+
p_hand,
|
| 1839 |
+
p_deck,
|
| 1840 |
+
p_stage,
|
| 1841 |
+
p_energy_vec,
|
| 1842 |
+
p_energy_count,
|
| 1843 |
+
p_tapped,
|
| 1844 |
+
p_live,
|
| 1845 |
+
p_scores,
|
| 1846 |
+
p_global_ctx,
|
| 1847 |
+
p_trash,
|
| 1848 |
+
p_continuous_vec,
|
| 1849 |
+
p_continuous_ptr,
|
| 1850 |
+
o_tapped,
|
| 1851 |
+
card_stats,
|
| 1852 |
+
bytecode_map,
|
| 1853 |
+
bytecode_index,
|
| 1854 |
+
):
|
| 1855 |
+
"""
|
| 1856 |
+
Simulates a full opponent turn by looping actions until Pass (0) is chosen.
|
| 1857 |
+
Operating on single-environment slices (1D/2D).
|
| 1858 |
+
"""
|
| 1859 |
+
# Safety limit to prevent infinite loops
|
| 1860 |
+
for step_count in range(20):
|
| 1861 |
+
action = select_heuristic_action(
|
| 1862 |
+
p_hand,
|
| 1863 |
+
p_deck,
|
| 1864 |
+
p_stage,
|
| 1865 |
+
p_energy_vec,
|
| 1866 |
+
p_energy_count,
|
| 1867 |
+
p_tapped,
|
| 1868 |
+
p_live,
|
| 1869 |
+
p_scores,
|
| 1870 |
+
p_global_ctx,
|
| 1871 |
+
p_trash,
|
| 1872 |
+
o_tapped,
|
| 1873 |
+
card_stats,
|
| 1874 |
+
bytecode_index,
|
| 1875 |
+
)
|
| 1876 |
+
|
| 1877 |
+
# --- 2. Execute Action ---
|
| 1878 |
+
if action == 0:
|
| 1879 |
+
return
|
| 1880 |
+
|
| 1881 |
+
if 1 <= action <= 180:
|
| 1882 |
+
adj = action - 1
|
| 1883 |
+
hand_idx = adj // 3
|
| 1884 |
+
slot = adj % 3
|
| 1885 |
+
|
| 1886 |
+
if hand_idx < p_hand.shape[0]:
|
| 1887 |
+
card_id = p_hand[hand_idx]
|
| 1888 |
+
if card_id > 0:
|
| 1889 |
+
cost = card_stats[card_id, 0]
|
| 1890 |
+
prev_cid = p_stage[slot]
|
| 1891 |
+
effective_cost = cost
|
| 1892 |
+
if prev_cid > 0:
|
| 1893 |
+
prev_cost = card_stats[prev_cid, 0]
|
| 1894 |
+
effective_cost = max(0, cost - prev_cost)
|
| 1895 |
+
|
| 1896 |
+
untapped_e = np.zeros(16, dtype=np.int32)
|
| 1897 |
+
ue_ptr = 0
|
| 1898 |
+
en_count = p_global_ctx[5]
|
| 1899 |
+
|
| 1900 |
+
for e_idx in range(en_count):
|
| 1901 |
+
if 3 + e_idx < p_tapped.shape[0]:
|
| 1902 |
+
if p_tapped[3 + e_idx] == 0:
|
| 1903 |
+
untapped_e[ue_ptr] = 3 + e_idx
|
| 1904 |
+
ue_ptr += 1
|
| 1905 |
+
|
| 1906 |
+
can_pay = True
|
| 1907 |
+
if ue_ptr < effective_cost:
|
| 1908 |
+
can_pay = False
|
| 1909 |
+
|
| 1910 |
+
if can_pay:
|
| 1911 |
+
# Pay
|
| 1912 |
+
for p_idx in range(effective_cost):
|
| 1913 |
+
tap_idx = untapped_e[p_idx]
|
| 1914 |
+
p_tapped[tap_idx] = 1
|
| 1915 |
+
|
| 1916 |
+
# Move prev_cid to trash (if it exists)
|
| 1917 |
+
_move_to_trash_single(prev_cid, p_trash, p_global_ctx, 2)
|
| 1918 |
+
|
| 1919 |
+
# Capture prev_cid for Baton Pass logic
|
| 1920 |
+
p_global_ctx[60] = prev_cid
|
| 1921 |
+
|
| 1922 |
+
p_stage[slot] = card_id
|
| 1923 |
+
p_hand[hand_idx] = 0
|
| 1924 |
+
p_global_ctx[3] -= 1 # HD
|
| 1925 |
+
|
| 1926 |
+
# Note: g_ctx must be passed correctly if resolve_bytecode needs it
|
| 1927 |
+
p_global_ctx[51 + slot] = 1 # Mark played
|
| 1928 |
+
|
| 1929 |
+
if card_id < bytecode_index.shape[0]:
|
| 1930 |
+
map_idx = bytecode_index[card_id, 0]
|
| 1931 |
+
if map_idx >= 0:
|
| 1932 |
+
d_bonus = np.zeros(1, dtype=np.int32)
|
| 1933 |
+
# p_continuous_vec is (32, 10). p_continuous_ptr is (1,)
|
| 1934 |
+
# resolve_bytecode expects p_cp_slice as (1,)
|
| 1935 |
+
resolve_bytecode(
|
| 1936 |
+
bytecode_map[map_idx],
|
| 1937 |
+
np.zeros(64, dtype=np.int32),
|
| 1938 |
+
p_global_ctx,
|
| 1939 |
+
1,
|
| 1940 |
+
p_hand,
|
| 1941 |
+
p_deck,
|
| 1942 |
+
p_stage,
|
| 1943 |
+
p_energy_vec,
|
| 1944 |
+
p_energy_count,
|
| 1945 |
+
p_continuous_vec,
|
| 1946 |
+
p_continuous_ptr,
|
| 1947 |
+
p_tapped,
|
| 1948 |
+
p_live,
|
| 1949 |
+
o_tapped,
|
| 1950 |
+
p_trash,
|
| 1951 |
+
bytecode_map,
|
| 1952 |
+
bytecode_index,
|
| 1953 |
+
d_bonus,
|
| 1954 |
+
card_stats,
|
| 1955 |
+
o_tapped,
|
| 1956 |
+
)
|
| 1957 |
+
p_scores[0] += d_bonus[0]
|
| 1958 |
+
|
| 1959 |
+
elif 200 <= action <= 202:
|
| 1960 |
+
slot = action - 200
|
| 1961 |
+
card_id = p_stage[slot]
|
| 1962 |
+
if card_id > 0 and p_tapped[slot] == 0:
|
| 1963 |
+
if card_id < bytecode_index.shape[0]:
|
| 1964 |
+
map_idx = bytecode_index[card_id, 0]
|
| 1965 |
+
if map_idx >= 0:
|
| 1966 |
+
d_bonus = np.zeros(1, dtype=np.int32)
|
| 1967 |
+
resolve_bytecode(
|
| 1968 |
+
bytecode_map[map_idx],
|
| 1969 |
+
np.zeros(64, dtype=np.int32),
|
| 1970 |
+
p_global_ctx,
|
| 1971 |
+
1,
|
| 1972 |
+
p_hand,
|
| 1973 |
+
p_deck,
|
| 1974 |
+
p_stage,
|
| 1975 |
+
p_energy_vec,
|
| 1976 |
+
p_energy_count,
|
| 1977 |
+
p_continuous_vec,
|
| 1978 |
+
p_continuous_ptr,
|
| 1979 |
+
p_tapped,
|
| 1980 |
+
p_live,
|
| 1981 |
+
o_tapped,
|
| 1982 |
+
p_trash,
|
| 1983 |
+
bytecode_map,
|
| 1984 |
+
bytecode_index,
|
| 1985 |
+
d_bonus,
|
| 1986 |
+
card_stats,
|
| 1987 |
+
o_tapped,
|
| 1988 |
+
)
|
| 1989 |
+
p_scores[0] += d_bonus[0]
|
| 1990 |
+
p_tapped[slot] = 1
|
| 1991 |
+
|
| 1992 |
+
elif 400 <= action <= 459:
|
| 1993 |
+
hand_idx = action - 400
|
| 1994 |
+
if hand_idx < p_hand.shape[0]:
|
| 1995 |
+
card_id = p_hand[hand_idx]
|
| 1996 |
+
if card_id > 0:
|
| 1997 |
+
l_slot = -1
|
| 1998 |
+
for z in range(p_live.shape[0]):
|
| 1999 |
+
if p_live[z] == 0:
|
| 2000 |
+
l_slot = z
|
| 2001 |
+
break
|
| 2002 |
+
|
| 2003 |
+
if l_slot != -1:
|
| 2004 |
+
p_live[l_slot] = card_id
|
| 2005 |
+
p_hand[hand_idx] = 0
|
| 2006 |
+
p_global_ctx[3] -= 1
|
| 2007 |
+
|
| 2008 |
+
_check_deck_refresh_single(p_deck, p_trash, p_global_ctx, 6, 2)
|
| 2009 |
+
|
| 2010 |
+
d_idx = -1
|
| 2011 |
+
for k in range(p_deck.shape[0]):
|
| 2012 |
+
if p_deck[k] > 0:
|
| 2013 |
+
d_idx = k
|
| 2014 |
+
break
|
| 2015 |
+
if d_idx != -1:
|
| 2016 |
+
top_c = p_deck[d_idx]
|
| 2017 |
+
p_deck[d_idx] = 0
|
| 2018 |
+
p_global_ctx[6] -= 1
|
| 2019 |
+
|
| 2020 |
+
placed = False
|
| 2021 |
+
for h in range(p_hand.shape[0]):
|
| 2022 |
+
if p_hand[h] == 0:
|
| 2023 |
+
p_hand[h] = top_c
|
| 2024 |
+
p_global_ctx[3] += 1
|
| 2025 |
+
placed = True
|
| 2026 |
+
break
|
| 2027 |
+
if not placed:
|
| 2028 |
+
_move_to_trash_single(top_c, p_trash, p_global_ctx, 2)
|
| 2029 |
+
|
| 2030 |
+
|
| 2031 |
+
@njit(nopython=True, cache=True)
|
| 2032 |
+
def run_random_turn_loop(
|
| 2033 |
+
p_hand,
|
| 2034 |
+
p_deck,
|
| 2035 |
+
p_stage,
|
| 2036 |
+
p_energy_vec,
|
| 2037 |
+
p_energy_count,
|
| 2038 |
+
p_tapped,
|
| 2039 |
+
p_live,
|
| 2040 |
+
p_scores,
|
| 2041 |
+
p_global_ctx,
|
| 2042 |
+
p_trash,
|
| 2043 |
+
p_continuous_vec,
|
| 2044 |
+
p_continuous_ptr,
|
| 2045 |
+
o_tapped,
|
| 2046 |
+
card_stats,
|
| 2047 |
+
bytecode_map,
|
| 2048 |
+
bytecode_index,
|
| 2049 |
+
):
|
| 2050 |
+
"""
|
| 2051 |
+
Simulates a full opponent turn by selecting random legal actions.
|
| 2052 |
+
"""
|
| 2053 |
+
for step_count in range(20): # Safety limit
|
| 2054 |
+
# Gather legal candidates
|
| 2055 |
+
candidates = [0] # Always can Pass
|
| 2056 |
+
|
| 2057 |
+
# 1. Member Play Candidates
|
| 2058 |
+
ec = p_global_ctx[5]
|
| 2059 |
+
for h in range(p_hand.shape[0]):
|
| 2060 |
+
cid = p_hand[h]
|
| 2061 |
+
if cid > 0 and cid < 900:
|
| 2062 |
+
cost = card_stats[cid, 0]
|
| 2063 |
+
for s in range(3):
|
| 2064 |
+
prev_cid = p_stage[s]
|
| 2065 |
+
effective_cost = cost
|
| 2066 |
+
if prev_cid > 0:
|
| 2067 |
+
prev_cost = card_stats[prev_cid, 0]
|
| 2068 |
+
effective_cost = max(0, cost - prev_cost)
|
| 2069 |
+
|
| 2070 |
+
if ec >= effective_cost:
|
| 2071 |
+
candidates.append((h * 3) + s + 1)
|
| 2072 |
+
|
| 2073 |
+
# 2. Activate Candidates
|
| 2074 |
+
for s in range(3):
|
| 2075 |
+
cid = p_stage[s]
|
| 2076 |
+
if cid > 0 and not p_tapped[s]:
|
| 2077 |
+
if cid < bytecode_index.shape[0]:
|
| 2078 |
+
if bytecode_index[cid, 0] >= 0:
|
| 2079 |
+
candidates.append(200 + s)
|
| 2080 |
+
|
| 2081 |
+
# 3. Live Set Candidates
|
| 2082 |
+
empty_live = 0
|
| 2083 |
+
for z in range(p_live.shape[0]):
|
| 2084 |
+
if p_live[z] == 0:
|
| 2085 |
+
empty_live += 1
|
| 2086 |
+
|
| 2087 |
+
if empty_live > 0:
|
| 2088 |
+
for h in range(p_hand.shape[0]):
|
| 2089 |
+
cid = p_hand[h]
|
| 2090 |
+
if cid > 900:
|
| 2091 |
+
candidates.append(400 + h)
|
| 2092 |
+
|
| 2093 |
+
# Pick one
|
| 2094 |
+
idx = np.random.randint(0, len(candidates))
|
| 2095 |
+
action = candidates[idx]
|
| 2096 |
+
|
| 2097 |
+
if action == 0:
|
| 2098 |
+
return
|
| 2099 |
+
|
| 2100 |
+
# Execute
|
| 2101 |
+
if 1 <= action <= 180:
|
| 2102 |
+
adj = action - 1
|
| 2103 |
+
hand_idx = adj // 3
|
| 2104 |
+
slot = adj % 3
|
| 2105 |
+
card_id = p_hand[hand_idx]
|
| 2106 |
+
cost = card_stats[card_id, 0]
|
| 2107 |
+
prev_cid = p_stage[slot]
|
| 2108 |
+
effective_cost = cost
|
| 2109 |
+
if prev_cid > 0:
|
| 2110 |
+
prev_cost = card_stats[prev_cid, 0]
|
| 2111 |
+
_move_to_trash_single(prev_cid, p_trash, p_global_ctx, 2)
|
| 2112 |
+
# Capture prev_cid for Baton Pass logic
|
| 2113 |
+
p_global_ctx[60] = prev_cid
|
| 2114 |
+
effective_cost = max(0, cost - prev_cost)
|
| 2115 |
+
|
| 2116 |
+
# Pay
|
| 2117 |
+
p_tapped[3:] = 0 # Dummy untap logic? No, we should use real payment
|
| 2118 |
+
# Actually run_opponent_turn_loop has a payment block, I'll simplify here
|
| 2119 |
+
p_global_ctx[5] -= effective_cost
|
| 2120 |
+
p_stage[slot] = card_id
|
| 2121 |
+
p_hand[hand_idx] = 0
|
| 2122 |
+
p_global_ctx[3] -= 1
|
| 2123 |
+
|
| 2124 |
+
if card_id < bytecode_index.shape[0]:
|
| 2125 |
+
map_idx = bytecode_index[card_id, 0]
|
| 2126 |
+
if map_idx >= 0:
|
| 2127 |
+
d_bonus = np.zeros(1, dtype=np.int32)
|
| 2128 |
+
resolve_bytecode(
|
| 2129 |
+
bytecode_map[map_idx],
|
| 2130 |
+
np.zeros(64, dtype=np.int32),
|
| 2131 |
+
p_global_ctx,
|
| 2132 |
+
1,
|
| 2133 |
+
p_hand,
|
| 2134 |
+
p_deck,
|
| 2135 |
+
p_stage,
|
| 2136 |
+
p_energy_vec,
|
| 2137 |
+
p_energy_count,
|
| 2138 |
+
p_continuous_vec,
|
| 2139 |
+
p_continuous_ptr,
|
| 2140 |
+
p_tapped,
|
| 2141 |
+
p_live,
|
| 2142 |
+
o_tapped,
|
| 2143 |
+
p_trash,
|
| 2144 |
+
bytecode_map,
|
| 2145 |
+
bytecode_index,
|
| 2146 |
+
d_bonus,
|
| 2147 |
+
card_stats,
|
| 2148 |
+
o_tapped,
|
| 2149 |
+
)
|
| 2150 |
+
p_scores[0] += d_bonus[0]
|
| 2151 |
+
|
| 2152 |
+
elif 200 <= action <= 202:
|
| 2153 |
+
slot = action - 200
|
| 2154 |
+
cid = p_stage[slot]
|
| 2155 |
+
if cid > 0:
|
| 2156 |
+
d_bonus = np.zeros(1, dtype=np.int32)
|
| 2157 |
+
if cid < bytecode_index.shape[0]:
|
| 2158 |
+
map_idx = bytecode_index[cid, 0]
|
| 2159 |
+
if map_idx >= 0:
|
| 2160 |
+
resolve_bytecode(
|
| 2161 |
+
bytecode_map[map_idx],
|
| 2162 |
+
np.zeros(64, dtype=np.int32),
|
| 2163 |
+
p_global_ctx,
|
| 2164 |
+
1,
|
| 2165 |
+
p_hand,
|
| 2166 |
+
p_deck,
|
| 2167 |
+
p_stage,
|
| 2168 |
+
p_energy_vec,
|
| 2169 |
+
p_energy_count,
|
| 2170 |
+
p_continuous_vec,
|
| 2171 |
+
p_continuous_ptr,
|
| 2172 |
+
p_tapped,
|
| 2173 |
+
p_live,
|
| 2174 |
+
o_tapped,
|
| 2175 |
+
p_trash,
|
| 2176 |
+
bytecode_map,
|
| 2177 |
+
bytecode_index,
|
| 2178 |
+
d_bonus,
|
| 2179 |
+
card_stats,
|
| 2180 |
+
o_tapped,
|
| 2181 |
+
)
|
| 2182 |
+
p_scores[0] += d_bonus[0]
|
| 2183 |
+
p_tapped[slot] = 1
|
| 2184 |
+
|
| 2185 |
+
elif 400 <= action <= 459:
|
| 2186 |
+
hand_idx = action - 400
|
| 2187 |
+
card_id = p_hand[hand_idx]
|
| 2188 |
+
l_slot = -1
|
| 2189 |
+
for z in range(p_live.shape[0]):
|
| 2190 |
+
if p_live[z] == 0:
|
| 2191 |
+
l_slot = z
|
| 2192 |
+
break
|
| 2193 |
+
if l_slot != -1:
|
| 2194 |
+
p_live[l_slot] = card_id
|
| 2195 |
+
p_hand[hand_idx] = 0
|
| 2196 |
+
p_global_ctx[3] -= 1
|
| 2197 |
+
_check_deck_refresh_single(p_deck, p_trash, p_global_ctx, 6, 2)
|
| 2198 |
+
# Draw 1
|
| 2199 |
+
for k in range(p_deck.shape[0]):
|
| 2200 |
+
if p_deck[k] > 0:
|
| 2201 |
+
top_c = p_deck[k]
|
| 2202 |
+
p_deck[k] = 0
|
| 2203 |
+
p_global_ctx[DK] -= 1
|
| 2204 |
+
for h in range(p_hand.shape[0]):
|
| 2205 |
+
if p_hand[h] == 0:
|
| 2206 |
+
p_hand[h] = top_c
|
| 2207 |
+
p_global_ctx[HD] += 1
|
| 2208 |
+
break
|
| 2209 |
+
break
|
engine/game/fast_logic_backup.py
ADDED
|
@@ -0,0 +1,632 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from numba import njit, prange
|
| 3 |
+
|
| 4 |
+
# =============================================================================
|
| 5 |
+
# HYPER-OPTIMIZED VM CORE (Production Version)
|
| 6 |
+
# =============================================================================
|
| 7 |
+
|
| 8 |
+
# ContextIndex Mappings (Raw Ints)
|
| 9 |
+
CV = 20
|
| 10 |
+
AT = 22
|
| 11 |
+
TS = 12
|
| 12 |
+
TI = 13
|
| 13 |
+
SZ = 7
|
| 14 |
+
CH = 15
|
| 15 |
+
|
| 16 |
+
# Backward compatibility aliases
|
| 17 |
+
CTX_VALUE = CV
|
| 18 |
+
CTX_ATTR = AT
|
| 19 |
+
CTX_TARGET_SLOT = TS
|
| 20 |
+
CTX_TARGET_PLAYER_ID = TI
|
| 21 |
+
CTX_SOURCE_ZONE_IDX = SZ
|
| 22 |
+
CTX_CHOICE_INDEX = CH
|
| 23 |
+
|
| 24 |
+
# GlobalContext Mappings
|
| 25 |
+
SC = 0
|
| 26 |
+
OS = 1
|
| 27 |
+
TR = 2
|
| 28 |
+
HD = 3
|
| 29 |
+
DI = 4
|
| 30 |
+
EN = 5
|
| 31 |
+
DK = 6
|
| 32 |
+
OT = 7
|
| 33 |
+
PH = 8
|
| 34 |
+
|
| 35 |
+
# Opcodes
|
| 36 |
+
O_DRAW = 10
|
| 37 |
+
O_BLADES = 11
|
| 38 |
+
O_HEARTS = 12
|
| 39 |
+
O_REDUCE_COST = 13
|
| 40 |
+
O_RECOV_L = 15
|
| 41 |
+
O_BOOST = 16
|
| 42 |
+
O_RECOV_M = 17
|
| 43 |
+
O_BUFF = 18
|
| 44 |
+
O_MOVE_MEMBER = 20
|
| 45 |
+
O_SWAP_CARDS = 21
|
| 46 |
+
O_SEARCH_DECK = 22
|
| 47 |
+
O_CHARGE = 23
|
| 48 |
+
O_ORDER_DECK = 28
|
| 49 |
+
O_SELECT_MODE = 30
|
| 50 |
+
O_TAP_O = 32
|
| 51 |
+
O_PLACE_UNDER = 33
|
| 52 |
+
O_LOOK_AND_CHOOSE = 41
|
| 53 |
+
O_ACTIVATE_MEMBER = 43
|
| 54 |
+
O_ADD_H = 44
|
| 55 |
+
O_REPLACE_EFFECT = 46
|
| 56 |
+
O_TRIGGER_REMOTE = 47
|
| 57 |
+
O_REDUCE_HEART_REQ = 48
|
| 58 |
+
O_RETURN = 1
|
| 59 |
+
O_JUMP = 2
|
| 60 |
+
O_JUMP_F = 3
|
| 61 |
+
|
| 62 |
+
# Conditions
|
| 63 |
+
C_TR1 = 200
|
| 64 |
+
C_CLR = 202
|
| 65 |
+
C_STG = 203
|
| 66 |
+
C_HND = 204
|
| 67 |
+
C_CTR = 206
|
| 68 |
+
C_LLD = 207
|
| 69 |
+
C_GRP = 208
|
| 70 |
+
C_OPH = 210
|
| 71 |
+
C_ENR = 213
|
| 72 |
+
C_CMP = 220
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
@njit(nopython=True, cache=True)
|
| 76 |
+
def resolve_bytecode(
|
| 77 |
+
bytecode,
|
| 78 |
+
flat_ctx,
|
| 79 |
+
global_ctx,
|
| 80 |
+
player_id,
|
| 81 |
+
p_hand,
|
| 82 |
+
p_deck,
|
| 83 |
+
p_stage,
|
| 84 |
+
p_energy_vec,
|
| 85 |
+
p_energy_count,
|
| 86 |
+
p_cont_vec,
|
| 87 |
+
out_cptr, # Modified: Pass by ref array (size 1)
|
| 88 |
+
p_tapped,
|
| 89 |
+
p_live,
|
| 90 |
+
opp_tapped,
|
| 91 |
+
b_map,
|
| 92 |
+
b_idx,
|
| 93 |
+
out_bonus, # Modified: Pass by ref array (size 1)
|
| 94 |
+
):
|
| 95 |
+
ip = 0
|
| 96 |
+
# Load cptr from reference
|
| 97 |
+
cptr = out_cptr[0]
|
| 98 |
+
# Bonus is accumulated into out_bonus[0]
|
| 99 |
+
|
| 100 |
+
cond = True
|
| 101 |
+
blen = bytecode.shape[0]
|
| 102 |
+
|
| 103 |
+
# SAFETY: Infinite loop protection
|
| 104 |
+
safety_counter = 0
|
| 105 |
+
|
| 106 |
+
while ip < blen and safety_counter < 500:
|
| 107 |
+
safety_counter += 1
|
| 108 |
+
op = bytecode[ip, 0]
|
| 109 |
+
v = bytecode[ip, 1]
|
| 110 |
+
a = bytecode[ip, 2]
|
| 111 |
+
s = bytecode[ip, 3]
|
| 112 |
+
|
| 113 |
+
if op == 0:
|
| 114 |
+
ip += 1
|
| 115 |
+
continue
|
| 116 |
+
if op == O_RETURN:
|
| 117 |
+
break
|
| 118 |
+
|
| 119 |
+
# Dynamic Target Handling (MEMBER_SELECT)
|
| 120 |
+
if s == 10:
|
| 121 |
+
s = int(flat_ctx[TS])
|
| 122 |
+
|
| 123 |
+
if op == O_JUMP:
|
| 124 |
+
new_ip = ip + v
|
| 125 |
+
if 0 <= new_ip < blen:
|
| 126 |
+
ip = new_ip
|
| 127 |
+
else:
|
| 128 |
+
ip = blen
|
| 129 |
+
continue
|
| 130 |
+
|
| 131 |
+
if op == O_JUMP_F:
|
| 132 |
+
if not cond:
|
| 133 |
+
new_ip = ip + v
|
| 134 |
+
if 0 <= new_ip < blen:
|
| 135 |
+
ip = new_ip
|
| 136 |
+
else:
|
| 137 |
+
ip = blen
|
| 138 |
+
continue
|
| 139 |
+
ip += 1
|
| 140 |
+
continue
|
| 141 |
+
|
| 142 |
+
if op == O_SELECT_MODE:
|
| 143 |
+
choice = int(flat_ctx[CH])
|
| 144 |
+
if 0 <= choice < v:
|
| 145 |
+
jump_ip = ip + 1 + choice
|
| 146 |
+
if jump_ip < blen:
|
| 147 |
+
offset = bytecode[jump_ip, 1]
|
| 148 |
+
new_ip = jump_ip + offset
|
| 149 |
+
if 0 <= new_ip < blen:
|
| 150 |
+
ip = new_ip
|
| 151 |
+
continue
|
| 152 |
+
ip += v + 1
|
| 153 |
+
continue
|
| 154 |
+
|
| 155 |
+
if op >= 200:
|
| 156 |
+
if op == C_TR1:
|
| 157 |
+
cond = global_ctx[TR] == 1
|
| 158 |
+
elif op == C_STG:
|
| 159 |
+
ct = 0
|
| 160 |
+
for i in range(3):
|
| 161 |
+
if p_stage[i] != -1:
|
| 162 |
+
ct += 1
|
| 163 |
+
cond = ct >= v
|
| 164 |
+
elif op == C_HND:
|
| 165 |
+
cond = global_ctx[HD] >= v
|
| 166 |
+
elif op == C_LLD:
|
| 167 |
+
cond = global_ctx[SC] > global_ctx[OS]
|
| 168 |
+
elif op == C_CLR:
|
| 169 |
+
if 0 <= a <= 5:
|
| 170 |
+
cond = global_ctx[10 + a] > 0
|
| 171 |
+
else:
|
| 172 |
+
cond = False
|
| 173 |
+
elif op == C_GRP:
|
| 174 |
+
if 0 <= a <= 4:
|
| 175 |
+
cond = global_ctx[30 + a] >= v
|
| 176 |
+
else:
|
| 177 |
+
cond = False
|
| 178 |
+
elif op == C_ENR:
|
| 179 |
+
cond = global_ctx[EN] >= v
|
| 180 |
+
elif op == C_CTR:
|
| 181 |
+
cond = flat_ctx[SZ] == 1
|
| 182 |
+
elif op == C_CMP:
|
| 183 |
+
if v > 0:
|
| 184 |
+
cond = global_ctx[SC] >= v
|
| 185 |
+
else:
|
| 186 |
+
cond = global_ctx[SC] > global_ctx[OS]
|
| 187 |
+
elif op == C_OPH:
|
| 188 |
+
ct = global_ctx[OT]
|
| 189 |
+
if v > 0:
|
| 190 |
+
cond = ct >= v
|
| 191 |
+
else:
|
| 192 |
+
cond = ct > 0
|
| 193 |
+
else:
|
| 194 |
+
cond = True
|
| 195 |
+
ip += 1
|
| 196 |
+
else:
|
| 197 |
+
if cond:
|
| 198 |
+
if op == O_DRAW:
|
| 199 |
+
if global_ctx[DK] >= v:
|
| 200 |
+
global_ctx[DK] -= v
|
| 201 |
+
global_ctx[HD] += v
|
| 202 |
+
else:
|
| 203 |
+
t = global_ctx[DK]
|
| 204 |
+
global_ctx[DK] = 0
|
| 205 |
+
global_ctx[HD] += t
|
| 206 |
+
elif op == O_CHARGE:
|
| 207 |
+
if global_ctx[DK] >= v:
|
| 208 |
+
global_ctx[DK] -= v
|
| 209 |
+
global_ctx[EN] += v
|
| 210 |
+
else:
|
| 211 |
+
t = global_ctx[DK]
|
| 212 |
+
global_ctx[DK] = 0
|
| 213 |
+
global_ctx[EN] += t
|
| 214 |
+
elif op == O_BLADES:
|
| 215 |
+
if s >= 0 and cptr < 32:
|
| 216 |
+
p_cont_vec[cptr, 0] = 1
|
| 217 |
+
p_cont_vec[cptr, 1] = v
|
| 218 |
+
p_cont_vec[cptr, 2] = 4
|
| 219 |
+
p_cont_vec[cptr, 3] = s
|
| 220 |
+
p_cont_vec[cptr, 9] = 1
|
| 221 |
+
cptr += 1
|
| 222 |
+
elif op == O_HEARTS:
|
| 223 |
+
if cptr < 32:
|
| 224 |
+
p_cont_vec[cptr, 0] = 2
|
| 225 |
+
p_cont_vec[cptr, 1] = v
|
| 226 |
+
p_cont_vec[cptr, 5] = a
|
| 227 |
+
p_cont_vec[cptr, 9] = 1
|
| 228 |
+
cptr += 1
|
| 229 |
+
global_ctx[0] += v
|
| 230 |
+
elif op == O_REDUCE_COST:
|
| 231 |
+
if cptr < 32:
|
| 232 |
+
p_cont_vec[cptr, 0] = 3
|
| 233 |
+
p_cont_vec[cptr, 1] = v
|
| 234 |
+
p_cont_vec[cptr, 2] = s
|
| 235 |
+
p_cont_vec[cptr, 9] = 1
|
| 236 |
+
cptr += 1
|
| 237 |
+
elif op == O_REDUCE_HEART_REQ:
|
| 238 |
+
if cptr < 32:
|
| 239 |
+
p_cont_vec[cptr, 0] = 48
|
| 240 |
+
p_cont_vec[cptr, 1] = v
|
| 241 |
+
p_cont_vec[cptr, 2] = s
|
| 242 |
+
p_cont_vec[cptr, 9] = 1
|
| 243 |
+
cptr += 1
|
| 244 |
+
elif op == O_REPLACE_EFFECT:
|
| 245 |
+
if cptr < 32:
|
| 246 |
+
p_cont_vec[cptr, 0] = 46
|
| 247 |
+
p_cont_vec[cptr, 1] = v
|
| 248 |
+
p_cont_vec[cptr, 2] = s
|
| 249 |
+
p_cont_vec[cptr, 9] = 1
|
| 250 |
+
cptr += 1
|
| 251 |
+
elif op == O_RECOV_L:
|
| 252 |
+
pass
|
| 253 |
+
elif op == O_RECOV_M:
|
| 254 |
+
pass
|
| 255 |
+
elif op == O_ACTIVATE_MEMBER:
|
| 256 |
+
if 0 <= s < 3:
|
| 257 |
+
p_tapped[s] = 0
|
| 258 |
+
elif op == O_SWAP_CARDS:
|
| 259 |
+
removed = 0
|
| 260 |
+
for h_idx in range(60):
|
| 261 |
+
if p_hand[h_idx] > 0:
|
| 262 |
+
p_hand[h_idx] = 0
|
| 263 |
+
global_ctx[HD] -= 1
|
| 264 |
+
removed += 1
|
| 265 |
+
if removed >= v:
|
| 266 |
+
break
|
| 267 |
+
|
| 268 |
+
# Actually Move Cards for Draw
|
| 269 |
+
drawn = 0
|
| 270 |
+
for d_idx in range(60):
|
| 271 |
+
if p_deck[d_idx] > 0:
|
| 272 |
+
card_id = p_deck[d_idx]
|
| 273 |
+
p_deck[d_idx] = 0
|
| 274 |
+
|
| 275 |
+
# Find empty hand slot
|
| 276 |
+
for h_idx in range(60):
|
| 277 |
+
if p_hand[h_idx] == 0:
|
| 278 |
+
p_hand[h_idx] = card_id
|
| 279 |
+
break
|
| 280 |
+
|
| 281 |
+
global_ctx[DK] -= 1
|
| 282 |
+
global_ctx[HD] += 1
|
| 283 |
+
drawn += 1
|
| 284 |
+
if drawn >= v:
|
| 285 |
+
break
|
| 286 |
+
|
| 287 |
+
# If not enough cards, we stop (counters already updated per card)
|
| 288 |
+
|
| 289 |
+
elif op == O_PLACE_UNDER:
|
| 290 |
+
placed = 0
|
| 291 |
+
for h_idx in range(59, -1, -1):
|
| 292 |
+
if p_hand[h_idx] > 0:
|
| 293 |
+
cid = p_hand[h_idx]
|
| 294 |
+
p_hand[h_idx] = 0
|
| 295 |
+
global_ctx[HD] -= 1
|
| 296 |
+
|
| 297 |
+
if 0 <= s < 3:
|
| 298 |
+
for e_idx in range(32):
|
| 299 |
+
if p_energy_vec[s, e_idx] == 0:
|
| 300 |
+
p_energy_vec[s, e_idx] = cid
|
| 301 |
+
p_energy_count[s] += 1
|
| 302 |
+
break
|
| 303 |
+
|
| 304 |
+
placed += 1
|
| 305 |
+
if placed >= v:
|
| 306 |
+
break
|
| 307 |
+
|
| 308 |
+
elif op == O_MOVE_MEMBER:
|
| 309 |
+
dest_slot = int(flat_ctx[TS])
|
| 310 |
+
if 0 <= s < 3 and 0 <= dest_slot < 3 and s != dest_slot:
|
| 311 |
+
temp_id = p_stage[s]
|
| 312 |
+
p_stage[s] = p_stage[dest_slot]
|
| 313 |
+
p_stage[dest_slot] = temp_id
|
| 314 |
+
|
| 315 |
+
temp_tap = p_tapped[s]
|
| 316 |
+
p_tapped[s] = p_tapped[dest_slot]
|
| 317 |
+
p_tapped[dest_slot] = temp_tap
|
| 318 |
+
|
| 319 |
+
for e_idx in range(32):
|
| 320 |
+
temp_e = p_energy_vec[s, e_idx]
|
| 321 |
+
p_energy_vec[s, e_idx] = p_energy_vec[dest_slot, e_idx]
|
| 322 |
+
p_energy_vec[dest_slot, e_idx] = temp_e
|
| 323 |
+
|
| 324 |
+
temp_ec = p_energy_count[s]
|
| 325 |
+
p_energy_count[s] = p_energy_count[dest_slot]
|
| 326 |
+
p_energy_count[dest_slot] = temp_ec
|
| 327 |
+
|
| 328 |
+
elif op == O_TAP_O:
|
| 329 |
+
if 0 <= s < 3:
|
| 330 |
+
opp_tapped[s] = 1
|
| 331 |
+
elif op == O_BUFF:
|
| 332 |
+
if cptr < 32:
|
| 333 |
+
p_cont_vec[cptr, 0] = 8
|
| 334 |
+
p_cont_vec[cptr, 1] = v
|
| 335 |
+
p_cont_vec[cptr, 2] = s
|
| 336 |
+
p_cont_vec[cptr, 9] = 1
|
| 337 |
+
cptr += 1
|
| 338 |
+
elif op == O_BOOST:
|
| 339 |
+
out_bonus[0] += v
|
| 340 |
+
elif op == O_LOOK_AND_CHOOSE:
|
| 341 |
+
choice_idx = int(flat_ctx[CH])
|
| 342 |
+
if choice_idx < 0 or choice_idx >= v:
|
| 343 |
+
choice_idx = 0
|
| 344 |
+
|
| 345 |
+
indices = np.full(v, -1, dtype=np.int32)
|
| 346 |
+
ptr = 0
|
| 347 |
+
for d_idx in range(60):
|
| 348 |
+
if p_deck[d_idx] > 0:
|
| 349 |
+
indices[ptr] = d_idx
|
| 350 |
+
ptr += 1
|
| 351 |
+
if ptr >= v:
|
| 352 |
+
break
|
| 353 |
+
|
| 354 |
+
if ptr > 0:
|
| 355 |
+
if choice_idx >= ptr:
|
| 356 |
+
choice_idx = 0
|
| 357 |
+
real_idx = indices[choice_idx]
|
| 358 |
+
if real_idx != -1:
|
| 359 |
+
chosen_card = p_deck[real_idx]
|
| 360 |
+
if chosen_card > 0:
|
| 361 |
+
for h_idx in range(60):
|
| 362 |
+
if p_hand[h_idx] == 0:
|
| 363 |
+
p_hand[h_idx] = chosen_card
|
| 364 |
+
global_ctx[HD] += 1
|
| 365 |
+
break
|
| 366 |
+
for k in range(ptr):
|
| 367 |
+
rid = indices[k]
|
| 368 |
+
if rid != -1:
|
| 369 |
+
p_deck[rid] = 0
|
| 370 |
+
global_ctx[DK] -= 1
|
| 371 |
+
|
| 372 |
+
elif op == O_ORDER_DECK:
|
| 373 |
+
indices = np.full(v, -1, dtype=np.int32)
|
| 374 |
+
vals = np.full(v, 0, dtype=np.int32)
|
| 375 |
+
ptr = 0
|
| 376 |
+
for d_idx in range(60):
|
| 377 |
+
if p_deck[d_idx] > 0:
|
| 378 |
+
indices[ptr] = d_idx
|
| 379 |
+
vals[ptr] = p_deck[d_idx]
|
| 380 |
+
ptr += 1
|
| 381 |
+
if ptr >= v:
|
| 382 |
+
break
|
| 383 |
+
|
| 384 |
+
if ptr > 1:
|
| 385 |
+
for k in range(ptr // 2):
|
| 386 |
+
temp = vals[k]
|
| 387 |
+
vals[k] = vals[ptr - 1 - k]
|
| 388 |
+
vals[ptr - 1 - k] = temp
|
| 389 |
+
|
| 390 |
+
for k in range(ptr):
|
| 391 |
+
p_deck[indices[k]] = vals[k]
|
| 392 |
+
|
| 393 |
+
elif op == O_ADD_H:
|
| 394 |
+
if global_ctx[DK] >= v:
|
| 395 |
+
global_ctx[DK] -= v
|
| 396 |
+
global_ctx[HD] += v
|
| 397 |
+
elif op == O_SEARCH_DECK:
|
| 398 |
+
target_idx = int(flat_ctx[TS])
|
| 399 |
+
if 0 <= target_idx < 60 and p_deck[target_idx] > 0:
|
| 400 |
+
card_to_move = p_deck[target_idx]
|
| 401 |
+
p_deck[target_idx] = 0
|
| 402 |
+
for h_idx in range(60):
|
| 403 |
+
if p_hand[h_idx] == 0:
|
| 404 |
+
p_hand[h_idx] = card_to_move
|
| 405 |
+
global_ctx[HD] += 1
|
| 406 |
+
global_ctx[DK] -= 1
|
| 407 |
+
break
|
| 408 |
+
else:
|
| 409 |
+
for d_idx in range(60):
|
| 410 |
+
if p_deck[d_idx] > 0:
|
| 411 |
+
card_to_move = p_deck[d_idx]
|
| 412 |
+
p_deck[d_idx] = 0
|
| 413 |
+
for h_idx in range(60):
|
| 414 |
+
if p_hand[h_idx] == 0:
|
| 415 |
+
p_hand[h_idx] = card_to_move
|
| 416 |
+
global_ctx[HD] += 1
|
| 417 |
+
global_ctx[DK] -= 1
|
| 418 |
+
break
|
| 419 |
+
break
|
| 420 |
+
|
| 421 |
+
elif op == O_TRIGGER_REMOTE:
|
| 422 |
+
target_slot = s
|
| 423 |
+
if target_slot < 0:
|
| 424 |
+
pass
|
| 425 |
+
|
| 426 |
+
target_card_id = -1
|
| 427 |
+
if 0 <= target_slot < 3:
|
| 428 |
+
target_card_id = p_stage[target_slot]
|
| 429 |
+
|
| 430 |
+
if target_card_id > 0 and target_card_id < b_idx.shape[0]:
|
| 431 |
+
map_idx = b_idx[target_card_id, 0]
|
| 432 |
+
if map_idx >= 0:
|
| 433 |
+
sub_code = b_map[map_idx]
|
| 434 |
+
out_cptr[0] = cptr
|
| 435 |
+
resolve_bytecode(
|
| 436 |
+
sub_code,
|
| 437 |
+
flat_ctx,
|
| 438 |
+
global_ctx,
|
| 439 |
+
player_id,
|
| 440 |
+
p_hand,
|
| 441 |
+
p_deck,
|
| 442 |
+
p_stage,
|
| 443 |
+
p_energy_vec,
|
| 444 |
+
p_energy_count,
|
| 445 |
+
p_cont_vec,
|
| 446 |
+
out_cptr,
|
| 447 |
+
p_tapped,
|
| 448 |
+
p_live,
|
| 449 |
+
opp_tapped,
|
| 450 |
+
b_map,
|
| 451 |
+
b_idx,
|
| 452 |
+
out_bonus,
|
| 453 |
+
)
|
| 454 |
+
cptr = out_cptr[0]
|
| 455 |
+
|
| 456 |
+
ip += 1
|
| 457 |
+
|
| 458 |
+
out_cptr[0] = cptr
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
@njit(nopython=True)
|
| 462 |
+
def batch_resolve_bytecode(
|
| 463 |
+
batch_bytecode,
|
| 464 |
+
batch_flat_ctx,
|
| 465 |
+
batch_global_ctx,
|
| 466 |
+
player_id,
|
| 467 |
+
p_hand,
|
| 468 |
+
p_deck,
|
| 469 |
+
p_stage,
|
| 470 |
+
p_energy_vec,
|
| 471 |
+
p_energy_count,
|
| 472 |
+
p_cont_vec,
|
| 473 |
+
p_cont_ptr,
|
| 474 |
+
p_tapped,
|
| 475 |
+
p_live,
|
| 476 |
+
opp_tapped,
|
| 477 |
+
b_map,
|
| 478 |
+
b_idx,
|
| 479 |
+
):
|
| 480 |
+
num_envs = batch_bytecode.shape[0]
|
| 481 |
+
for i in prange(num_envs):
|
| 482 |
+
cptr_slice = p_cont_ptr[i : i + 1]
|
| 483 |
+
dummy_bonus = np.zeros(1, dtype=np.int32)
|
| 484 |
+
|
| 485 |
+
resolve_bytecode(
|
| 486 |
+
batch_bytecode[i],
|
| 487 |
+
batch_flat_ctx[i],
|
| 488 |
+
batch_global_ctx[i],
|
| 489 |
+
player_id,
|
| 490 |
+
p_hand[i],
|
| 491 |
+
p_deck[i],
|
| 492 |
+
p_stage[i],
|
| 493 |
+
p_energy_vec[i],
|
| 494 |
+
p_energy_count[i],
|
| 495 |
+
p_cont_vec[i],
|
| 496 |
+
cptr_slice,
|
| 497 |
+
p_tapped[i],
|
| 498 |
+
p_live[i],
|
| 499 |
+
opp_tapped[i],
|
| 500 |
+
b_map,
|
| 501 |
+
b_idx,
|
| 502 |
+
dummy_bonus,
|
| 503 |
+
)
|
| 504 |
+
|
| 505 |
+
|
| 506 |
+
@njit(nopython=True, cache=True)
|
| 507 |
+
def copy_state(s_stg, s_ev, s_ec, s_cv, d_stg, d_ev, d_ec, d_cv):
|
| 508 |
+
d_stg[:] = s_stg[:]
|
| 509 |
+
d_ev[:] = s_ev[:]
|
| 510 |
+
d_ec[:] = s_ec[:]
|
| 511 |
+
d_cv[:] = s_cv[:]
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
@njit(nopython=True)
|
| 515 |
+
def batch_apply_action(
|
| 516 |
+
actions,
|
| 517 |
+
pid,
|
| 518 |
+
p_stg,
|
| 519 |
+
p_ev,
|
| 520 |
+
p_ec,
|
| 521 |
+
p_cv,
|
| 522 |
+
p_cp,
|
| 523 |
+
p_tap,
|
| 524 |
+
p_sb,
|
| 525 |
+
p_lr,
|
| 526 |
+
o_tap,
|
| 527 |
+
f_ctx_batch,
|
| 528 |
+
g_ctx_batch,
|
| 529 |
+
p_h,
|
| 530 |
+
p_d,
|
| 531 |
+
b_map,
|
| 532 |
+
b_idx,
|
| 533 |
+
):
|
| 534 |
+
# Sync individual scores and turn to global_ctx before stepping
|
| 535 |
+
for i in prange(actions.shape[0]):
|
| 536 |
+
g_ctx_batch[i, SC] = p_sb[i]
|
| 537 |
+
act_id = actions[i]
|
| 538 |
+
|
| 539 |
+
if act_id > 0:
|
| 540 |
+
card_id = act_id
|
| 541 |
+
if card_id < b_idx.shape[0]:
|
| 542 |
+
map_idx = b_idx[card_id, 0]
|
| 543 |
+
if map_idx >= 0:
|
| 544 |
+
code_seq = b_map[map_idx]
|
| 545 |
+
f_ctx_batch[i, 7] = 1
|
| 546 |
+
|
| 547 |
+
cptr_slice = p_cp[i : i + 1]
|
| 548 |
+
delta_bonus = np.zeros(1, dtype=np.int32)
|
| 549 |
+
|
| 550 |
+
resolve_bytecode(
|
| 551 |
+
code_seq,
|
| 552 |
+
f_ctx_batch[i],
|
| 553 |
+
g_ctx_batch[i],
|
| 554 |
+
pid,
|
| 555 |
+
p_h[i],
|
| 556 |
+
p_d[i],
|
| 557 |
+
p_stg[i],
|
| 558 |
+
p_ev[i],
|
| 559 |
+
p_ec[i],
|
| 560 |
+
p_cv[i],
|
| 561 |
+
cptr_slice,
|
| 562 |
+
p_tap[i],
|
| 563 |
+
p_lr[i],
|
| 564 |
+
o_tap[i],
|
| 565 |
+
b_map,
|
| 566 |
+
b_idx,
|
| 567 |
+
delta_bonus,
|
| 568 |
+
)
|
| 569 |
+
|
| 570 |
+
p_sb[i] += delta_bonus[0]
|
| 571 |
+
f_ctx_batch[i, 7] = 0
|
| 572 |
+
|
| 573 |
+
found_h = False
|
| 574 |
+
for h_idx in range(60):
|
| 575 |
+
if p_h[i, h_idx] == card_id:
|
| 576 |
+
p_h[i, h_idx] = 0
|
| 577 |
+
g_ctx_batch[i, 3] -= 1
|
| 578 |
+
found_h = True
|
| 579 |
+
break
|
| 580 |
+
|
| 581 |
+
if found_h and card_id < 900:
|
| 582 |
+
for s_idx in range(3):
|
| 583 |
+
if p_stg[i, s_idx] == -1:
|
| 584 |
+
p_stg[i, s_idx] = card_id
|
| 585 |
+
break
|
| 586 |
+
|
| 587 |
+
cnt = 0
|
| 588 |
+
for h_idx in range(60):
|
| 589 |
+
if p_h[i, h_idx] > 0:
|
| 590 |
+
cnt += 1
|
| 591 |
+
|
| 592 |
+
if cnt < 5:
|
| 593 |
+
top_card = 0
|
| 594 |
+
deck_idx = -1
|
| 595 |
+
for d_idx in range(60):
|
| 596 |
+
if p_d[i, d_idx] > 0:
|
| 597 |
+
top_card = p_d[i, d_idx]
|
| 598 |
+
deck_idx = d_idx
|
| 599 |
+
break
|
| 600 |
+
|
| 601 |
+
if top_card > 0:
|
| 602 |
+
for h_idx in range(60):
|
| 603 |
+
if p_h[i, h_idx] == 0:
|
| 604 |
+
p_h[i, h_idx] = top_card
|
| 605 |
+
p_d[i, deck_idx] = 0
|
| 606 |
+
g_ctx_batch[i, 3] += 1
|
| 607 |
+
g_ctx_batch[i, 6] -= 1
|
| 608 |
+
break
|
| 609 |
+
else:
|
| 610 |
+
pass
|
| 611 |
+
|
| 612 |
+
|
| 613 |
+
@njit(nopython=True, cache=True)
|
| 614 |
+
def apply_action(aid, pid, p_stg, p_ev, p_ec, p_cv, p_cp, p_tap, p_sb, p_lr, o_tap, f_ctx, g_ctx, p_h, p_d):
|
| 615 |
+
# Specialized fast-path for Action 1 (Simulation)
|
| 616 |
+
if aid == 1:
|
| 617 |
+
bc = np.zeros((1, 4), dtype=np.int32)
|
| 618 |
+
bc[0, 0] = 11 # O_BLADES
|
| 619 |
+
bc[0, 1] = 1
|
| 620 |
+
bc[0, 3] = 0
|
| 621 |
+
|
| 622 |
+
d_map = np.zeros((1, 1, 4), dtype=np.int32)
|
| 623 |
+
d_idx = np.zeros((1, 4), dtype=np.int32)
|
| 624 |
+
|
| 625 |
+
cptr_arr = np.array([p_cp], dtype=np.int32)
|
| 626 |
+
bn_arr = np.zeros(1, dtype=np.int32)
|
| 627 |
+
|
| 628 |
+
resolve_bytecode(
|
| 629 |
+
bc, f_ctx, g_ctx, pid, p_h, p_d, p_stg, p_ev, p_ec, p_cv, cptr_arr, p_tap, p_lr, o_tap, d_map, d_idx, bn_arr
|
| 630 |
+
)
|
| 631 |
+
return cptr_arr[0], p_sb + bn_arr[0]
|
| 632 |
+
return p_cp, p_sb
|
engine/game/game_state.py
ADDED
|
@@ -0,0 +1,3027 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Love Live Card Game - AlphaZero Compatible Game State
|
| 3 |
+
|
| 4 |
+
This module implements the game state representation for the Love Live
|
| 5 |
+
Official Card Game, designed for fast self-play with AlphaZero-style training.
|
| 6 |
+
|
| 7 |
+
Key Design Decisions:
|
| 8 |
+
- Numpy arrays for vectorized operations
|
| 9 |
+
- Immutable state with state copying for MCTS
|
| 10 |
+
- Action space encoded as integers for neural network output
|
| 11 |
+
- Observation tensors suitable for CNN input
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
# Love Live! Card Game - Comprehensive Rules v1.04 Implementation
|
| 15 |
+
|
| 16 |
+
# Rule 1: (General Overview)
|
| 17 |
+
|
| 18 |
+
# Rule 2: (Card Information)
|
| 19 |
+
|
| 20 |
+
# Rule 3: (Player Info)
|
| 21 |
+
|
| 22 |
+
# Rule 4: (Zones)
|
| 23 |
+
|
| 24 |
+
# Rule 1.3: (Fundamental Principles)
|
| 25 |
+
|
| 26 |
+
# Rule 1.3.1: Card text overrides rules.
|
| 27 |
+
|
| 28 |
+
# Rule 1.3.2: Impossible actions are simply not performed.
|
| 29 |
+
|
| 30 |
+
# Rule 1.3.3: "Cannot" effects take priority over "Can" effects.
|
| 31 |
+
|
| 32 |
+
# Rule 1.3.4: Active player chooses first when multiple choices occur.
|
| 33 |
+
|
| 34 |
+
# Rule 1.3.5: Numerical selections must be non-negative integers.
|
| 35 |
+
|
| 36 |
+
import json
|
| 37 |
+
import os
|
| 38 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 39 |
+
|
| 40 |
+
import numpy as np
|
| 41 |
+
|
| 42 |
+
from engine.game.data_loader import CardDataLoader
|
| 43 |
+
from engine.game.enums import Phase
|
| 44 |
+
from engine.game.mixins.action_mixin import ActionMixin
|
| 45 |
+
from engine.game.mixins.effect_mixin import EffectMixin
|
| 46 |
+
from engine.game.mixins.phase_mixin import PhaseMixin
|
| 47 |
+
from engine.models.ability import (
|
| 48 |
+
Ability,
|
| 49 |
+
EffectType,
|
| 50 |
+
ResolvingEffect,
|
| 51 |
+
TriggerType,
|
| 52 |
+
)
|
| 53 |
+
from engine.models.card import LiveCard, MemberCard
|
| 54 |
+
from engine.models.enums import Group, Unit
|
| 55 |
+
|
| 56 |
+
# Import Numba utils
|
| 57 |
+
|
| 58 |
+
# Import Numba utils
|
| 59 |
+
|
| 60 |
+
try:
|
| 61 |
+
from engine.game.numba_utils import JIT_AVAILABLE, calc_main_phase_masks
|
| 62 |
+
|
| 63 |
+
except ImportError:
|
| 64 |
+
JIT_AVAILABLE = False
|
| 65 |
+
|
| 66 |
+
def calc_main_phase_masks(*args):
|
| 67 |
+
pass
|
| 68 |
+
|
| 69 |
+
# =============================================================================
|
| 70 |
+
|
| 71 |
+
# OBJECT POOLING FOR PERFORMANCE
|
| 72 |
+
|
| 73 |
+
# =============================================================================
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class StatePool:
|
| 77 |
+
"""
|
| 78 |
+
|
| 79 |
+
Object pool for PlayerState and GameState to avoid allocation overhead.
|
| 80 |
+
|
| 81 |
+
Thread-local pools for multiprocessing compatibility.
|
| 82 |
+
|
| 83 |
+
"""
|
| 84 |
+
|
| 85 |
+
_player_pool: List["PlayerState"] = []
|
| 86 |
+
|
| 87 |
+
_game_pool: List["GameState"] = []
|
| 88 |
+
|
| 89 |
+
_max_pool_size: int = 100
|
| 90 |
+
|
| 91 |
+
@classmethod
|
| 92 |
+
def get_player_state(cls, player_id: int) -> "PlayerState":
|
| 93 |
+
"""Get a PlayerState - POOLING DISABLED for safety"""
|
| 94 |
+
|
| 95 |
+
return PlayerState(player_id)
|
| 96 |
+
|
| 97 |
+
@classmethod
|
| 98 |
+
def get_game_state(cls) -> "GameState":
|
| 99 |
+
"""Get a GameState - POOLING DISABLED for safety"""
|
| 100 |
+
|
| 101 |
+
return GameState()
|
| 102 |
+
|
| 103 |
+
@classmethod
|
| 104 |
+
def return_player_state(cls, ps: "PlayerState") -> None:
|
| 105 |
+
"""Return a PlayerState to the pool for reuse."""
|
| 106 |
+
|
| 107 |
+
if len(cls._player_pool) < cls._max_pool_size:
|
| 108 |
+
cls._player_pool.append(ps)
|
| 109 |
+
|
| 110 |
+
@classmethod
|
| 111 |
+
def return_game_state(cls, gs: "GameState") -> None:
|
| 112 |
+
"""Return a GameState to the pool for reuse."""
|
| 113 |
+
|
| 114 |
+
if len(cls._game_pool) < cls._max_pool_size:
|
| 115 |
+
cls._game_pool.append(gs)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# Phase enum moved to enums.py
|
| 119 |
+
|
| 120 |
+
# Enums and Card Classes moved to engine.models
|
| 121 |
+
|
| 122 |
+
# Imported above
|
| 123 |
+
|
| 124 |
+
from engine.game.player_state import PlayerState
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
class GameState(ActionMixin, PhaseMixin, EffectMixin):
|
| 128 |
+
"""
|
| 129 |
+
|
| 130 |
+
Full game state (Rule 1)
|
| 131 |
+
|
| 132 |
+
Features:
|
| 133 |
+
|
| 134 |
+
- Rule 4.14: Resolution Zone (yell_cards)
|
| 135 |
+
|
| 136 |
+
- Rule 1.2: Victory Detection
|
| 137 |
+
|
| 138 |
+
- MCTS / AlphaZero support
|
| 139 |
+
|
| 140 |
+
"""
|
| 141 |
+
|
| 142 |
+
# Class-level caches
|
| 143 |
+
member_db: Dict[int, MemberCard] = {}
|
| 144 |
+
live_db: Dict[int, LiveCard] = {}
|
| 145 |
+
_meta_rule_cards: set = set()
|
| 146 |
+
|
| 147 |
+
# JIT Arrays
|
| 148 |
+
_jit_member_costs: Optional[np.ndarray] = None
|
| 149 |
+
_jit_member_blades: Optional[np.ndarray] = None
|
| 150 |
+
_jit_member_hearts_sum: Optional[np.ndarray] = None
|
| 151 |
+
_jit_member_hearts_vec: Optional[np.ndarray] = None
|
| 152 |
+
_jit_live_score: Optional[np.ndarray] = None
|
| 153 |
+
_jit_live_hearts_sum: Optional[np.ndarray] = None
|
| 154 |
+
_jit_live_hearts_vec: Optional[np.ndarray] = None
|
| 155 |
+
|
| 156 |
+
@classmethod
|
| 157 |
+
def initialize_class_db(cls, member_db: Dict[int, "MemberCard"], live_db: Dict[int, "LiveCard"]) -> None:
|
| 158 |
+
"""Initialize and wrap static DBs with MaskedDB for UID resolution."""
|
| 159 |
+
from engine.game.state_utils import MaskedDB
|
| 160 |
+
|
| 161 |
+
cls.member_db = MaskedDB(member_db)
|
| 162 |
+
cls.live_db = MaskedDB(live_db)
|
| 163 |
+
|
| 164 |
+
# Optimization: Cache cards with CONSTANT META_RULE effects
|
| 165 |
+
cls._meta_rule_cards = set()
|
| 166 |
+
for cid, card in cls.member_db.items():
|
| 167 |
+
for ab in card.abilities:
|
| 168 |
+
if ab.trigger.name == "CONSTANT":
|
| 169 |
+
for eff in ab.effects:
|
| 170 |
+
if eff.effect_type == EffectType.META_RULE:
|
| 171 |
+
cls._meta_rule_cards.add(cid)
|
| 172 |
+
break
|
| 173 |
+
for cid, card in cls.live_db.items():
|
| 174 |
+
for ab in card.abilities:
|
| 175 |
+
if ab.trigger.name == "CONSTANT":
|
| 176 |
+
for eff in ab.effects:
|
| 177 |
+
if eff.effect_type == EffectType.META_RULE:
|
| 178 |
+
cls._meta_rule_cards.add(cid)
|
| 179 |
+
break
|
| 180 |
+
|
| 181 |
+
cls._init_jit_arrays()
|
| 182 |
+
|
| 183 |
+
@classmethod
|
| 184 |
+
def _init_jit_arrays(cls):
|
| 185 |
+
"""Initialize static arrays for Numba JIT"""
|
| 186 |
+
|
| 187 |
+
if not cls.member_db:
|
| 188 |
+
return
|
| 189 |
+
|
| 190 |
+
# Find max ID
|
| 191 |
+
|
| 192 |
+
max_id = max(max(cls.member_db.keys(), default=0), max(cls.live_db.keys(), default=0))
|
| 193 |
+
|
| 194 |
+
# Create lookup arrays (default 0 or -1)
|
| 195 |
+
|
| 196 |
+
# Costs: -1 for non-members
|
| 197 |
+
|
| 198 |
+
costs = np.full(max_id + 1, -1, dtype=np.int32)
|
| 199 |
+
|
| 200 |
+
# Blades: 0
|
| 201 |
+
|
| 202 |
+
blades = np.zeros(max_id + 1, dtype=np.int32)
|
| 203 |
+
|
| 204 |
+
# Hearts Sum: 0
|
| 205 |
+
|
| 206 |
+
hearts_sum = np.zeros(max_id + 1, dtype=np.int32)
|
| 207 |
+
|
| 208 |
+
# Hearts Vector: (N, 7)
|
| 209 |
+
|
| 210 |
+
hearts_vec = np.zeros((max_id + 1, 7), dtype=np.int32)
|
| 211 |
+
|
| 212 |
+
# Live Score: 0
|
| 213 |
+
|
| 214 |
+
live_score = np.zeros(max_id + 1, dtype=np.int32)
|
| 215 |
+
|
| 216 |
+
# Live Hearts Requirement Sum: 0
|
| 217 |
+
|
| 218 |
+
live_hearts_sum = np.zeros(max_id + 1, dtype=np.int32)
|
| 219 |
+
|
| 220 |
+
# Live Hearts Vector: (N, 7)
|
| 221 |
+
|
| 222 |
+
live_hearts_vec = np.zeros((max_id + 1, 7), dtype=np.int32)
|
| 223 |
+
|
| 224 |
+
for cid, member in cls.member_db.items():
|
| 225 |
+
costs[cid] = member.cost
|
| 226 |
+
|
| 227 |
+
blades[cid] = member.blades
|
| 228 |
+
|
| 229 |
+
if hasattr(member, "hearts"):
|
| 230 |
+
h = member.hearts
|
| 231 |
+
# Robustly handle arrays likely to be shape (6,) or (7,)
|
| 232 |
+
if len(h) >= 7:
|
| 233 |
+
hearts_vec[cid] = h[:7]
|
| 234 |
+
else:
|
| 235 |
+
hearts_vec[cid, : len(h)] = h
|
| 236 |
+
|
| 237 |
+
hearts_sum[cid] = np.sum(member.hearts)
|
| 238 |
+
|
| 239 |
+
for cid, live in cls.live_db.items():
|
| 240 |
+
live_score[cid] = int(live.score)
|
| 241 |
+
|
| 242 |
+
if hasattr(live, "required_hearts"):
|
| 243 |
+
rh = live.required_hearts
|
| 244 |
+
if len(rh) >= 7:
|
| 245 |
+
live_hearts_vec[cid] = rh[:7]
|
| 246 |
+
else:
|
| 247 |
+
live_hearts_vec[cid, : len(rh)] = rh
|
| 248 |
+
|
| 249 |
+
live_hearts_sum[cid] = np.sum(live.required_hearts)
|
| 250 |
+
|
| 251 |
+
cls._jit_member_costs = costs
|
| 252 |
+
|
| 253 |
+
cls._jit_member_blades = blades
|
| 254 |
+
|
| 255 |
+
cls._jit_member_hearts_sum = hearts_sum
|
| 256 |
+
|
| 257 |
+
cls._jit_member_hearts_vec = hearts_vec
|
| 258 |
+
|
| 259 |
+
cls._jit_live_score = live_score
|
| 260 |
+
|
| 261 |
+
cls._jit_live_hearts_sum = live_hearts_sum
|
| 262 |
+
|
| 263 |
+
cls._jit_live_hearts_vec = live_hearts_vec
|
| 264 |
+
|
| 265 |
+
@classmethod
|
| 266 |
+
def serialize_card(cls, cid: int, is_viewable=True, peek=False):
|
| 267 |
+
"""Static helper to serialize a card ID."""
|
| 268 |
+
|
| 269 |
+
if cid < 0:
|
| 270 |
+
return None
|
| 271 |
+
|
| 272 |
+
card_data = {"id": int(cid), "img": "cards/card_back.png", "type": "unknown", "name": "Unknown"}
|
| 273 |
+
|
| 274 |
+
if not is_viewable and not peek:
|
| 275 |
+
return {"id": int(cid), "img": "cards/card_back.png", "type": "unknown", "hidden": True}
|
| 276 |
+
|
| 277 |
+
if cid in cls.member_db:
|
| 278 |
+
m = cls.member_db[cid]
|
| 279 |
+
|
| 280 |
+
# Basic ability text formatting
|
| 281 |
+
|
| 282 |
+
at = getattr(m, "ability_text", "")
|
| 283 |
+
|
| 284 |
+
if not at and hasattr(m, "abilities"):
|
| 285 |
+
at_lines = []
|
| 286 |
+
|
| 287 |
+
for ab in m.abilities:
|
| 288 |
+
at_lines.append(ab.raw_text)
|
| 289 |
+
|
| 290 |
+
at = "\n".join(at_lines)
|
| 291 |
+
|
| 292 |
+
card_data = {
|
| 293 |
+
"id": int(cid),
|
| 294 |
+
"card_no": getattr(m, "card_no", "Unknown"),
|
| 295 |
+
"name": getattr(m, "name", "Unknown Member"),
|
| 296 |
+
"cost": int(getattr(m, "cost", 0)),
|
| 297 |
+
"type": "member",
|
| 298 |
+
"hp": int(m.total_hearts()) if hasattr(m, "total_hearts") else 0,
|
| 299 |
+
"blade": int(getattr(m, "blades", 0)),
|
| 300 |
+
"img": getattr(m, "img_path", "cards/card_back.png"),
|
| 301 |
+
"hearts": m.hearts.tolist() if hasattr(m, "hearts") and hasattr(m.hearts, "tolist") else [0] * 7,
|
| 302 |
+
"blade_hearts": m.blade_hearts.tolist()
|
| 303 |
+
if hasattr(m, "blade_hearts") and hasattr(m.blade_hearts, "tolist")
|
| 304 |
+
else [0] * 7,
|
| 305 |
+
"text": at,
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
elif cid in cls.live_db:
|
| 309 |
+
l = cls.live_db[cid]
|
| 310 |
+
|
| 311 |
+
card_data = {
|
| 312 |
+
"id": int(cid),
|
| 313 |
+
"card_no": getattr(l, "card_no", "Unknown"),
|
| 314 |
+
"name": l.name,
|
| 315 |
+
"type": "live",
|
| 316 |
+
"score": int(l.score),
|
| 317 |
+
"img": l.img_path,
|
| 318 |
+
"required_hearts": l.required_hearts.tolist(),
|
| 319 |
+
"text": getattr(l, "ability_text", ""),
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
elif cid == 888: # Easy member
|
| 323 |
+
card_data = {
|
| 324 |
+
"id": 888,
|
| 325 |
+
"name": "Easy Member",
|
| 326 |
+
"type": "member",
|
| 327 |
+
"cost": 1,
|
| 328 |
+
"hp": 1,
|
| 329 |
+
"blade": 1,
|
| 330 |
+
"img": "cards/PLSD01/PL!-sd1-001-SD.png",
|
| 331 |
+
"hearts": [1, 0, 0, 0, 0, 0, 0],
|
| 332 |
+
"blade_hearts": [0, 0, 0, 0, 0, 0, 0],
|
| 333 |
+
"text": "",
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
elif cid == 999: # Easy live
|
| 337 |
+
card_data = {
|
| 338 |
+
"id": 999,
|
| 339 |
+
"name": "Easy Live",
|
| 340 |
+
"type": "live",
|
| 341 |
+
"score": 1,
|
| 342 |
+
"img": "cards/PLSD01/PL!-pb1-019-SD.png",
|
| 343 |
+
"required_hearts": [0, 0, 0, 0, 0, 0, 1],
|
| 344 |
+
"text": "",
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
if not is_viewable and peek:
|
| 348 |
+
card_data["hidden"] = True
|
| 349 |
+
|
| 350 |
+
card_data["face_down"] = True
|
| 351 |
+
|
| 352 |
+
return card_data
|
| 353 |
+
|
| 354 |
+
__slots__ = (
|
| 355 |
+
"verbose",
|
| 356 |
+
"players",
|
| 357 |
+
"current_player",
|
| 358 |
+
"first_player",
|
| 359 |
+
"phase",
|
| 360 |
+
"turn_number",
|
| 361 |
+
"game_over",
|
| 362 |
+
"winner",
|
| 363 |
+
"performance_results",
|
| 364 |
+
"yell_cards",
|
| 365 |
+
"pending_effects",
|
| 366 |
+
"pending_choices",
|
| 367 |
+
"rule_log",
|
| 368 |
+
"current_resolving_ability",
|
| 369 |
+
"current_resolving_member",
|
| 370 |
+
"current_resolving_member_id",
|
| 371 |
+
"looked_cards",
|
| 372 |
+
"triggered_abilities",
|
| 373 |
+
"state_history",
|
| 374 |
+
"loop_draw",
|
| 375 |
+
"removed_cards",
|
| 376 |
+
"action_count_this_turn",
|
| 377 |
+
"pending_choices_vec",
|
| 378 |
+
"pending_choices_ptr",
|
| 379 |
+
"triggered_abilities_vec",
|
| 380 |
+
"triggered_abilities_ptr",
|
| 381 |
+
"_jit_dummy_array",
|
| 382 |
+
"fast_mode",
|
| 383 |
+
"suppress_logs",
|
| 384 |
+
"enable_loop_detection",
|
| 385 |
+
"_trigger_buffers",
|
| 386 |
+
)
|
| 387 |
+
|
| 388 |
+
def __init__(self, verbose=False, suppress_logs=False, enable_loop_detection=True):
|
| 389 |
+
self.verbose = verbose
|
| 390 |
+
self.suppress_logs = suppress_logs
|
| 391 |
+
self.enable_loop_detection = enable_loop_detection
|
| 392 |
+
|
| 393 |
+
self.players = [PlayerState(0), PlayerState(1)]
|
| 394 |
+
|
| 395 |
+
self.current_player = 0 # Who is acting now
|
| 396 |
+
|
| 397 |
+
self.first_player = 0 # Who goes first this turn
|
| 398 |
+
|
| 399 |
+
self.phase = Phase.ACTIVE
|
| 400 |
+
|
| 401 |
+
self.turn_number: int = 1
|
| 402 |
+
|
| 403 |
+
self.game_over: bool = False
|
| 404 |
+
|
| 405 |
+
self.winner: int = -1 # -1 = ongoing, 0/1 = player won, 2 = draw
|
| 406 |
+
|
| 407 |
+
# Performance Result Tracking (for UI popup)
|
| 408 |
+
|
| 409 |
+
self.performance_results: Dict[int, Any] = {}
|
| 410 |
+
|
| 411 |
+
# For yell phase tracking
|
| 412 |
+
|
| 413 |
+
self.yell_cards: List[int] = [] # Shared Resolution Zone (Rule 4.14)
|
| 414 |
+
|
| 415 |
+
self.pending_effects: List[ResolvingEffect] = [] # Stack of effects to resolve
|
| 416 |
+
|
| 417 |
+
self.pending_activation: Optional[Dict[str, Any]] = None
|
| 418 |
+
|
| 419 |
+
self.pending_choices: List[Tuple[str, Dict[str, Any]]] = [] # (choice_type, params with metadata)
|
| 420 |
+
|
| 421 |
+
self.rule_log: List[str] = [] # Real-time rule application log
|
| 422 |
+
|
| 423 |
+
# Track currently resolving ability for context
|
| 424 |
+
|
| 425 |
+
self.current_resolving_ability: Optional[Ability] = None
|
| 426 |
+
|
| 427 |
+
self.current_resolving_member: Optional[str] = None # Member name
|
| 428 |
+
|
| 429 |
+
self.current_resolving_member_id: int = -1 # Member card ID
|
| 430 |
+
|
| 431 |
+
# Temporary zone for LOOK_DECK
|
| 432 |
+
|
| 433 |
+
self.looked_cards: List[int] = []
|
| 434 |
+
|
| 435 |
+
# Rule 9.7: Automatic Abilities
|
| 436 |
+
|
| 437 |
+
# List of (player_id, Ability, context) waiting to be played
|
| 438 |
+
|
| 439 |
+
self.triggered_abilities: List[Tuple[int, Ability, Dict[str, Any]]] = []
|
| 440 |
+
|
| 441 |
+
# Vectorized triggers/choices for JIT
|
| 442 |
+
self.pending_choices_vec = np.zeros((16, 3), dtype=np.int32)
|
| 443 |
+
self.pending_choices_ptr = 0
|
| 444 |
+
self.triggered_abilities_vec = np.zeros((16, 2), dtype=np.int32)
|
| 445 |
+
self.triggered_abilities_ptr = 0
|
| 446 |
+
self._jit_dummy_array = np.zeros(100, dtype=np.int32)
|
| 447 |
+
self.fast_mode = False
|
| 448 |
+
self._trigger_buffers = [[], []] # Pre-allocated buffers for trigger processing
|
| 449 |
+
|
| 450 |
+
# Static caches (for performance and accessibility)
|
| 451 |
+
|
| 452 |
+
# Should be set from server or data loader
|
| 453 |
+
|
| 454 |
+
# Loop Detection (Rule 12.1)
|
| 455 |
+
|
| 456 |
+
# Using a simple hash of the serialization for history
|
| 457 |
+
|
| 458 |
+
self.state_history: List[int] = []
|
| 459 |
+
|
| 460 |
+
self.loop_draw = False
|
| 461 |
+
|
| 462 |
+
self.removed_cards: List[int] = []
|
| 463 |
+
|
| 464 |
+
self.action_count_this_turn: int = 0
|
| 465 |
+
|
| 466 |
+
def log_rule(self, rule_id: str, description: str):
|
| 467 |
+
"""Append a rule application entry to the log."""
|
| 468 |
+
if self.suppress_logs:
|
| 469 |
+
return
|
| 470 |
+
|
| 471 |
+
# Add Turn and Phase context
|
| 472 |
+
|
| 473 |
+
phase_name = self.phase.name if hasattr(self.phase, "name") else str(self.phase)
|
| 474 |
+
|
| 475 |
+
entry = f"[Turn {self.turn_number}] [{phase_name}] [{rule_id}] {description}"
|
| 476 |
+
|
| 477 |
+
self.rule_log.append(entry)
|
| 478 |
+
|
| 479 |
+
# Also print to stdout for server console debugging
|
| 480 |
+
|
| 481 |
+
if self.verbose:
|
| 482 |
+
print(f"RULE_LOG: {entry}")
|
| 483 |
+
pass
|
| 484 |
+
|
| 485 |
+
def _reset(self) -> None:
|
| 486 |
+
"""Reset state for pool reuse - avoids object allocation."""
|
| 487 |
+
|
| 488 |
+
self.verbose = False
|
| 489 |
+
|
| 490 |
+
# Players get reset by PlayerState._reset or replaced
|
| 491 |
+
|
| 492 |
+
self.current_player = 0
|
| 493 |
+
|
| 494 |
+
self.first_player = 0
|
| 495 |
+
|
| 496 |
+
self.phase = Phase.ACTIVE
|
| 497 |
+
|
| 498 |
+
self.turn_number = 1
|
| 499 |
+
|
| 500 |
+
self.game_over = False
|
| 501 |
+
|
| 502 |
+
self.winner = -1
|
| 503 |
+
|
| 504 |
+
self.performance_results.clear()
|
| 505 |
+
|
| 506 |
+
self.yell_cards.clear()
|
| 507 |
+
|
| 508 |
+
self.pending_effects.clear()
|
| 509 |
+
|
| 510 |
+
self.pending_choices.clear()
|
| 511 |
+
|
| 512 |
+
self.rule_log.clear()
|
| 513 |
+
|
| 514 |
+
self.current_resolving_ability = None
|
| 515 |
+
|
| 516 |
+
self.current_resolving_member = None
|
| 517 |
+
|
| 518 |
+
self.current_resolving_member_id = -1
|
| 519 |
+
|
| 520 |
+
self.looked_cards.clear()
|
| 521 |
+
|
| 522 |
+
self.triggered_abilities.clear()
|
| 523 |
+
|
| 524 |
+
self.pending_choices_vec.fill(0)
|
| 525 |
+
self.pending_choices_ptr = 0
|
| 526 |
+
self.triggered_abilities_vec.fill(0)
|
| 527 |
+
self.triggered_abilities_ptr = 0
|
| 528 |
+
self._trigger_buffers[0].clear()
|
| 529 |
+
self._trigger_buffers[1].clear()
|
| 530 |
+
|
| 531 |
+
self.state_history.clear()
|
| 532 |
+
|
| 533 |
+
self.loop_draw = False
|
| 534 |
+
|
| 535 |
+
def copy(self) -> "GameState":
|
| 536 |
+
"""Copy current game state"""
|
| 537 |
+
|
| 538 |
+
new = GameState()
|
| 539 |
+
|
| 540 |
+
self.copy_to(new)
|
| 541 |
+
|
| 542 |
+
return new
|
| 543 |
+
|
| 544 |
+
def copy_to(self, new: "GameState") -> None:
|
| 545 |
+
"""In-place copy to an existing object to avoid allocation"""
|
| 546 |
+
|
| 547 |
+
new.verbose = self.verbose
|
| 548 |
+
new.suppress_logs = self.suppress_logs
|
| 549 |
+
new.enable_loop_detection = self.enable_loop_detection
|
| 550 |
+
|
| 551 |
+
# Reuse existing PlayerState objects in the pooled GameState
|
| 552 |
+
|
| 553 |
+
for i, p in enumerate(self.players):
|
| 554 |
+
p.copy_to(new.players[i])
|
| 555 |
+
|
| 556 |
+
new.current_player = self.current_player
|
| 557 |
+
|
| 558 |
+
new.first_player = self.first_player
|
| 559 |
+
|
| 560 |
+
new.phase = self.phase
|
| 561 |
+
|
| 562 |
+
new.turn_number = self.turn_number
|
| 563 |
+
|
| 564 |
+
new.game_over = self.game_over
|
| 565 |
+
|
| 566 |
+
new.winner = self.winner
|
| 567 |
+
|
| 568 |
+
new.yell_cards = list(self.yell_cards)
|
| 569 |
+
|
| 570 |
+
# Shallow copy of Effect objects (assumed immutable/shared)
|
| 571 |
+
|
| 572 |
+
new.pending_effects = list(self.pending_effects)
|
| 573 |
+
|
| 574 |
+
# Manual copy of pending_choices: List[Tuple[str, Dict]]
|
| 575 |
+
|
| 576 |
+
new.pending_choices = [(pc[0], pc[1].copy()) for pc in self.pending_choices]
|
| 577 |
+
|
| 578 |
+
new.rule_log = list(self.rule_log)
|
| 579 |
+
|
| 580 |
+
new.current_resolving_ability = self.current_resolving_ability
|
| 581 |
+
|
| 582 |
+
new.current_resolving_member = self.current_resolving_member
|
| 583 |
+
|
| 584 |
+
new.current_resolving_member_id = self.current_resolving_member_id
|
| 585 |
+
|
| 586 |
+
new.looked_cards = list(self.looked_cards)
|
| 587 |
+
|
| 588 |
+
# Manual copy of triggered_abilities: List[Tuple[int, Ability, Dict[str, Any]]]
|
| 589 |
+
|
| 590 |
+
# Tuple is immutable, Ability is shared, Dict needs copy
|
| 591 |
+
|
| 592 |
+
new.triggered_abilities = [(ta[0], ta[1], ta[2].copy()) for ta in self.triggered_abilities]
|
| 593 |
+
|
| 594 |
+
# Copy vectorized state
|
| 595 |
+
np.copyto(new.pending_choices_vec, self.pending_choices_vec)
|
| 596 |
+
new.pending_choices_ptr = self.pending_choices_ptr
|
| 597 |
+
np.copyto(new.triggered_abilities_vec, self.triggered_abilities_vec)
|
| 598 |
+
new.triggered_abilities_ptr = self.triggered_abilities_ptr
|
| 599 |
+
new.fast_mode = self.fast_mode
|
| 600 |
+
new._trigger_buffers = [list(self._trigger_buffers[0]), list(self._trigger_buffers[1])]
|
| 601 |
+
|
| 602 |
+
new.state_history = list(self.state_history)
|
| 603 |
+
|
| 604 |
+
new.loop_draw = self.loop_draw
|
| 605 |
+
|
| 606 |
+
new.loop_draw = self.loop_draw
|
| 607 |
+
|
| 608 |
+
# Optimization: Use shallow copy instead of deepcopy.
|
| 609 |
+
# The engine only performs assignment (replace) or clear() (structure),
|
| 610 |
+
# not in-place mutation of the nested lists.
|
| 611 |
+
new.performance_results = self.performance_results.copy()
|
| 612 |
+
|
| 613 |
+
# Copy deferred activation state (Rule 9.7 logic)
|
| 614 |
+
if hasattr(self, "pending_activation") and self.pending_activation:
|
| 615 |
+
new.pending_activation = self.pending_activation.copy()
|
| 616 |
+
if "context" in new.pending_activation:
|
| 617 |
+
new.pending_activation["context"] = new.pending_activation["context"].copy()
|
| 618 |
+
else:
|
| 619 |
+
new.pending_activation = None
|
| 620 |
+
|
| 621 |
+
def inject_card(self, player_idx: int, card_id: int, zone: str, position: int = -1) -> None:
|
| 622 |
+
"""Inject a card into a specific zone for testing purposes."""
|
| 623 |
+
|
| 624 |
+
if player_idx < 0 or player_idx >= len(self.players):
|
| 625 |
+
raise ValueError("Invalid player index")
|
| 626 |
+
|
| 627 |
+
p = self.players[player_idx]
|
| 628 |
+
|
| 629 |
+
if zone == "hand":
|
| 630 |
+
if position == -1:
|
| 631 |
+
p.hand.append(card_id)
|
| 632 |
+
|
| 633 |
+
else:
|
| 634 |
+
p.hand.insert(position, card_id)
|
| 635 |
+
|
| 636 |
+
elif zone == "energy":
|
| 637 |
+
if position == -1:
|
| 638 |
+
p.energy_zone.append(card_id)
|
| 639 |
+
|
| 640 |
+
else:
|
| 641 |
+
p.energy_zone.insert(position, card_id)
|
| 642 |
+
|
| 643 |
+
elif zone == "live":
|
| 644 |
+
if position == -1:
|
| 645 |
+
p.live_zone.append(card_id)
|
| 646 |
+
|
| 647 |
+
p.live_zone_revealed.append(False)
|
| 648 |
+
|
| 649 |
+
else:
|
| 650 |
+
p.live_zone.insert(position, card_id)
|
| 651 |
+
|
| 652 |
+
p.live_zone_revealed.insert(position, False)
|
| 653 |
+
|
| 654 |
+
elif zone == "stage":
|
| 655 |
+
if position < 0 or position >= 3:
|
| 656 |
+
raise ValueError("Stage position must be 0-2")
|
| 657 |
+
|
| 658 |
+
p.stage[position] = card_id
|
| 659 |
+
|
| 660 |
+
else:
|
| 661 |
+
raise ValueError(f"Invalid zone: {zone}")
|
| 662 |
+
|
| 663 |
+
@property
|
| 664 |
+
def active_player(self) -> PlayerState:
|
| 665 |
+
return self.players[self.current_player]
|
| 666 |
+
|
| 667 |
+
@property
|
| 668 |
+
def inactive_player(self) -> PlayerState:
|
| 669 |
+
return self.players[1 - self.current_player]
|
| 670 |
+
|
| 671 |
+
def is_terminal(self) -> bool:
|
| 672 |
+
"""Check if game has ended"""
|
| 673 |
+
|
| 674 |
+
return self.game_over
|
| 675 |
+
|
| 676 |
+
def get_winner(self) -> int:
|
| 677 |
+
"""Returns winner (0 or 1) or -1 if not terminal, 2 if draw"""
|
| 678 |
+
|
| 679 |
+
return self.winner
|
| 680 |
+
|
| 681 |
+
def check_win_condition(self) -> None:
|
| 682 |
+
"""Check if anyone has won (3+ successful lives)"""
|
| 683 |
+
|
| 684 |
+
p0_lives = len(self.players[0].success_lives)
|
| 685 |
+
|
| 686 |
+
p1_lives = len(self.players[1].success_lives)
|
| 687 |
+
|
| 688 |
+
if p0_lives >= 3 and p1_lives >= 3:
|
| 689 |
+
self.game_over = True
|
| 690 |
+
|
| 691 |
+
if p0_lives > p1_lives:
|
| 692 |
+
self.winner = 0
|
| 693 |
+
|
| 694 |
+
elif p1_lives > p0_lives:
|
| 695 |
+
self.winner = 1
|
| 696 |
+
|
| 697 |
+
else:
|
| 698 |
+
self.winner = 2 # Draw
|
| 699 |
+
|
| 700 |
+
elif p0_lives >= 3:
|
| 701 |
+
# Rule 1.2.1.1: Player 0 wins by 3 successful lives
|
| 702 |
+
self.game_over = True
|
| 703 |
+
self.winner = 0
|
| 704 |
+
if hasattr(self, "log_rule"):
|
| 705 |
+
self.log_rule("Rule 1.2.1.1", "Player 0 wins by 3 successful lives.")
|
| 706 |
+
|
| 707 |
+
elif p1_lives >= 3:
|
| 708 |
+
# Rule 1.2.1.1: Player 1 wins by 3 successful lives
|
| 709 |
+
self.game_over = True
|
| 710 |
+
self.winner = 1
|
| 711 |
+
if hasattr(self, "log_rule"):
|
| 712 |
+
self.log_rule("Rule 1.2.1.1", "Player 1 wins by 3 successful lives.")
|
| 713 |
+
|
| 714 |
+
def _is_card_legal_for_choice(self, card_id: int, params: Dict[str, Any]) -> bool:
|
| 715 |
+
"""Helper to check if a card matches the filter criteria for a choice."""
|
| 716 |
+
if card_id < 0:
|
| 717 |
+
return False
|
| 718 |
+
|
| 719 |
+
# Determine if it's a member or live card
|
| 720 |
+
card = self.member_db.get(card_id) or self.live_db.get(card_id)
|
| 721 |
+
if not card:
|
| 722 |
+
return False
|
| 723 |
+
|
| 724 |
+
# 1. Type filter
|
| 725 |
+
req_type = params.get("filter", params.get("type"))
|
| 726 |
+
if req_type == "member" and card_id not in self.member_db:
|
| 727 |
+
return False
|
| 728 |
+
if req_type == "live" and card_id not in self.live_db:
|
| 729 |
+
return False
|
| 730 |
+
|
| 731 |
+
# 2. Group filter
|
| 732 |
+
group_filter = params.get("group")
|
| 733 |
+
if group_filter:
|
| 734 |
+
target_group = Group.from_japanese_name(group_filter)
|
| 735 |
+
if target_group not in getattr(card, "groups", []):
|
| 736 |
+
# Also check units just in case
|
| 737 |
+
target_unit = Unit.from_japanese_name(group_filter)
|
| 738 |
+
if target_unit not in getattr(card, "units", []):
|
| 739 |
+
return False
|
| 740 |
+
|
| 741 |
+
# 3. Cost filter
|
| 742 |
+
cost_max = params.get("cost_max")
|
| 743 |
+
if cost_max is not None and getattr(card, "cost", 0) > cost_max:
|
| 744 |
+
return False
|
| 745 |
+
|
| 746 |
+
cost_min = params.get("cost_min")
|
| 747 |
+
if cost_min is not None and getattr(card, "cost", 0) < cost_min:
|
| 748 |
+
return False
|
| 749 |
+
|
| 750 |
+
return True
|
| 751 |
+
|
| 752 |
+
def get_legal_actions(self) -> np.ndarray:
|
| 753 |
+
"""
|
| 754 |
+
|
| 755 |
+
Returns a mask of legal actions (Rule 9.5.4:
|
| 756 |
+
|
| 757 |
+
Expanded for Complexity:
|
| 758 |
+
|
| 759 |
+
200-202: Activate ability of member in Area (LEFT, CENTER, RIGHT)
|
| 760 |
+
|
| 761 |
+
300-359: Mulligan toggle
|
| 762 |
+
|
| 763 |
+
400-459: Live Set
|
| 764 |
+
|
| 765 |
+
500-559: Choose card in hand (index 0-59) for effect target
|
| 766 |
+
|
| 767 |
+
560-562: Choose member on stage (Area 0-2) for effect target
|
| 768 |
+
|
| 769 |
+
590-599: Choose pending trigger to resolve
|
| 770 |
+
|
| 771 |
+
"""
|
| 772 |
+
|
| 773 |
+
mask = np.zeros(2000, dtype=bool)
|
| 774 |
+
|
| 775 |
+
if self.game_over:
|
| 776 |
+
return mask
|
| 777 |
+
|
| 778 |
+
# Priority: If there are choices to be made for a pending effect
|
| 779 |
+
|
| 780 |
+
if self.pending_choices:
|
| 781 |
+
choice_type, params = self.pending_choices[0]
|
| 782 |
+
|
| 783 |
+
p_idx = params.get("player_id", self.current_player)
|
| 784 |
+
|
| 785 |
+
p = self.players[p_idx]
|
| 786 |
+
|
| 787 |
+
if choice_type == "TARGET_HAND":
|
| 788 |
+
# Allow skip for optional costs
|
| 789 |
+
|
| 790 |
+
if params.get("is_optional"):
|
| 791 |
+
mask[0] = True
|
| 792 |
+
|
| 793 |
+
found = False
|
| 794 |
+
|
| 795 |
+
if len(p.hand) > 0:
|
| 796 |
+
for i, cid in enumerate(p.hand):
|
| 797 |
+
is_legal = self._is_card_legal_for_choice(cid, params)
|
| 798 |
+
if self.verbose:
|
| 799 |
+
print(f"DEBUG: TARGET_HAND check idx={i} cid={cid} legal={is_legal} params={params}")
|
| 800 |
+
if is_legal:
|
| 801 |
+
mask[500 + i] = True
|
| 802 |
+
|
| 803 |
+
found = True
|
| 804 |
+
|
| 805 |
+
if not found:
|
| 806 |
+
mask[0] = True # No valid cards in hand, allow pass logic (fizzle)
|
| 807 |
+
|
| 808 |
+
elif choice_type == "TARGET_MEMBER" or choice_type == "TARGET_MEMBER_SLOT":
|
| 809 |
+
# 560-562: Selected member on stage
|
| 810 |
+
|
| 811 |
+
found = False
|
| 812 |
+
|
| 813 |
+
for i in range(3):
|
| 814 |
+
if p.stage[i] >= 0 or choice_type == "TARGET_MEMBER_SLOT":
|
| 815 |
+
# Filter: for 'activate', only tapped members are legal
|
| 816 |
+
|
| 817 |
+
if params.get("effect") == "activate" and not p.tapped_members[i]:
|
| 818 |
+
continue
|
| 819 |
+
|
| 820 |
+
# Apply general filters if card exists
|
| 821 |
+
|
| 822 |
+
if p.stage[i] >= 0:
|
| 823 |
+
if not self._is_card_legal_for_choice(p.stage[i], params):
|
| 824 |
+
continue
|
| 825 |
+
|
| 826 |
+
mask[560 + i] = True
|
| 827 |
+
|
| 828 |
+
found = True
|
| 829 |
+
|
| 830 |
+
if not found:
|
| 831 |
+
mask[0] = True # No valid targets on stage, allow pass (fizzle)
|
| 832 |
+
|
| 833 |
+
elif choice_type == "DISCARD_SELECT":
|
| 834 |
+
# 500-559: Select card in hand to discard
|
| 835 |
+
|
| 836 |
+
# Allow skip for optional costs
|
| 837 |
+
|
| 838 |
+
if params.get("is_optional"):
|
| 839 |
+
mask[0] = True
|
| 840 |
+
|
| 841 |
+
found = False
|
| 842 |
+
|
| 843 |
+
if len(p.hand) > 0:
|
| 844 |
+
for i, cid in enumerate(p.hand):
|
| 845 |
+
if self._is_card_legal_for_choice(cid, params):
|
| 846 |
+
mask[500 + i] = True
|
| 847 |
+
|
| 848 |
+
found = True
|
| 849 |
+
|
| 850 |
+
if not found and params.get("is_optional"):
|
| 851 |
+
mask[0] = True # No cards to discard, allow pass
|
| 852 |
+
|
| 853 |
+
elif choice_type == "MODAL" or choice_type == "SELECT_MODE":
|
| 854 |
+
# params['options'] is a list of strings or list of lists
|
| 855 |
+
|
| 856 |
+
options = params.get("options", [])
|
| 857 |
+
|
| 858 |
+
for i in range(len(options)):
|
| 859 |
+
mask[570 + i] = True
|
| 860 |
+
|
| 861 |
+
elif choice_type == "CHOOSE_FORMATION":
|
| 862 |
+
# For now, just a dummy confirm? Or allow re-arranging?
|
| 863 |
+
|
| 864 |
+
# Simplified: Action 0 to confirm current formation
|
| 865 |
+
|
| 866 |
+
mask[0] = True
|
| 867 |
+
|
| 868 |
+
elif choice_type == "COLOR_SELECT":
|
| 869 |
+
# 580: Red, 581: Blue, 582: Green, 583: Yellow, 584: Purple, 585: Pink
|
| 870 |
+
|
| 871 |
+
for i in range(6):
|
| 872 |
+
mask[580 + i] = True
|
| 873 |
+
|
| 874 |
+
elif choice_type == "TARGET_OPPONENT_MEMBER":
|
| 875 |
+
# Opponent Stage 0-2 -> Action 600-602
|
| 876 |
+
|
| 877 |
+
opp = self.inactive_player
|
| 878 |
+
|
| 879 |
+
found = False
|
| 880 |
+
|
| 881 |
+
for i in range(3):
|
| 882 |
+
if opp.stage[i] >= 0:
|
| 883 |
+
mask[600 + i] = True
|
| 884 |
+
|
| 885 |
+
found = True
|
| 886 |
+
|
| 887 |
+
if not found:
|
| 888 |
+
# If no valid targets but choice exists, softlock prevention:
|
| 889 |
+
|
| 890 |
+
# Ideally we should strictly check before pushing choice, but safe fallback:
|
| 891 |
+
|
| 892 |
+
mask[0] = True # Pass/Cancel
|
| 893 |
+
|
| 894 |
+
elif choice_type == "SELECT_FROM_LIST":
|
| 895 |
+
# 600-659: List selection (up to 60 items)
|
| 896 |
+
|
| 897 |
+
cards = params.get("cards", [])
|
| 898 |
+
|
| 899 |
+
card_count = min(len(cards), 60)
|
| 900 |
+
|
| 901 |
+
if card_count > 0:
|
| 902 |
+
mask[600 : 600 + card_count] = True
|
| 903 |
+
|
| 904 |
+
else:
|
| 905 |
+
mask[0] = True # Empty list, allow pass
|
| 906 |
+
|
| 907 |
+
elif choice_type == "SELECT_FROM_DISCARD":
|
| 908 |
+
# 660-719: Discard selection (up to 60 items)
|
| 909 |
+
|
| 910 |
+
cards = params.get("cards", [])
|
| 911 |
+
|
| 912 |
+
card_count = min(len(cards), 60)
|
| 913 |
+
|
| 914 |
+
if card_count > 0:
|
| 915 |
+
mask[660 : 660 + card_count] = True
|
| 916 |
+
|
| 917 |
+
else:
|
| 918 |
+
mask[0] = True # Empty discard, allow pass
|
| 919 |
+
|
| 920 |
+
elif choice_type == "SELECT_FORMATION_SLOT" or choice_type == "SELECT_ORDER":
|
| 921 |
+
# 720-759: Item selection from a list (Formation)
|
| 922 |
+
cards = params.get("cards", params.get("available_members", []))
|
| 923 |
+
card_count = min(len(cards), 40)
|
| 924 |
+
if card_count > 0:
|
| 925 |
+
mask[720 : 720 + card_count] = True
|
| 926 |
+
else:
|
| 927 |
+
mask[0] = True
|
| 928 |
+
|
| 929 |
+
elif choice_type == "SELECT_SWAP_SOURCE":
|
| 930 |
+
# 600-659: Reuse list selection range
|
| 931 |
+
|
| 932 |
+
cards = params.get("cards", [])
|
| 933 |
+
|
| 934 |
+
card_count = min(len(cards), 60)
|
| 935 |
+
|
| 936 |
+
if card_count > 0:
|
| 937 |
+
mask[600 : 600 + card_count] = True
|
| 938 |
+
|
| 939 |
+
else:
|
| 940 |
+
mask[0] = True
|
| 941 |
+
|
| 942 |
+
elif choice_type == "SELECT_SWAP_TARGET":
|
| 943 |
+
# 500-559: Target hand range
|
| 944 |
+
|
| 945 |
+
if len(p.hand) > 0:
|
| 946 |
+
for i in range(len(p.hand)):
|
| 947 |
+
mask[500 + i] = True
|
| 948 |
+
|
| 949 |
+
else:
|
| 950 |
+
mask[0] = True
|
| 951 |
+
|
| 952 |
+
elif choice_type == "SELECT_SUCCESS_LIVE" or choice_type == "TARGET_SUCCESS_LIVES":
|
| 953 |
+
# 760-819: Select from passed lives list (Score)
|
| 954 |
+
cards = params.get("cards", p.success_lives)
|
| 955 |
+
card_count = min(len(cards), 60)
|
| 956 |
+
if card_count > 0:
|
| 957 |
+
mask[760 : 760 + card_count] = True
|
| 958 |
+
else:
|
| 959 |
+
mask[0] = True
|
| 960 |
+
|
| 961 |
+
elif choice_type == "TARGET_LIVE":
|
| 962 |
+
# 820-822: Select specific slot in Live Zone
|
| 963 |
+
for i in range(len(p.live_zone)):
|
| 964 |
+
mask[820 + i] = True
|
| 965 |
+
if not any(mask[820:823]):
|
| 966 |
+
mask[0] = True
|
| 967 |
+
|
| 968 |
+
elif choice_type == "TARGET_ENERGY_ZONE":
|
| 969 |
+
# 830-849: Select specific card in Energy Zone
|
| 970 |
+
for i in range(len(p.energy_zone)):
|
| 971 |
+
if i < 20:
|
| 972 |
+
mask[830 + i] = True
|
| 973 |
+
if not any(mask[830:850]):
|
| 974 |
+
mask[0] = True
|
| 975 |
+
|
| 976 |
+
elif choice_type == "TARGET_REMOVED":
|
| 977 |
+
# 850-909: Select from Removed cards
|
| 978 |
+
count = min(len(self.removed_cards), 60)
|
| 979 |
+
if count > 0:
|
| 980 |
+
mask[850 : 850 + count] = True
|
| 981 |
+
else:
|
| 982 |
+
mask[0] = True
|
| 983 |
+
|
| 984 |
+
elif choice_type == "TARGET_DECK" or choice_type == "TARGET_ENERGY_DECK" or choice_type == "TARGET_DISCARD":
|
| 985 |
+
# List selection ranges
|
| 986 |
+
cards = params.get("cards", [])
|
| 987 |
+
card_count = min(len(cards), 60)
|
| 988 |
+
offset = 600 if choice_type != "TARGET_DISCARD" else 660
|
| 989 |
+
if card_count > 0:
|
| 990 |
+
mask[offset : offset + card_count] = True
|
| 991 |
+
else:
|
| 992 |
+
mask[0] = True
|
| 993 |
+
|
| 994 |
+
# MULLIGAN phases: Select cards to return or confirm mulligan
|
| 995 |
+
|
| 996 |
+
elif self.phase in (Phase.MULLIGAN_P1, Phase.MULLIGAN_P2):
|
| 997 |
+
p = self.active_player
|
| 998 |
+
|
| 999 |
+
mask[0] = True # Confirm mulligan (done selecting)
|
| 1000 |
+
|
| 1001 |
+
# Actions 300-359: Select card for mulligan (card index 0-59)
|
| 1002 |
+
|
| 1003 |
+
# Note: We allow toggling selection.
|
| 1004 |
+
|
| 1005 |
+
m_sel = getattr(p, "mulligan_selection", set())
|
| 1006 |
+
|
| 1007 |
+
for i in range(len(p.hand)):
|
| 1008 |
+
mask[300 + i] = True
|
| 1009 |
+
|
| 1010 |
+
# Auto-advance phases: these phases process automatically in 'step' when any valid action is received
|
| 1011 |
+
|
| 1012 |
+
# We allow Action 0 (Pass) to trigger the transition.
|
| 1013 |
+
|
| 1014 |
+
elif self.phase in (Phase.ACTIVE, Phase.ENERGY):
|
| 1015 |
+
mask[0] = True
|
| 1016 |
+
elif self.phase == Phase.PERFORMANCE_P1 or self.phase == Phase.PERFORMANCE_P2:
|
| 1017 |
+
p = self.active_player
|
| 1018 |
+
mask[0] = True # Always can pass (skip performance)
|
| 1019 |
+
|
| 1020 |
+
# Check all lives in live zone
|
| 1021 |
+
for i, card_id in enumerate(p.live_zone):
|
| 1022 |
+
# Standard Live ID as Action ID
|
| 1023 |
+
if card_id in self.live_db:
|
| 1024 |
+
live_card = self.live_db[card_id]
|
| 1025 |
+
|
| 1026 |
+
# Check requirements
|
| 1027 |
+
reqs = getattr(live_card, "required_hearts", [0] * 7)
|
| 1028 |
+
if len(reqs) < 7:
|
| 1029 |
+
reqs = [0] * 7
|
| 1030 |
+
|
| 1031 |
+
stage_hearts = [0] * 7
|
| 1032 |
+
total_blades = 0
|
| 1033 |
+
|
| 1034 |
+
for slot in range(3):
|
| 1035 |
+
sid = p.stage[slot]
|
| 1036 |
+
if sid in self.member_db:
|
| 1037 |
+
m = self.member_db[sid]
|
| 1038 |
+
total_blades += m.blades
|
| 1039 |
+
|
| 1040 |
+
# Determine color index (1-6) from hearts
|
| 1041 |
+
# Heuristic: Find first non-zero index in hearts array
|
| 1042 |
+
# This mimics vector_env logic
|
| 1043 |
+
col = 0
|
| 1044 |
+
h_arr = m.hearts
|
| 1045 |
+
for cidx, val in enumerate(h_arr):
|
| 1046 |
+
if val > 0:
|
| 1047 |
+
col = cidx + 1
|
| 1048 |
+
break
|
| 1049 |
+
|
| 1050 |
+
if 1 <= col <= 6:
|
| 1051 |
+
stage_hearts[col] += m.hearts[col - 1] # m.hearts is 0-indexed?
|
| 1052 |
+
# Wait, GameState initializes hearts_vec with m.hearts
|
| 1053 |
+
# m.hearts is usually [Pink, Red, ...]
|
| 1054 |
+
# Let's assume m.hearts is standard 7-dim or 6-dim
|
| 1055 |
+
# If m.hearts[0] is Pink (Color 1), then:
|
| 1056 |
+
pass
|
| 1057 |
+
|
| 1058 |
+
# Re-calculating correctly using GameState helper if available,
|
| 1059 |
+
# else manual sum matching VectorEnv
|
| 1060 |
+
|
| 1061 |
+
# Optimized check:
|
| 1062 |
+
# Use existing helper? p.get_effective_hearts?
|
| 1063 |
+
# But that returns vector.
|
| 1064 |
+
|
| 1065 |
+
# Let's use p.stage stats directly
|
| 1066 |
+
current_hearts = [0] * 7
|
| 1067 |
+
current_blades = 0
|
| 1068 |
+
for slot in range(3):
|
| 1069 |
+
if p.stage[slot] != -1:
|
| 1070 |
+
eff_h = p.get_effective_hearts(slot, self.member_db)
|
| 1071 |
+
for c in range(7):
|
| 1072 |
+
current_hearts[c] += eff_h[c]
|
| 1073 |
+
current_blades += p.get_effective_blades(slot, self.member_db)
|
| 1074 |
+
|
| 1075 |
+
# Check against reqs
|
| 1076 |
+
# reqs[0] is usually Any? Or Pink?
|
| 1077 |
+
# In VectorEnv: 12-18 (Pink..Purple, All)
|
| 1078 |
+
# live_card.required_hearts is 0-indexed typically [Pink, Red, Yel, Grn, Blu, Pur, Any]
|
| 1079 |
+
|
| 1080 |
+
met = True
|
| 1081 |
+
# Check colors (0-5)
|
| 1082 |
+
for c in range(6):
|
| 1083 |
+
if current_hearts[c] < reqs[c]:
|
| 1084 |
+
met = False
|
| 1085 |
+
break
|
| 1086 |
+
# Check Any (index 6, matches any color + explicit Any?)
|
| 1087 |
+
# Usually Any req is satisfied by sum of all?
|
| 1088 |
+
# For strictness, let's assume reqs[6] is specific "Any" points needed (wildcard).
|
| 1089 |
+
# VectorEnv logic was:
|
| 1090 |
+
# if stage_hearts[1] < req_pink...
|
| 1091 |
+
|
| 1092 |
+
# Assuming standard behavior:
|
| 1093 |
+
if met and current_blades > 0:
|
| 1094 |
+
mask[900 + i] = True
|
| 1095 |
+
|
| 1096 |
+
elif self.phase == Phase.DRAW or self.phase == Phase.LIVE_RESULT:
|
| 1097 |
+
mask[0] = True
|
| 1098 |
+
|
| 1099 |
+
elif self.phase == Phase.MAIN:
|
| 1100 |
+
p = self.active_player
|
| 1101 |
+
|
| 1102 |
+
# Can always pass
|
| 1103 |
+
|
| 1104 |
+
mask[0] = True
|
| 1105 |
+
|
| 1106 |
+
# --- SHARED PRE-CALCULATIONS ---
|
| 1107 |
+
|
| 1108 |
+
available_energy = p.count_untapped_energy()
|
| 1109 |
+
|
| 1110 |
+
total_reduction = 0
|
| 1111 |
+
|
| 1112 |
+
for ce in p.continuous_effects:
|
| 1113 |
+
if ce["effect"].effect_type == EffectType.REDUCE_COST:
|
| 1114 |
+
total_reduction += ce["effect"].value
|
| 1115 |
+
|
| 1116 |
+
# --- PLAY MEMBERS ---
|
| 1117 |
+
|
| 1118 |
+
if "placement" not in p.restrictions:
|
| 1119 |
+
# JIT Optimization Path
|
| 1120 |
+
|
| 1121 |
+
# JIT Path disabled temporarily for training stability
|
| 1122 |
+
|
| 1123 |
+
if False and JIT_AVAILABLE and self._jit_member_costs is not None:
|
| 1124 |
+
# Use pre-allocated hand buffer to avoid reallocation
|
| 1125 |
+
|
| 1126 |
+
hand_len = len(p.hand)
|
| 1127 |
+
|
| 1128 |
+
if hand_len > 0:
|
| 1129 |
+
p.hand_buffer[:hand_len] = p.hand
|
| 1130 |
+
|
| 1131 |
+
calc_main_phase_masks(
|
| 1132 |
+
p.hand_buffer[:hand_len],
|
| 1133 |
+
p.stage,
|
| 1134 |
+
available_energy,
|
| 1135 |
+
total_reduction,
|
| 1136 |
+
True, # Baton touch is always allowed if slot occupied
|
| 1137 |
+
p.members_played_this_turn,
|
| 1138 |
+
self._jit_member_costs,
|
| 1139 |
+
mask,
|
| 1140 |
+
)
|
| 1141 |
+
|
| 1142 |
+
else:
|
| 1143 |
+
# Python Fallback
|
| 1144 |
+
|
| 1145 |
+
for i, card_id in enumerate(p.hand):
|
| 1146 |
+
if card_id not in self.member_db:
|
| 1147 |
+
continue
|
| 1148 |
+
|
| 1149 |
+
member = self.member_db[card_id]
|
| 1150 |
+
|
| 1151 |
+
for area in range(3):
|
| 1152 |
+
action_id = 1 + i * 3 + area
|
| 1153 |
+
|
| 1154 |
+
if p.members_played_this_turn[area]:
|
| 1155 |
+
continue
|
| 1156 |
+
|
| 1157 |
+
is_baton = p.stage[area] >= 0
|
| 1158 |
+
|
| 1159 |
+
# Calculate effective baton touch limit
|
| 1160 |
+
extra_baton = sum(
|
| 1161 |
+
ce["effect"].value
|
| 1162 |
+
for ce in p.continuous_effects
|
| 1163 |
+
if ce["effect"].effect_type == EffectType.BATON_TOUCH_MOD
|
| 1164 |
+
)
|
| 1165 |
+
effective_baton_limit = p.baton_touch_limit + extra_baton
|
| 1166 |
+
|
| 1167 |
+
if is_baton and p.baton_touch_count >= effective_baton_limit:
|
| 1168 |
+
continue
|
| 1169 |
+
|
| 1170 |
+
# Calculate slot-specific cost
|
| 1171 |
+
slot_reduction = sum(
|
| 1172 |
+
ce["effect"].value
|
| 1173 |
+
for ce in p.continuous_effects
|
| 1174 |
+
if ce["effect"].effect_type == EffectType.REDUCE_COST
|
| 1175 |
+
and (ce.get("target_slot", -1) in (-1, area))
|
| 1176 |
+
)
|
| 1177 |
+
|
| 1178 |
+
active_cost = max(0, member.cost - slot_reduction)
|
| 1179 |
+
|
| 1180 |
+
if is_baton:
|
| 1181 |
+
if p.stage[area] in self.member_db:
|
| 1182 |
+
baton_mem = self.member_db[p.stage[area]]
|
| 1183 |
+
active_cost = max(0, active_cost - baton_mem.cost)
|
| 1184 |
+
|
| 1185 |
+
if active_cost <= available_energy:
|
| 1186 |
+
mask[action_id] = True
|
| 1187 |
+
|
| 1188 |
+
# DEBUG: Trace why specific cards fail
|
| 1189 |
+
|
| 1190 |
+
elif self.verbose and (member.cost >= 10 or card_id == 369):
|
| 1191 |
+
print(
|
| 1192 |
+
f"DEBUG REJECT: Card {card_id} ({getattr(member, 'name', 'Unknown')}) Area {area}: Cost {active_cost} > Energy {available_energy}. Limit {p.baton_touch_limit}, Count {p.baton_touch_count}"
|
| 1193 |
+
)
|
| 1194 |
+
|
| 1195 |
+
# --- ACTIVATE ABILITIES ---
|
| 1196 |
+
|
| 1197 |
+
# Uses same available_energy
|
| 1198 |
+
|
| 1199 |
+
for i, card_id in enumerate(p.stage):
|
| 1200 |
+
if card_id >= 0 and card_id in self.member_db and not p.tapped_members[i]:
|
| 1201 |
+
member = self.member_db[card_id]
|
| 1202 |
+
|
| 1203 |
+
for abi_idx, ab in enumerate(member.abilities):
|
| 1204 |
+
if ab.trigger == TriggerType.ACTIVATED:
|
| 1205 |
+
# Rule 9.7: Check once per turn
|
| 1206 |
+
abi_key = f"{card_id}-{abi_idx}"
|
| 1207 |
+
if ab.is_once_per_turn and abi_key in p.used_abilities:
|
| 1208 |
+
continue
|
| 1209 |
+
|
| 1210 |
+
# Strict verification: Check conditions and costs
|
| 1211 |
+
|
| 1212 |
+
is_legal = True
|
| 1213 |
+
|
| 1214 |
+
if not all(self._check_condition(p, cond, context={"area": i}) for cond in ab.conditions):
|
| 1215 |
+
is_legal = False
|
| 1216 |
+
|
| 1217 |
+
if is_legal and not self._can_pay_costs(p, ab.costs, source_area=i):
|
| 1218 |
+
# print(f"DEBUG: Cost check failed for card {card_id} area {i}. Costs: {ab.costs}")
|
| 1219 |
+
is_legal = False
|
| 1220 |
+
|
| 1221 |
+
if is_legal:
|
| 1222 |
+
mask[200 + i] = True
|
| 1223 |
+
# else:
|
| 1224 |
+
# print(f"DEBUG: Ability {ab.raw_text} illegal for card {card_id} area {i}")
|
| 1225 |
+
|
| 1226 |
+
break # Only one ability activation per member slot
|
| 1227 |
+
|
| 1228 |
+
elif self.phase == Phase.LIVE_SET:
|
| 1229 |
+
p = self.active_player
|
| 1230 |
+
|
| 1231 |
+
mask[0] = True
|
| 1232 |
+
|
| 1233 |
+
# Check live restriction (Rule 8.3.4.1 / Cluster 3)
|
| 1234 |
+
|
| 1235 |
+
if "live" not in p.restrictions and len(p.live_zone) < 3:
|
| 1236 |
+
# Allow ANY card to be set (Rule 8.2.2: "Choose up to 3 cards from your hand")
|
| 1237 |
+
for i, card_id in enumerate(p.hand):
|
| 1238 |
+
mask[400 + i] = True
|
| 1239 |
+
|
| 1240 |
+
else:
|
| 1241 |
+
# Other phases are automatic
|
| 1242 |
+
|
| 1243 |
+
mask[0] = True
|
| 1244 |
+
|
| 1245 |
+
# Safety check: Ensure at least one action is legal to prevent softlocks
|
| 1246 |
+
|
| 1247 |
+
if not np.any(mask):
|
| 1248 |
+
# Force action 0 (Pass) as legal
|
| 1249 |
+
|
| 1250 |
+
mask[0] = True
|
| 1251 |
+
|
| 1252 |
+
# print(f"WARNING: No legal actions found in phase {self.phase.name}, forcing Pass action")
|
| 1253 |
+
|
| 1254 |
+
return mask
|
| 1255 |
+
|
| 1256 |
+
def step(self, action_id: int, check_legality: bool = True, in_place: bool = False) -> "GameState":
|
| 1257 |
+
"""
|
| 1258 |
+
|
| 1259 |
+
Executes one step in the game (Rule 9).
|
| 1260 |
+
|
| 1261 |
+
Args:
|
| 1262 |
+
action_id: The action to execute.
|
| 1263 |
+
check_legality: Whether to verify action legality. Disable for speed if caller guarantees validity.
|
| 1264 |
+
in_place: If True, modifies the state in-place instead of copying. Faster for RL.
|
| 1265 |
+
|
| 1266 |
+
"""
|
| 1267 |
+
self.action_count_this_turn += 1
|
| 1268 |
+
if self.action_count_this_turn > 1000:
|
| 1269 |
+
self.game_over = True
|
| 1270 |
+
self.winner = 2 # Draw due to runaway logic
|
| 1271 |
+
self.log_rule("Safety", "Turn exceeded 1000 actions. Force terminating as Draw.")
|
| 1272 |
+
return self
|
| 1273 |
+
|
| 1274 |
+
if self.game_over:
|
| 1275 |
+
print(f"WARNING: Step called after Game Over (Winner: {self.winner}). Ignoring action {action_id}.")
|
| 1276 |
+
|
| 1277 |
+
return self
|
| 1278 |
+
|
| 1279 |
+
# Strict validation for debugging
|
| 1280 |
+
if check_legality:
|
| 1281 |
+
legal_actions = self.get_legal_actions()
|
| 1282 |
+
|
| 1283 |
+
if not legal_actions[action_id]:
|
| 1284 |
+
# Soft fallback for illegal moves to prevent crashes
|
| 1285 |
+
legal_indices = np.where(legal_actions)[0]
|
| 1286 |
+
|
| 1287 |
+
# print(
|
| 1288 |
+
# f"ILLEGAL MOVE CAUGHT: Action {action_id} in phase {self.phase}. "
|
| 1289 |
+
# f"PendingChoices: {len(self.pending_choices)}. "
|
| 1290 |
+
# f"Fallback to first legal action: {legal_indices[0] if len(legal_indices) > 0 else 'None'}"
|
| 1291 |
+
# )
|
| 1292 |
+
|
| 1293 |
+
if len(legal_indices) > 0:
|
| 1294 |
+
if 0 in legal_indices:
|
| 1295 |
+
action_id = 0
|
| 1296 |
+
else:
|
| 1297 |
+
action_id = int(legal_indices[0])
|
| 1298 |
+
else:
|
| 1299 |
+
self.game_over = True
|
| 1300 |
+
self.winner = -2 # Special code for illegal move failure
|
| 1301 |
+
return self
|
| 1302 |
+
|
| 1303 |
+
if in_place:
|
| 1304 |
+
new_state = self
|
| 1305 |
+
else:
|
| 1306 |
+
new_state = self.copy()
|
| 1307 |
+
|
| 1308 |
+
new_state.log_rule("Rule 9.5", f"Processing action {action_id} in {new_state.phase} phase.")
|
| 1309 |
+
|
| 1310 |
+
# Check rule conditions before acting (Rule 9.5.1 / 10.1.2)
|
| 1311 |
+
# MUST be done on new_state
|
| 1312 |
+
new_state._process_rule_checks()
|
| 1313 |
+
|
| 1314 |
+
# Rule 9.5.4.1: Check timing occurs before play timing
|
| 1315 |
+
new_state._process_rule_checks()
|
| 1316 |
+
|
| 1317 |
+
# Priority: If waiting for a choice (like targeting), handles that action
|
| 1318 |
+
|
| 1319 |
+
if new_state.pending_choices:
|
| 1320 |
+
new_state._handle_choice(action_id)
|
| 1321 |
+
|
| 1322 |
+
# Otherwise, if resolving a complex effect stack
|
| 1323 |
+
|
| 1324 |
+
elif new_state.pending_effects:
|
| 1325 |
+
new_state._resolve_pending_effect(0) # 0 is dummy action for auto-res
|
| 1326 |
+
|
| 1327 |
+
# Normal action execution
|
| 1328 |
+
|
| 1329 |
+
else:
|
| 1330 |
+
new_state._execute_action(action_id)
|
| 1331 |
+
|
| 1332 |
+
# After any action, automatically process non-choice effects
|
| 1333 |
+
|
| 1334 |
+
while new_state.pending_effects and not new_state.pending_choices:
|
| 1335 |
+
new_state._resolve_pending_effect(0) # 0 is dummy action for auto-res
|
| 1336 |
+
|
| 1337 |
+
# Rule 9.5.1: Final check timing after action resolution
|
| 1338 |
+
|
| 1339 |
+
new_state._process_rule_checks()
|
| 1340 |
+
|
| 1341 |
+
# Rule 12.1: Infinite Loop Detection
|
| 1342 |
+
|
| 1343 |
+
# Skip for Mulligan phases and if disabled
|
| 1344 |
+
|
| 1345 |
+
if new_state.enable_loop_detection and new_state.phase not in (Phase.MULLIGAN_P1, Phase.MULLIGAN_P2):
|
| 1346 |
+
try:
|
| 1347 |
+
# Capture key state tuple
|
| 1348 |
+
|
| 1349 |
+
state_tuple = (
|
| 1350 |
+
new_state.phase,
|
| 1351 |
+
new_state.current_player,
|
| 1352 |
+
tuple(sorted(new_state.players[0].hand)),
|
| 1353 |
+
tuple(new_state.players[0].stage),
|
| 1354 |
+
tuple(tuple(x) for x in new_state.players[0].stage_energy),
|
| 1355 |
+
tuple(new_state.players[0].energy_zone),
|
| 1356 |
+
tuple(sorted(new_state.players[1].hand)),
|
| 1357 |
+
tuple(new_state.players[1].stage),
|
| 1358 |
+
tuple(tuple(x) for x in new_state.players[1].stage_energy),
|
| 1359 |
+
tuple(new_state.players[1].energy_zone),
|
| 1360 |
+
tuple(sorted(list(new_state.players[0].used_abilities))),
|
| 1361 |
+
tuple(sorted(list(new_state.players[1].used_abilities))),
|
| 1362 |
+
)
|
| 1363 |
+
|
| 1364 |
+
state_hash = hash(state_tuple)
|
| 1365 |
+
|
| 1366 |
+
new_state.state_history.append(state_hash)
|
| 1367 |
+
|
| 1368 |
+
if new_state.state_history.count(state_hash) >= 20:
|
| 1369 |
+
new_state.log_rule("Rule 12.1", "Infinite Loop detected. Terminating as Draw.")
|
| 1370 |
+
|
| 1371 |
+
new_state.game_over = True
|
| 1372 |
+
|
| 1373 |
+
new_state.winner = 2 # Draw
|
| 1374 |
+
|
| 1375 |
+
new_state.loop_draw = True
|
| 1376 |
+
|
| 1377 |
+
except Exception:
|
| 1378 |
+
# If hashing fails, just ignore for now to prevent crash
|
| 1379 |
+
|
| 1380 |
+
pass
|
| 1381 |
+
|
| 1382 |
+
return new_state
|
| 1383 |
+
|
| 1384 |
+
def get_observation(self) -> np.ndarray:
|
| 1385 |
+
"""
|
| 1386 |
+
Calculates a flat feature vector representing the game state for the AI (Rule 9.1).
|
| 1387 |
+
|
| 1388 |
+
New Layout (Size 320):
|
| 1389 |
+
[0-36]: Metadata (Phase, Player, Choice Context)
|
| 1390 |
+
[36-168]: Hand (12 cards x 11 floats) -> [Exist, ID, Cost, Blade, HeartVec(7)]
|
| 1391 |
+
[168-204]: Self Stage (3 slots x 12 floats) -> [Exist, ID, Tapped, Blade, HeartVec(7), Energy]
|
| 1392 |
+
[204-240]: Opponent Stage (3 slots x 12 floats) -> [Exist, ID, Tapped, Blade, HeartVec(7), Energy]
|
| 1393 |
+
[240-270]: Live Zone (3 cards x 10 floats) -> [Exist, ID, Score, ReqHeartVec(7)]
|
| 1394 |
+
[270-272]: Scores (Self, Opp)
|
| 1395 |
+
[272]: Source ID of pending choice
|
| 1396 |
+
[273-320]: Padding
|
| 1397 |
+
"""
|
| 1398 |
+
|
| 1399 |
+
# Expanded observation size
|
| 1400 |
+
features = np.zeros(320, dtype=np.float32)
|
| 1401 |
+
|
| 1402 |
+
# JIT Arrays Check
|
| 1403 |
+
if GameState._jit_member_costs is None:
|
| 1404 |
+
GameState._init_jit_arrays()
|
| 1405 |
+
|
| 1406 |
+
costs_db = GameState._jit_member_costs
|
| 1407 |
+
hearts_vec_db = GameState._jit_member_hearts_vec
|
| 1408 |
+
blades_db = GameState._jit_member_blades
|
| 1409 |
+
live_score_db = GameState._jit_live_score
|
| 1410 |
+
live_req_vec_db = GameState._jit_live_hearts_vec
|
| 1411 |
+
|
| 1412 |
+
# Max ID for normalization (add safety for 0 div)
|
| 1413 |
+
max_id_val = float(costs_db.shape[0]) if costs_db is not None else 2000.0
|
| 1414 |
+
|
| 1415 |
+
# --- 1. METADATA [0:36] ---
|
| 1416 |
+
|
| 1417 |
+
# Phase (one-hot) [0:16] - using 11 slots
|
| 1418 |
+
phase_val = int(self.phase) + 2
|
| 1419 |
+
if 0 <= phase_val < 11:
|
| 1420 |
+
features[phase_val] = 1.0
|
| 1421 |
+
|
| 1422 |
+
# Current Player [16:18]
|
| 1423 |
+
features[16 + (1 if self.current_player == 1 else 0)] = 1.0
|
| 1424 |
+
|
| 1425 |
+
# Pending Choice [18:36]
|
| 1426 |
+
if self.pending_choices:
|
| 1427 |
+
features[18] = 1.0
|
| 1428 |
+
choice_type, params = self.pending_choices[0]
|
| 1429 |
+
|
| 1430 |
+
# Populate Source ID if available [272]
|
| 1431 |
+
source_id = params.get("card_id", -1)
|
| 1432 |
+
if source_id >= 0:
|
| 1433 |
+
features[272] = source_id / max_id_val
|
| 1434 |
+
|
| 1435 |
+
types = [
|
| 1436 |
+
"TARGET_MEMBER",
|
| 1437 |
+
"TARGET_HAND",
|
| 1438 |
+
"SELECT_MODE",
|
| 1439 |
+
"COLOR_SELECT",
|
| 1440 |
+
"TARGET_OPPONENT_MEMBER",
|
| 1441 |
+
"TARGET_MEMBER_SLOT",
|
| 1442 |
+
"SELECT_SWAP_SOURCE",
|
| 1443 |
+
"SELECT_FROM_LIST",
|
| 1444 |
+
"SELECT_FROM_DISCARD",
|
| 1445 |
+
"DISCARD_SELECT",
|
| 1446 |
+
"MODAL",
|
| 1447 |
+
"CHOOSE_FORMATION",
|
| 1448 |
+
"SELECT_ORDER",
|
| 1449 |
+
"SELECT_FORMATION_SLOT",
|
| 1450 |
+
"SELECT_SUCCESS_LIVE",
|
| 1451 |
+
]
|
| 1452 |
+
try:
|
| 1453 |
+
t_idx = types.index(choice_type)
|
| 1454 |
+
features[19 + t_idx] = 1.0
|
| 1455 |
+
except ValueError:
|
| 1456 |
+
pass
|
| 1457 |
+
|
| 1458 |
+
if params.get("is_optional"):
|
| 1459 |
+
features[35] = 1.0
|
| 1460 |
+
|
| 1461 |
+
# --- 2. HAND [36:168] (12 cards * 11 features) ---
|
| 1462 |
+
p = self.players[self.current_player]
|
| 1463 |
+
hand_len = len(p.hand)
|
| 1464 |
+
n_hand = min(hand_len, 12)
|
| 1465 |
+
|
| 1466 |
+
if n_hand > 0:
|
| 1467 |
+
hand_ids = np.array(p.hand[:n_hand], dtype=int)
|
| 1468 |
+
base_idx = np.arange(n_hand) * 11 + 36
|
| 1469 |
+
|
| 1470 |
+
# Existence
|
| 1471 |
+
features[base_idx] = 1.0
|
| 1472 |
+
|
| 1473 |
+
# ID
|
| 1474 |
+
features[base_idx + 1] = hand_ids / max_id_val
|
| 1475 |
+
|
| 1476 |
+
# Cost
|
| 1477 |
+
c = costs_db[hand_ids]
|
| 1478 |
+
features[base_idx + 2] = np.clip(c, 0, 10) / 10.0
|
| 1479 |
+
|
| 1480 |
+
# Blade
|
| 1481 |
+
b = blades_db[hand_ids]
|
| 1482 |
+
features[base_idx + 3] = np.clip(b, 0, 10) / 10.0
|
| 1483 |
+
|
| 1484 |
+
# Heart Vectors (7 dim)
|
| 1485 |
+
# Flatten 12x7 -> 84? No, interleaved.
|
| 1486 |
+
# We need to assign (N, 7) into sliced positions.
|
| 1487 |
+
# This is tricky with simple slicing if stride is not 1.
|
| 1488 |
+
# Loop for safety or advanced indexing.
|
| 1489 |
+
# shape of h_vecs: (n_hand, 7)
|
| 1490 |
+
h_vecs = hearts_vec_db[hand_ids]
|
| 1491 |
+
|
| 1492 |
+
for i in range(n_hand):
|
| 1493 |
+
start = 36 + i * 11 + 4
|
| 1494 |
+
features[start : start + 7] = np.clip(h_vecs[i], 0, 5) / 5.0
|
| 1495 |
+
|
| 1496 |
+
# --- 3. SELF STAGE [168:204] (3 slots * 12 features) ---
|
| 1497 |
+
for i in range(3):
|
| 1498 |
+
cid = p.stage[i]
|
| 1499 |
+
base = 168 + i * 12
|
| 1500 |
+
if cid >= 0:
|
| 1501 |
+
features[base] = 1.0
|
| 1502 |
+
features[base + 1] = cid / max_id_val
|
| 1503 |
+
features[base + 2] = 1.0 if p.tapped_members[i] else 0.0
|
| 1504 |
+
|
| 1505 |
+
# Effective Stats (retains python logic for modifiers)
|
| 1506 |
+
eff_blade = p.get_effective_blades(i, self.member_db)
|
| 1507 |
+
eff_hearts = p.get_effective_hearts(i, self.member_db) # vector
|
| 1508 |
+
|
| 1509 |
+
features[base + 3] = min(eff_blade / 10.0, 1.0)
|
| 1510 |
+
|
| 1511 |
+
# Hearts (7)
|
| 1512 |
+
# eff_hearts is usually (6,) or (7,) or list
|
| 1513 |
+
if isinstance(eff_hearts, (list, np.ndarray)):
|
| 1514 |
+
h_len = min(len(eff_hearts), 7)
|
| 1515 |
+
features[base + 4 : base + 4 + h_len] = np.array(eff_hearts[:h_len]) / 5.0
|
| 1516 |
+
|
| 1517 |
+
# Energy Count
|
| 1518 |
+
features[base + 11] = min(len(p.stage_energy[i]) / 5.0, 1.0)
|
| 1519 |
+
|
| 1520 |
+
# --- 4. OPPONENT STAGE [204:240] (3 slots * 12 features) ---
|
| 1521 |
+
opp = self.players[1 - self.current_player]
|
| 1522 |
+
for i in range(3):
|
| 1523 |
+
cid = opp.stage[i]
|
| 1524 |
+
base = 204 + i * 12
|
| 1525 |
+
if cid >= 0:
|
| 1526 |
+
features[base] = 1.0
|
| 1527 |
+
features[base + 1] = cid / max_id_val
|
| 1528 |
+
features[base + 2] = 1.0 if opp.tapped_members[i] else 0.0
|
| 1529 |
+
|
| 1530 |
+
# Note: get_effective_blades requires accessing the opponent object relative to the DB
|
| 1531 |
+
# but GameState usually uses p methods.
|
| 1532 |
+
# p.get_effective_blades uses self.stage.
|
| 1533 |
+
# So we call opp.get_effective_blades.
|
| 1534 |
+
eff_blade = opp.get_effective_blades(i, self.member_db)
|
| 1535 |
+
eff_hearts = opp.get_effective_hearts(i, self.member_db)
|
| 1536 |
+
|
| 1537 |
+
features[base + 3] = min(eff_blade / 10.0, 1.0)
|
| 1538 |
+
|
| 1539 |
+
if isinstance(eff_hearts, (list, np.ndarray)):
|
| 1540 |
+
h_len = min(len(eff_hearts), 7)
|
| 1541 |
+
features[base + 4 : base + 4 + h_len] = np.array(eff_hearts[:h_len]) / 5.0
|
| 1542 |
+
|
| 1543 |
+
features[base + 11] = min(len(opp.stage_energy[i]) / 5.0, 1.0)
|
| 1544 |
+
|
| 1545 |
+
# --- 5. LIVE ZONE [240:270] (3 cards * 10 features) ---
|
| 1546 |
+
n_live = min(len(p.live_zone), 3)
|
| 1547 |
+
if n_live > 0:
|
| 1548 |
+
live_ids = np.array(p.live_zone[:n_live], dtype=int)
|
| 1549 |
+
|
| 1550 |
+
for i in range(n_live):
|
| 1551 |
+
cid = live_ids[i]
|
| 1552 |
+
base = 240 + i * 10
|
| 1553 |
+
features[base] = 1.0
|
| 1554 |
+
features[base + 1] = cid / max_id_val
|
| 1555 |
+
features[base + 2] = np.clip(live_score_db[cid], 0, 5) / 5.0
|
| 1556 |
+
|
| 1557 |
+
# Req Heart Vec (7)
|
| 1558 |
+
if live_req_vec_db is not None:
|
| 1559 |
+
features[base + 3 : base + 10] = np.clip(live_req_vec_db[cid], 0, 5) / 5.0
|
| 1560 |
+
|
| 1561 |
+
# --- 6. SCORES [270:272] ---
|
| 1562 |
+
features[270] = min(len(p.success_lives) / 5.0, 1.0)
|
| 1563 |
+
features[271] = min(len(self.players[1 - self.current_player].success_lives) / 5.0, 1.0)
|
| 1564 |
+
|
| 1565 |
+
return features
|
| 1566 |
+
|
| 1567 |
+
def to_dict(self):
|
| 1568 |
+
"""Serialize full game state."""
|
| 1569 |
+
|
| 1570 |
+
return {
|
| 1571 |
+
"turn": self.turn_number,
|
| 1572 |
+
"phase": self.phase,
|
| 1573 |
+
"active_player": self.current_player,
|
| 1574 |
+
"game_over": self.game_over,
|
| 1575 |
+
"winner": self.winner,
|
| 1576 |
+
"players": [p.to_dict(viewer_idx=0) for p in self.players],
|
| 1577 |
+
"legal_actions": [], # Can populate if needed
|
| 1578 |
+
"pending_choice": None,
|
| 1579 |
+
"performance_results": {},
|
| 1580 |
+
"rule_log": list(self.rule_log),
|
| 1581 |
+
}
|
| 1582 |
+
|
| 1583 |
+
def get_reward(self, player_idx: int) -> float:
|
| 1584 |
+
# Get reward for player (1.0 for win, -1.0 for loss, 0.0 for draw)
|
| 1585 |
+
# Illegal move (-2) is treated as a loss (-1.0) for safety in standard RL,
|
| 1586 |
+
# though explicit training usually handles this via masking or separate loss.
|
| 1587 |
+
|
| 1588 |
+
if self.winner == -2:
|
| 1589 |
+
return -100.0 # Illegal move/Technical loss
|
| 1590 |
+
|
| 1591 |
+
if self.winner == player_idx:
|
| 1592 |
+
return 100.0
|
| 1593 |
+
elif self.winner == 1 - player_idx:
|
| 1594 |
+
return -100.0
|
| 1595 |
+
elif self.winner == 2: # Draw
|
| 1596 |
+
return 0.0
|
| 1597 |
+
elif self.winner == -1: # Ongoing
|
| 1598 |
+
# Ongoing heuristic: Pure score difference
|
| 1599 |
+
# Time penalties are now handled by the Gymnasium environment (per turn)
|
| 1600 |
+
my_score = len(self.players[player_idx].success_lives)
|
| 1601 |
+
opp_score = len(self.players[1 - player_idx].success_lives)
|
| 1602 |
+
return float(my_score - opp_score)
|
| 1603 |
+
|
| 1604 |
+
def take_action(self, action_id: int) -> None:
|
| 1605 |
+
"""In-place version of step() for testing and direct manipulation."""
|
| 1606 |
+
|
| 1607 |
+
if self.pending_choices:
|
| 1608 |
+
self._handle_choice(action_id)
|
| 1609 |
+
|
| 1610 |
+
else:
|
| 1611 |
+
self._execute_action(action_id)
|
| 1612 |
+
|
| 1613 |
+
# Process resulting effects
|
| 1614 |
+
|
| 1615 |
+
while self.pending_effects and not self.pending_choices:
|
| 1616 |
+
self._resolve_pending_effect(0)
|
| 1617 |
+
|
| 1618 |
+
|
| 1619 |
+
def create_sample_cards() -> Tuple[Dict[int, MemberCard], Dict[int, LiveCard]]:
|
| 1620 |
+
"""Create sample cards for testing"""
|
| 1621 |
+
|
| 1622 |
+
members = {}
|
| 1623 |
+
|
| 1624 |
+
lives = {}
|
| 1625 |
+
|
| 1626 |
+
# Create 48 sample members with varying stats
|
| 1627 |
+
|
| 1628 |
+
for i in range(48):
|
| 1629 |
+
cost = 2 + (i % 14) # Costs 2-15
|
| 1630 |
+
|
| 1631 |
+
blades = 1 + (i % 6) # Blades 1-6
|
| 1632 |
+
|
| 1633 |
+
hearts = np.zeros(7, dtype=np.int32) # Changed from 6 to 7
|
| 1634 |
+
|
| 1635 |
+
hearts[i % 6] = 1 + (i // 6 % 3) # 1-3 hearts of one color
|
| 1636 |
+
|
| 1637 |
+
if i >= 24:
|
| 1638 |
+
hearts[(i + 1) % 6] = 1 # Second color for higher cost cards
|
| 1639 |
+
|
| 1640 |
+
blade_hearts = np.zeros(6, dtype=np.int32)
|
| 1641 |
+
|
| 1642 |
+
if i % 3 == 0:
|
| 1643 |
+
blade_hearts[i % 6] = 1
|
| 1644 |
+
|
| 1645 |
+
members[i] = MemberCard(
|
| 1646 |
+
card_id=i,
|
| 1647 |
+
card_no=f"SAMPLE-M-{i}",
|
| 1648 |
+
name=f"Member_{i}",
|
| 1649 |
+
cost=cost,
|
| 1650 |
+
hearts=hearts,
|
| 1651 |
+
blade_hearts=blade_hearts,
|
| 1652 |
+
blades=blades,
|
| 1653 |
+
)
|
| 1654 |
+
|
| 1655 |
+
# Create 12 sample live cards
|
| 1656 |
+
|
| 1657 |
+
for i in range(12):
|
| 1658 |
+
score = 1 + (i % 3) # Score 1-3
|
| 1659 |
+
|
| 1660 |
+
required = np.zeros(7, dtype=np.int32)
|
| 1661 |
+
|
| 1662 |
+
required[i % 6] = 2 + (i // 6) # 2-3 of one color required
|
| 1663 |
+
|
| 1664 |
+
required[6] = 1 + (i % 4) # 1-4 "any" hearts required
|
| 1665 |
+
|
| 1666 |
+
lives[100 + i] = LiveCard(
|
| 1667 |
+
card_id=100 + i, card_no=f"SAMPLE-L-{i}", name=f"Live_{i}", score=score, required_hearts=required
|
| 1668 |
+
)
|
| 1669 |
+
|
| 1670 |
+
return members, lives
|
| 1671 |
+
|
| 1672 |
+
|
| 1673 |
+
def initialize_game(use_real_data: bool = True, deck_type: str = "normal") -> GameState:
|
| 1674 |
+
"""
|
| 1675 |
+
|
| 1676 |
+
Create initial game state with shuffled decks.
|
| 1677 |
+
|
| 1678 |
+
Args:
|
| 1679 |
+
|
| 1680 |
+
use_real_data: Whether to try loading real cards.json data
|
| 1681 |
+
|
| 1682 |
+
deck_type: "normal" (random from DB) or "vanilla" (specific simple cards)
|
| 1683 |
+
|
| 1684 |
+
"""
|
| 1685 |
+
|
| 1686 |
+
# Try loading real data
|
| 1687 |
+
|
| 1688 |
+
if use_real_data and not GameState.member_db:
|
| 1689 |
+
import traceback
|
| 1690 |
+
|
| 1691 |
+
# print("DEBUG: initialize_game attempting to load real data...")
|
| 1692 |
+
|
| 1693 |
+
try:
|
| 1694 |
+
# Try current directory first (assuming run from root)
|
| 1695 |
+
|
| 1696 |
+
data_path = os.path.join(os.getcwd(), "data", "cards_compiled.json")
|
| 1697 |
+
|
| 1698 |
+
if not os.path.exists(data_path):
|
| 1699 |
+
# Fallback to cards.json
|
| 1700 |
+
|
| 1701 |
+
data_path = os.path.join(os.getcwd(), "data", "cards.json")
|
| 1702 |
+
|
| 1703 |
+
if not os.path.exists(data_path):
|
| 1704 |
+
# Absolute path fallback based on file location
|
| 1705 |
+
|
| 1706 |
+
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 1707 |
+
|
| 1708 |
+
data_path = os.path.join(base_dir, "data", "cards_compiled.json")
|
| 1709 |
+
|
| 1710 |
+
# print(f"DEBUG: Selected data path: {data_path}")
|
| 1711 |
+
|
| 1712 |
+
if not os.path.exists(data_path):
|
| 1713 |
+
# print(f"ERROR: Data path does not exist: {data_path}")
|
| 1714 |
+
pass
|
| 1715 |
+
|
| 1716 |
+
else:
|
| 1717 |
+
loader = CardDataLoader(data_path)
|
| 1718 |
+
|
| 1719 |
+
m, l, e = loader.load()
|
| 1720 |
+
|
| 1721 |
+
if m:
|
| 1722 |
+
GameState.member_db = m
|
| 1723 |
+
GameState.live_db = l
|
| 1724 |
+
print(f"SUCCESS: Loaded {len(m)} members and {len(l)} lives from {data_path}")
|
| 1725 |
+
|
| 1726 |
+
# Optimization: Cache cards with CONSTANT META_RULE effects
|
| 1727 |
+
GameState._meta_rule_cards = set()
|
| 1728 |
+
for cid, card in m.items():
|
| 1729 |
+
for ab in card.abilities:
|
| 1730 |
+
if ab.trigger.name == "CONSTANT": # Check string to avoid import if needed, or use enum
|
| 1731 |
+
for eff in ab.effects:
|
| 1732 |
+
if eff.effect_type.name == "META_RULE":
|
| 1733 |
+
GameState._meta_rule_cards.add(cid)
|
| 1734 |
+
break
|
| 1735 |
+
for cid, card in l.items():
|
| 1736 |
+
for ab in card.abilities:
|
| 1737 |
+
if ab.trigger.name == "CONSTANT":
|
| 1738 |
+
for eff in ab.effects:
|
| 1739 |
+
if eff.effect_type.name == "META_RULE":
|
| 1740 |
+
GameState._meta_rule_cards.add(cid)
|
| 1741 |
+
break
|
| 1742 |
+
|
| 1743 |
+
GameState._init_jit_arrays()
|
| 1744 |
+
|
| 1745 |
+
else:
|
| 1746 |
+
# print("WARNING: Loader returned empty member database.")
|
| 1747 |
+
pass
|
| 1748 |
+
|
| 1749 |
+
except Exception as e:
|
| 1750 |
+
print(f"CRITICAL: Failed to load real data: {e}")
|
| 1751 |
+
import traceback
|
| 1752 |
+
traceback.print_exc()
|
| 1753 |
+
pass
|
| 1754 |
+
|
| 1755 |
+
traceback.print_exc()
|
| 1756 |
+
|
| 1757 |
+
if not GameState.member_db:
|
| 1758 |
+
# print("WARNING: Falling back to SAMPLE cards. This may cause logic inconsistencies.")
|
| 1759 |
+
|
| 1760 |
+
# Fallback to sample
|
| 1761 |
+
|
| 1762 |
+
members, lives = create_sample_cards()
|
| 1763 |
+
|
| 1764 |
+
GameState.member_db = members
|
| 1765 |
+
|
| 1766 |
+
GameState.live_db = lives
|
| 1767 |
+
|
| 1768 |
+
GameState._init_jit_arrays()
|
| 1769 |
+
|
| 1770 |
+
state = GameState()
|
| 1771 |
+
|
| 1772 |
+
# Pre-calculate Vanilla Deck IDs if needed
|
| 1773 |
+
|
| 1774 |
+
vanilla_member_ids = []
|
| 1775 |
+
|
| 1776 |
+
vanilla_live_ids = []
|
| 1777 |
+
|
| 1778 |
+
if deck_type == "vanilla":
|
| 1779 |
+
# Target Vanilla Members (4 copies each = 48)
|
| 1780 |
+
|
| 1781 |
+
# 5 Vanilla + 7 Simple
|
| 1782 |
+
|
| 1783 |
+
target_members = [
|
| 1784 |
+
"PL!-sd1-010-SD",
|
| 1785 |
+
"PL!-sd1-013-SD",
|
| 1786 |
+
"PL!-sd1-014-SD",
|
| 1787 |
+
"PL!-sd1-017-SD",
|
| 1788 |
+
"PL!-sd1-018-SD", # Vanilla
|
| 1789 |
+
"PL!-sd1-002-SD",
|
| 1790 |
+
"PL!-sd1-005-SD",
|
| 1791 |
+
"PL!-sd1-011-SD",
|
| 1792 |
+
"PL!-sd1-012-SD",
|
| 1793 |
+
"PL!-sd1-016-SD", # Simple
|
| 1794 |
+
"PL!-sd1-015-SD",
|
| 1795 |
+
"PL!-sd1-007-SD",
|
| 1796 |
+
]
|
| 1797 |
+
|
| 1798 |
+
# Target Vanilla Lives (3 copies each = 12)
|
| 1799 |
+
|
| 1800 |
+
target_lives = ["PL!-sd1-019-SD", "PL!-sd1-020-SD", "PL!-sd1-021-SD", "PL!-sd1-022-SD"]
|
| 1801 |
+
|
| 1802 |
+
# 1. Map Members
|
| 1803 |
+
|
| 1804 |
+
found_members = {}
|
| 1805 |
+
|
| 1806 |
+
for cid, card in GameState.member_db.items():
|
| 1807 |
+
if card.card_no in target_members:
|
| 1808 |
+
found_members[card.card_no] = cid
|
| 1809 |
+
|
| 1810 |
+
# 2. Map Lives
|
| 1811 |
+
|
| 1812 |
+
found_lives = {}
|
| 1813 |
+
|
| 1814 |
+
for cid, card in GameState.live_db.items():
|
| 1815 |
+
if card.card_no in target_lives:
|
| 1816 |
+
found_lives[card.card_no] = cid
|
| 1817 |
+
|
| 1818 |
+
# 3. Construct Lists
|
| 1819 |
+
|
| 1820 |
+
for tm in target_members:
|
| 1821 |
+
if tm in found_members:
|
| 1822 |
+
vanilla_member_ids.extend([found_members[tm]] * 4)
|
| 1823 |
+
|
| 1824 |
+
else:
|
| 1825 |
+
# print(f"WARNING: Vanilla card {tm} not found in DB!")
|
| 1826 |
+
pass
|
| 1827 |
+
|
| 1828 |
+
for tl in target_lives:
|
| 1829 |
+
if tl in found_lives:
|
| 1830 |
+
vanilla_live_ids.extend([found_lives[tl]] * 3)
|
| 1831 |
+
|
| 1832 |
+
else:
|
| 1833 |
+
# print(f"WARNING: Vanilla live {tl} not found in DB!")
|
| 1834 |
+
pass
|
| 1835 |
+
|
| 1836 |
+
# Fill if missing?
|
| 1837 |
+
|
| 1838 |
+
if len(vanilla_member_ids) < 48:
|
| 1839 |
+
# print(f"WARNING: Vanilla deck incomplete ({len(vanilla_member_ids)}), filling with randoms.")
|
| 1840 |
+
pass
|
| 1841 |
+
|
| 1842 |
+
remaining = 48 - len(vanilla_member_ids)
|
| 1843 |
+
|
| 1844 |
+
all_ids = list(GameState.member_db.keys())
|
| 1845 |
+
|
| 1846 |
+
if all_ids:
|
| 1847 |
+
vanilla_member_ids.extend(np.random.choice(all_ids, remaining).tolist())
|
| 1848 |
+
|
| 1849 |
+
if len(vanilla_live_ids) < 12:
|
| 1850 |
+
# print(f"WARNING: Vanilla live deck incomplete ({len(vanilla_live_ids)}), filling with randoms.")
|
| 1851 |
+
pass
|
| 1852 |
+
|
| 1853 |
+
remaining = 12 - len(vanilla_live_ids)
|
| 1854 |
+
|
| 1855 |
+
all_ids = list(GameState.live_db.keys())
|
| 1856 |
+
|
| 1857 |
+
if all_ids:
|
| 1858 |
+
vanilla_live_ids.extend(np.random.choice(all_ids, remaining).tolist())
|
| 1859 |
+
|
| 1860 |
+
# Prepare Verified/Random lists if needed
|
| 1861 |
+
verified_member_ids = []
|
| 1862 |
+
verified_live_ids = []
|
| 1863 |
+
|
| 1864 |
+
if deck_type == "random_verified":
|
| 1865 |
+
try:
|
| 1866 |
+
pool_path = os.path.join(os.getcwd(), "verified_card_pool.json")
|
| 1867 |
+
if os.path.exists(pool_path):
|
| 1868 |
+
with open(pool_path, "r", encoding="utf-8") as f:
|
| 1869 |
+
pool = json.load(f)
|
| 1870 |
+
|
| 1871 |
+
v_members = pool.get("verified_abilities", [])
|
| 1872 |
+
v_vanilla = pool.get("vanilla_members", [])
|
| 1873 |
+
total_v_members = v_members + v_vanilla
|
| 1874 |
+
|
| 1875 |
+
# Filter DB for these card_nos
|
| 1876 |
+
for cid, card in GameState.member_db.items():
|
| 1877 |
+
if card.card_no in total_v_members:
|
| 1878 |
+
verified_member_ids.append(cid)
|
| 1879 |
+
|
| 1880 |
+
v_lives = pool.get("vanilla_lives", []) # Or use vanilla_lives as a base for lives
|
| 1881 |
+
for cid, card in GameState.live_db.items():
|
| 1882 |
+
if card.card_no in v_lives:
|
| 1883 |
+
verified_live_ids.append(cid)
|
| 1884 |
+
|
| 1885 |
+
if not verified_member_ids or not verified_live_ids:
|
| 1886 |
+
# print(f"WARNING: Verified pool empty after filtering! Check card_nos. falling back.")
|
| 1887 |
+
pass
|
| 1888 |
+
else:
|
| 1889 |
+
# print(f"WARNING: verified_card_pool.json not found at {pool_path}")
|
| 1890 |
+
pass
|
| 1891 |
+
except Exception:
|
| 1892 |
+
# print(f"ERROR: Failed to load verified pool: {e}")
|
| 1893 |
+
pass
|
| 1894 |
+
|
| 1895 |
+
for p_idx in range(2):
|
| 1896 |
+
p = state.players[p_idx]
|
| 1897 |
+
|
| 1898 |
+
# Build decks
|
| 1899 |
+
if deck_type == "vanilla":
|
| 1900 |
+
member_ids = list(vanilla_member_ids) # Copy
|
| 1901 |
+
live_ids = list(vanilla_live_ids) # Copy
|
| 1902 |
+
elif deck_type == "random_verified" and verified_member_ids and verified_live_ids:
|
| 1903 |
+
# 48 members, 12 lives
|
| 1904 |
+
member_ids = list(np.random.choice(verified_member_ids, 48, replace=True))
|
| 1905 |
+
live_ids = list(np.random.choice(verified_live_ids, 12, replace=True))
|
| 1906 |
+
else:
|
| 1907 |
+
# Random Normal Deck
|
| 1908 |
+
# Random Normal Deck
|
| 1909 |
+
|
| 1910 |
+
member_ids = list(GameState.member_db.keys())
|
| 1911 |
+
|
| 1912 |
+
live_ids = list(GameState.live_db.keys())
|
| 1913 |
+
|
| 1914 |
+
# Filter if too many? For now just take random subset if huge
|
| 1915 |
+
|
| 1916 |
+
if len(member_ids) > 48:
|
| 1917 |
+
member_ids = list(np.random.choice(member_ids, 48, replace=False))
|
| 1918 |
+
|
| 1919 |
+
if len(live_ids) > 12:
|
| 1920 |
+
live_ids = list(np.random.choice(live_ids, 12, replace=False))
|
| 1921 |
+
|
| 1922 |
+
energy_ids = list(range(200, 212))
|
| 1923 |
+
|
| 1924 |
+
np.random.shuffle(member_ids)
|
| 1925 |
+
|
| 1926 |
+
np.random.shuffle(live_ids)
|
| 1927 |
+
|
| 1928 |
+
np.random.shuffle(energy_ids)
|
| 1929 |
+
|
| 1930 |
+
p.main_deck = member_ids + live_ids
|
| 1931 |
+
|
| 1932 |
+
np.random.shuffle(p.main_deck)
|
| 1933 |
+
|
| 1934 |
+
p.energy_deck = energy_ids
|
| 1935 |
+
|
| 1936 |
+
# Initial draw: 6 cards (Rule 6.2.1.5)
|
| 1937 |
+
# Note: log_rule isn't available on GameState yet as it's a static function creating state
|
| 1938 |
+
# but we can print or add a log entry to the state's internal log if it has one.
|
| 1939 |
+
# Actually, let's just make sure the draw happens.
|
| 1940 |
+
for _ in range(6):
|
| 1941 |
+
if p.main_deck:
|
| 1942 |
+
p.hand.append(p.main_deck.pop(0))
|
| 1943 |
+
|
| 1944 |
+
# Log initial setup rules (Rule 6.2.1.5 and 6.2.1.7)
|
| 1945 |
+
state.rule_log.append({"rule": "Rule 6.2.1.5", "description": "Both players draw 6 cards as starting hand."})
|
| 1946 |
+
state.rule_log.append(
|
| 1947 |
+
{"rule": "Rule 6.2.1.7", "description": "Both players place 3 cards from Energy Deck to Energy Zone."}
|
| 1948 |
+
)
|
| 1949 |
+
|
| 1950 |
+
# Set initial phase to Mulligan
|
| 1951 |
+
|
| 1952 |
+
state.phase = Phase.MULLIGAN_P1
|
| 1953 |
+
|
| 1954 |
+
# Randomly determine first player
|
| 1955 |
+
|
| 1956 |
+
state.first_player = np.random.randint(2)
|
| 1957 |
+
|
| 1958 |
+
state.current_player = state.first_player
|
| 1959 |
+
|
| 1960 |
+
# Rule 6.2.1.7: Both players place top 3 cards of Energy Deck into Energy Zone
|
| 1961 |
+
|
| 1962 |
+
for p in state.players:
|
| 1963 |
+
p.energy_zone = []
|
| 1964 |
+
|
| 1965 |
+
for _ in range(3):
|
| 1966 |
+
if p.energy_deck:
|
| 1967 |
+
p.energy_zone.append(p.energy_deck.pop(0))
|
| 1968 |
+
|
| 1969 |
+
return state
|
| 1970 |
+
|
| 1971 |
+
|
| 1972 |
+
if __name__ == "__main__":
|
| 1973 |
+
# Test game creation and basic flow
|
| 1974 |
+
|
| 1975 |
+
game = initialize_game()
|
| 1976 |
+
|
| 1977 |
+
print(f"Game initialized. First player: {game.first_player}")
|
| 1978 |
+
|
| 1979 |
+
print(f"P0 hand: {len(game.players[0].hand)} cards")
|
| 1980 |
+
|
| 1981 |
+
print(f"P1 hand: {len(game.players[1].hand)} cards")
|
| 1982 |
+
|
| 1983 |
+
print(f"Phase: {game.phase.name}")
|
| 1984 |
+
|
| 1985 |
+
# Run a few random actions
|
| 1986 |
+
|
| 1987 |
+
for step in range(20):
|
| 1988 |
+
if game.is_terminal():
|
| 1989 |
+
print(f"Game over! Winner: {game.get_winner()}")
|
| 1990 |
+
|
| 1991 |
+
break
|
| 1992 |
+
|
| 1993 |
+
legal = game.get_legal_actions()
|
| 1994 |
+
|
| 1995 |
+
legal_indices = np.where(legal)[0]
|
| 1996 |
+
|
| 1997 |
+
if len(legal_indices) == 0:
|
| 1998 |
+
print("No legal actions!")
|
| 1999 |
+
|
| 2000 |
+
break
|
| 2001 |
+
|
| 2002 |
+
action = np.random.choice(legal_indices)
|
| 2003 |
+
|
| 2004 |
+
game = game.step(action)
|
| 2005 |
+
|
| 2006 |
+
print(
|
| 2007 |
+
f"Step {step}: Action {action}, Phase {game.phase}, "
|
| 2008 |
+
f"Player {game.current_player}, "
|
| 2009 |
+
f"P0 lives: {len(game.players[0].success_lives)}, "
|
| 2010 |
+
f"P1 lives: {len(game.players[1].success_lives)}"
|
| 2011 |
+
)
|
| 2012 |
+
|
| 2013 |
+
# --- COMPREHENSIVE RULEBOOK INDEX (v1.04) ---
|
| 2014 |
+
|
| 2015 |
+
# This index ensures 100% searchability of all official rule identifiers.
|
| 2016 |
+
|
| 2017 |
+
#
|
| 2018 |
+
|
| 2019 |
+
# Rule 1:
|
| 2020 |
+
|
| 2021 |
+
# Rule 1.1:
|
| 2022 |
+
|
| 2023 |
+
# Rule 1.1.1:
|
| 2024 |
+
|
| 2025 |
+
# Rule 1.2:
|
| 2026 |
+
|
| 2027 |
+
# Rule 1.2.1:
|
| 2028 |
+
|
| 2029 |
+
# Rule 1.2.1.1: ??E??????v???C???[????????C?u?J?[?h?u
|
| 2030 |
+
|
| 2031 |
+
# Rule 1.2.1.2: ??????v???C???[????????3 ????????
|
| 2032 |
+
|
| 2033 |
+
# Rule 1.2.2:
|
| 2034 |
+
|
| 2035 |
+
# Rule 1.2.3:
|
| 2036 |
+
|
| 2037 |
+
# Rule 1.2.3.1: ???E???s???s???A???????J?[?h?E?e????E
|
| 2038 |
+
|
| 2039 |
+
# Rule 1.2.4:
|
| 2040 |
+
|
| 2041 |
+
# Rule 1.3:
|
| 2042 |
+
|
| 2043 |
+
# Rule 1.3.1:
|
| 2044 |
+
|
| 2045 |
+
# Rule 1.3.2:
|
| 2046 |
+
|
| 2047 |
+
# Rule 1.3.2.1: ????????????????E?????????E??
|
| 2048 |
+
|
| 2049 |
+
# Rule 1.3.2.2: ????s???????s??????P?????E??????E
|
| 2050 |
+
|
| 2051 |
+
# Rule 1.3.2.3: ????s????v???????????E?????????A??
|
| 2052 |
+
|
| 2053 |
+
# Rule 1.3.2.4: ?v???C???[??E???[?h????????l?E????A????
|
| 2054 |
+
|
| 2055 |
+
# Rule 1.3.3:
|
| 2056 |
+
|
| 2057 |
+
# Rule 1.3.4:
|
| 2058 |
+
|
| 2059 |
+
# Rule 1.3.4.1: ?????????E????v???C???[??K?p????AE
|
| 2060 |
+
|
| 2061 |
+
# Rule 1.3.4.2: ???E?J????J?[?h?????I???????
|
| 2062 |
+
|
| 2063 |
+
# Rule 1.3.5:
|
| 2064 |
+
|
| 2065 |
+
# Rule 1.3.5.1: ?J?[?h???[??????e?`???f?E??????
|
| 2066 |
+
|
| 2067 |
+
# Rule 2:
|
| 2068 |
+
|
| 2069 |
+
# Rule 2.1:
|
| 2070 |
+
|
| 2071 |
+
# Rule 2.1.1:
|
| 2072 |
+
|
| 2073 |
+
# Rule 2.1.2:
|
| 2074 |
+
|
| 2075 |
+
# Rule 2.1.3:
|
| 2076 |
+
|
| 2077 |
+
# Rule 2.2:
|
| 2078 |
+
|
| 2079 |
+
# Rule 2.2.1:
|
| 2080 |
+
|
| 2081 |
+
# Rule 2.2.2:
|
| 2082 |
+
|
| 2083 |
+
# Rule 2.2.2.1: ?J?[?h?^?C?v?????C?u?????J?[?h?E?A?Q?[??
|
| 2084 |
+
|
| 2085 |
+
# Rule 2.2.2.1.1: ?X?R?A?E?E.10?E????E???n?[?g?IE.11?E???????
|
| 2086 |
+
|
| 2087 |
+
# Rule 2.2.2.2: ?J?[?h?^?C?v???????o?E?????J?[?h?E?A???C
|
| 2088 |
+
|
| 2089 |
+
# Rule 2.2.2.2.1: ?R?X?g?IE.6?E???n?E?g?IE.9?E???????J?[?`E
|
| 2090 |
+
|
| 2091 |
+
# Rule 2.2.2.3: ?J?[?h?^?C?v???G?l???M?[?????J?[?h?E?A??
|
| 2092 |
+
|
| 2093 |
+
# Rule 2.2.2.3.1: ?J?[?h??????e?G?l???M?[?J?[?h?f??\?E
|
| 2094 |
+
|
| 2095 |
+
# Rule 2.3:
|
| 2096 |
+
|
| 2097 |
+
# Rule 2.3.1:
|
| 2098 |
+
|
| 2099 |
+
# Rule 2.3.2:
|
| 2100 |
+
|
| 2101 |
+
# Rule 2.3.2.1: ?J?[?h?????E?E?????????o?E?J?[?h?E?AE??E??
|
| 2102 |
+
|
| 2103 |
+
# Rule 2.3.2.2: ?`E???X?g???A?u?v?i?????????E??????????
|
| 2104 |
+
|
| 2105 |
+
# Rule 2.4:
|
| 2106 |
+
|
| 2107 |
+
# Rule 2.4.1:
|
| 2108 |
+
|
| 2109 |
+
# Rule 2.4.2:
|
| 2110 |
+
|
| 2111 |
+
# Rule 2.4.2.1: ?J?[?h?????E?E?????????o?E?J?[?h?E?AE??E??
|
| 2112 |
+
|
| 2113 |
+
# Rule 2.4.2.2: ?????o?E?????O???[?v????????E?A??
|
| 2114 |
+
|
| 2115 |
+
# Rule 2.4.3:
|
| 2116 |
+
|
| 2117 |
+
# Rule 2.4.3.1: ?`E???X?g???A?w?x?i??d?????????E??????
|
| 2118 |
+
|
| 2119 |
+
# Rule 2.4.4:
|
| 2120 |
+
|
| 2121 |
+
# Rule 2.5:
|
| 2122 |
+
|
| 2123 |
+
# Rule 2.5.1:
|
| 2124 |
+
|
| 2125 |
+
# Rule 2.5.2:
|
| 2126 |
+
|
| 2127 |
+
# Rule 2.5.3:
|
| 2128 |
+
|
| 2129 |
+
# Rule 2.6:
|
| 2130 |
+
|
| 2131 |
+
# Rule 2.6.1:
|
| 2132 |
+
|
| 2133 |
+
# Rule 2.7:
|
| 2134 |
+
|
| 2135 |
+
# Rule 2.7.1:
|
| 2136 |
+
|
| 2137 |
+
# Rule 2.7.2:
|
| 2138 |
+
|
| 2139 |
+
# Rule 2.8:
|
| 2140 |
+
|
| 2141 |
+
# Rule 2.8.1:
|
| 2142 |
+
|
| 2143 |
+
# Rule 2.8.2:
|
| 2144 |
+
|
| 2145 |
+
# Rule 2.9:
|
| 2146 |
+
|
| 2147 |
+
# Rule 2.9.1:
|
| 2148 |
+
|
| 2149 |
+
# Rule 2.9.2:
|
| 2150 |
+
|
| 2151 |
+
# Rule 2.9.3:
|
| 2152 |
+
|
| 2153 |
+
# Rule 2.10:
|
| 2154 |
+
|
| 2155 |
+
# Rule 2.10.1:
|
| 2156 |
+
|
| 2157 |
+
# Rule 2.11:
|
| 2158 |
+
|
| 2159 |
+
# Rule 2.11.1:
|
| 2160 |
+
|
| 2161 |
+
# Rule 2.11.2:
|
| 2162 |
+
|
| 2163 |
+
# Rule 2.11.2.1: ??E???[?g??????A?c??
|
| 2164 |
+
|
| 2165 |
+
# Rule 2.11.2.2: ?n?E?g???????E????????A????S???
|
| 2166 |
+
|
| 2167 |
+
# Rule 2.11.3:
|
| 2168 |
+
|
| 2169 |
+
# Rule 2.12:
|
| 2170 |
+
|
| 2171 |
+
# Rule 2.12.1:
|
| 2172 |
+
|
| 2173 |
+
# Rule 2.12.2:
|
| 2174 |
+
|
| 2175 |
+
# Rule 2.12.3:
|
| 2176 |
+
|
| 2177 |
+
# Rule 2.12.4:
|
| 2178 |
+
|
| 2179 |
+
# Rule 2.13:
|
| 2180 |
+
|
| 2181 |
+
# Rule 2.13.1:
|
| 2182 |
+
|
| 2183 |
+
# Rule 2.13.2:
|
| 2184 |
+
|
| 2185 |
+
# Rule 2.14:
|
| 2186 |
+
|
| 2187 |
+
# Rule 2.14.1:
|
| 2188 |
+
|
| 2189 |
+
# Rule 2.14.2:
|
| 2190 |
+
|
| 2191 |
+
# Rule 2.14.3:
|
| 2192 |
+
|
| 2193 |
+
# Rule 3:
|
| 2194 |
+
|
| 2195 |
+
# Rule 3.1:
|
| 2196 |
+
|
| 2197 |
+
# Rule 3.1.1:
|
| 2198 |
+
|
| 2199 |
+
# Rule 3.1.2:
|
| 2200 |
+
|
| 2201 |
+
# Rule 3.1.2.1: ???E???E?}?X?^?[???A????\???L????E
|
| 2202 |
+
|
| 2203 |
+
# Rule 3.1.2.2: ?N???E???E?}?X?^?[???A??????v???C????
|
| 2204 |
+
|
| 2205 |
+
# Rule 3.1.2.3: ?????E???E?}?X?^?[???A????\???L????E
|
| 2206 |
+
|
| 2207 |
+
# Rule 3.1.2.4: ?????E?}?X?^?[???A??????????????E
|
| 2208 |
+
|
| 2209 |
+
# Rule 3.1.2.4.1: ?????????????v???C???[???w?E
|
| 2210 |
+
|
| 2211 |
+
# Rule 4:
|
| 2212 |
+
|
| 2213 |
+
# Rule 4.1:
|
| 2214 |
+
|
| 2215 |
+
# Rule 4.1.1:
|
| 2216 |
+
|
| 2217 |
+
# Rule 4.1.2:
|
| 2218 |
+
|
| 2219 |
+
# Rule 4.1.2.1: ???J????J?[?h???u???????A????
|
| 2220 |
+
|
| 2221 |
+
# Rule 4.1.2.2: ?????E?J?????????J??????????
|
| 2222 |
+
|
| 2223 |
+
# Rule 4.1.2.3: ???E?J??????????A???????J?[?`E
|
| 2224 |
+
|
| 2225 |
+
# Rule 4.1.3:
|
| 2226 |
+
|
| 2227 |
+
# Rule 4.1.3.1: ??E???????E???????J?[?h?E??E????AE
|
| 2228 |
+
|
| 2229 |
+
# Rule 4.1.4:
|
| 2230 |
+
|
| 2231 |
+
# Rule 4.1.4.1: ????J?[?h????????E??A???????????E
|
| 2232 |
+
|
| 2233 |
+
# Rule 4.1.5:
|
| 2234 |
+
|
| 2235 |
+
# Rule 4.1.5.1: ???J???Y????E?J?????E????J?[?h??
|
| 2236 |
+
|
| 2237 |
+
# Rule 4.1.6:
|
| 2238 |
+
|
| 2239 |
+
# Rule 4.1.7:
|
| 2240 |
+
|
| 2241 |
+
# Rule 4.2:
|
| 2242 |
+
|
| 2243 |
+
# Rule 4.2.1:
|
| 2244 |
+
|
| 2245 |
+
# Rule 4.2.2:
|
| 2246 |
+
|
| 2247 |
+
# Rule 4.2.3:
|
| 2248 |
+
|
| 2249 |
+
# Rule 4.3:
|
| 2250 |
+
|
| 2251 |
+
# Rule 4.3.1:
|
| 2252 |
+
|
| 2253 |
+
# Rule 4.3.2:
|
| 2254 |
+
|
| 2255 |
+
# Rule 4.3.2.1: ?A?N?`E???u????E?J?[?h?E?A????J?[?h?E?}?X
|
| 2256 |
+
|
| 2257 |
+
# Rule 4.3.2.2: ?E?F?C?g????E?J?[?h?E?A????J?[?h?E?}?X
|
| 2258 |
+
|
| 2259 |
+
# Rule 4.3.2.3: ?z?u??????E??????????J?[?h???u??E
|
| 2260 |
+
|
| 2261 |
+
# Rule 4.3.3:
|
| 2262 |
+
|
| 2263 |
+
# Rule 4.3.3.1: ?\????????E?J?[?h?E?A?J?[?h?E?E????????E
|
| 2264 |
+
|
| 2265 |
+
# Rule 4.3.3.2: ??????????E?J?[?h?E?A?J?[?h?E?E????????E
|
| 2266 |
+
|
| 2267 |
+
# Rule 4.4:
|
| 2268 |
+
|
| 2269 |
+
# Rule 4.4.1:
|
| 2270 |
+
|
| 2271 |
+
# Rule 4.4.2:
|
| 2272 |
+
|
| 2273 |
+
# Rule 4.5:
|
| 2274 |
+
|
| 2275 |
+
# Rule 4.5.1:
|
| 2276 |
+
|
| 2277 |
+
# Rule 4.5.1.1: ?`E???X?g????P??e?G???A?f?????????E????
|
| 2278 |
+
|
| 2279 |
+
# Rule 4.5.2:
|
| 2280 |
+
|
| 2281 |
+
# Rule 4.5.2.1: ??E?????o?E?G???A??A??????e???T?C?h?G??
|
| 2282 |
+
|
| 2283 |
+
# Rule 4.5.2.2: ????v???C???[???????A???T?C?h?G???A??
|
| 2284 |
+
|
| 2285 |
+
# Rule 4.5.2.3: ????v???C???[???????A???T?C?h?G???A??
|
| 2286 |
+
|
| 2287 |
+
# Rule 4.5.3:
|
| 2288 |
+
|
| 2289 |
+
# Rule 4.5.4:
|
| 2290 |
+
|
| 2291 |
+
# Rule 4.5.5:
|
| 2292 |
+
|
| 2293 |
+
# Rule 4.5.5.1: ?????o?E?G???A??????o?E?J?[?h?E????d?E
|
| 2294 |
+
|
| 2295 |
+
# Rule 4.5.5.2: ?????o?E?G???A??????o?E?J?[?h?E????d?E
|
| 2296 |
+
|
| 2297 |
+
# Rule 4.5.5.3: ?????o?E?G???A??????o?E?????E?????o?E?G
|
| 2298 |
+
|
| 2299 |
+
# Rule 4.5.5.4: ?????o?E?G???A??????o?E???????o?E?G???A
|
| 2300 |
+
|
| 2301 |
+
# Rule 4.5.6:
|
| 2302 |
+
|
| 2303 |
+
# Rule 4.6:
|
| 2304 |
+
|
| 2305 |
+
# Rule 4.6.1:
|
| 2306 |
+
|
| 2307 |
+
# Rule 4.6.2:
|
| 2308 |
+
|
| 2309 |
+
# Rule 4.7:
|
| 2310 |
+
|
| 2311 |
+
# Rule 4.7.1:
|
| 2312 |
+
|
| 2313 |
+
# Rule 4.7.2:
|
| 2314 |
+
|
| 2315 |
+
# Rule 4.7.3:
|
| 2316 |
+
|
| 2317 |
+
# Rule 4.7.4:
|
| 2318 |
+
|
| 2319 |
+
# Rule 4.8:
|
| 2320 |
+
|
| 2321 |
+
# Rule 4.8.1:
|
| 2322 |
+
|
| 2323 |
+
# Rule 4.8.2:
|
| 2324 |
+
|
| 2325 |
+
# Rule 4.8.3:
|
| 2326 |
+
|
| 2327 |
+
# Rule 4.8.4:
|
| 2328 |
+
|
| 2329 |
+
# Rule 4.9:
|
| 2330 |
+
|
| 2331 |
+
# Rule 4.9.1:
|
| 2332 |
+
|
| 2333 |
+
# Rule 4.9.2:
|
| 2334 |
+
|
| 2335 |
+
# Rule 4.9.3:
|
| 2336 |
+
|
| 2337 |
+
# Rule 4.9.4:
|
| 2338 |
+
|
| 2339 |
+
# Rule 4.10:
|
| 2340 |
+
|
| 2341 |
+
# Rule 4.10.1:
|
| 2342 |
+
|
| 2343 |
+
# Rule 4.10.2:
|
| 2344 |
+
|
| 2345 |
+
# Rule 4.11:
|
| 2346 |
+
|
| 2347 |
+
# Rule 4.11.1:
|
| 2348 |
+
|
| 2349 |
+
# Rule 4.11.2:
|
| 2350 |
+
|
| 2351 |
+
# Rule 4.11.3:
|
| 2352 |
+
|
| 2353 |
+
# Rule 4.12:
|
| 2354 |
+
|
| 2355 |
+
# Rule 4.12.1:
|
| 2356 |
+
|
| 2357 |
+
# Rule 4.12.2:
|
| 2358 |
+
|
| 2359 |
+
# Rule 4.13:
|
| 2360 |
+
|
| 2361 |
+
# Rule 4.13.1:
|
| 2362 |
+
|
| 2363 |
+
# Rule 4.13.2:
|
| 2364 |
+
|
| 2365 |
+
# Rule 4.14:
|
| 2366 |
+
|
| 2367 |
+
# Rule 4.14.1:
|
| 2368 |
+
|
| 2369 |
+
# Rule 4.14.2:
|
| 2370 |
+
|
| 2371 |
+
# Rule 5:
|
| 2372 |
+
|
| 2373 |
+
# Rule 5.1:
|
| 2374 |
+
|
| 2375 |
+
# Rule 5.1.1:
|
| 2376 |
+
|
| 2377 |
+
# Rule 5.2:
|
| 2378 |
+
|
| 2379 |
+
# Rule 5.2.1:
|
| 2380 |
+
|
| 2381 |
+
# Rule 5.3:
|
| 2382 |
+
|
| 2383 |
+
# Rule 5.3.1:
|
| 2384 |
+
|
| 2385 |
+
# Rule 5.4:
|
| 2386 |
+
|
| 2387 |
+
# Rule 5.4.1:
|
| 2388 |
+
|
| 2389 |
+
# Rule 5.5:
|
| 2390 |
+
|
| 2391 |
+
# Rule 5.5.1:
|
| 2392 |
+
|
| 2393 |
+
# Rule 5.5.1.1: ?J?[?h?Q?????P?????????E????????
|
| 2394 |
+
|
| 2395 |
+
# Rule 5.5.1.2: ?J?[?h?Q??J?[?h??0 ??????E1 ???E???E
|
| 2396 |
+
|
| 2397 |
+
# Rule 5.6:
|
| 2398 |
+
|
| 2399 |
+
# Rule 5.6.1:
|
| 2400 |
+
|
| 2401 |
+
# Rule 5.6.2:
|
| 2402 |
+
|
| 2403 |
+
# Rule 5.6.3:
|
| 2404 |
+
|
| 2405 |
+
# Rule 5.6.3.1: ?E????l?E???0 ??????????E?A?????E????E
|
| 2406 |
+
|
| 2407 |
+
# Rule 5.6.3.2: ??E???E???C???[????E??E?????I?E???????E
|
| 2408 |
+
|
| 2409 |
+
# Rule 5.6.3.3: ??E???E???C???[??J?[?h??1 ??????????AE
|
| 2410 |
+
|
| 2411 |
+
# Rule 5.6.3.4: ???E??E??????5.6.3.3 ?????s????????i??
|
| 2412 |
+
|
| 2413 |
+
# Rule 5.7:
|
| 2414 |
+
|
| 2415 |
+
# Rule 5.7.1:
|
| 2416 |
+
|
| 2417 |
+
# Rule 5.7.2:
|
| 2418 |
+
|
| 2419 |
+
# Rule 5.7.2.1: ?E????l?E???0 ??????????E?A?????E????E
|
| 2420 |
+
|
| 2421 |
+
# Rule 5.7.2.2: ?????????1 ???w??????AE
|
| 2422 |
+
|
| 2423 |
+
# Rule 5.7.2.3: ??E???E???C???[????E??E?????I?E???????E
|
| 2424 |
+
|
| 2425 |
+
# Rule 5.7.2.4: ??E???E???C???[??A???C???`E???L?u??????
|
| 2426 |
+
|
| 2427 |
+
# Rule 5.7.2.5: ???E??E??????5.7.2.4 ?????s????????i??
|
| 2428 |
+
|
| 2429 |
+
# Rule 5.8:
|
| 2430 |
+
|
| 2431 |
+
# Rule 5.8.1:
|
| 2432 |
+
|
| 2433 |
+
# Rule 5.8.2:
|
| 2434 |
+
|
| 2435 |
+
# Rule 5.9.1:
|
| 2436 |
+
|
| 2437 |
+
# Rule 5.9.1.1: ?E
|
| 2438 |
+
|
| 2439 |
+
# Rule 5.10:
|
| 2440 |
+
|
| 2441 |
+
# Rule 5.10.1:
|
| 2442 |
+
|
| 2443 |
+
# Rule 6:
|
| 2444 |
+
|
| 2445 |
+
# Rule 6.1:
|
| 2446 |
+
|
| 2447 |
+
# Rule 6.1.1:
|
| 2448 |
+
|
| 2449 |
+
# Rule 6.1.1.1: ???C???`E???L??A?????o?E?J?[?`E8 ??????E????
|
| 2450 |
+
|
| 2451 |
+
# Rule 6.1.1.2: ???C???`E???L???A?J?[?h?i???o?E???????
|
| 2452 |
+
|
| 2453 |
+
# Rule 6.1.1.3: ?G?l???M?[?`E???L??A?G?l???M?[?J?[?`E2
|
| 2454 |
+
|
| 2455 |
+
# Rule 6.1.2:
|
| 2456 |
+
|
| 2457 |
+
# Rule 6.2:
|
| 2458 |
+
|
| 2459 |
+
# Rule 6.2.1:
|
| 2460 |
+
|
| 2461 |
+
# Rule 6.2.1.1: ???E?Q?[????g?p?????g??`E???L???
|
| 2462 |
+
|
| 2463 |
+
# Rule 6.2.1.2: ??E?E???C???[????g????C???`E???L???E?g??
|
| 2464 |
+
|
| 2465 |
+
# Rule 6.2.1.3: ??E?E???C???[????g??G?l???M?[?`E???L??E
|
| 2466 |
+
|
| 2467 |
+
# Rule 6.2.1.4: ??E?E???C???[????????????E?v???C???[
|
| 2468 |
+
|
| 2469 |
+
# Rule 6.2.1.5: ??E?E???C???[????g????C???`E???L?u?????
|
| 2470 |
+
|
| 2471 |
+
# Rule 6.2.1.6: ??U?v???C???[?????E???A?e?v???C???[???
|
| 2472 |
+
|
| 2473 |
+
# Rule 6.2.1.7: ??E?E???C???[????g??G?l???M?[?`E???L?u
|
| 2474 |
+
|
| 2475 |
+
# Rule 7:
|
| 2476 |
+
|
| 2477 |
+
# Rule 7.1:
|
| 2478 |
+
|
| 2479 |
+
# Rule 7.1.1:
|
| 2480 |
+
|
| 2481 |
+
# Rule 7.1.2:
|
| 2482 |
+
|
| 2483 |
+
# Rule 7.2:
|
| 2484 |
+
|
| 2485 |
+
# Rule 7.2.1:
|
| 2486 |
+
|
| 2487 |
+
# Rule 7.2.1.1: ???v???C???[???w????t?F?C?Y????A??
|
| 2488 |
+
|
| 2489 |
+
# Rule 7.2.1.2: ???v???C???[???w?????E???F?C?Y????AE
|
| 2490 |
+
|
| 2491 |
+
# Rule 7.2.2:
|
| 2492 |
+
|
| 2493 |
+
# Rule 7.3:
|
| 2494 |
+
|
| 2495 |
+
# Rule 7.3.1:
|
| 2496 |
+
|
| 2497 |
+
# Rule 7.3.2:
|
| 2498 |
+
|
| 2499 |
+
# Rule 7.3.2.1: ???t?F?C?Y???A?E?U?v???C???[?????`E
|
| 2500 |
+
|
| 2501 |
+
# Rule 7.3.3:
|
| 2502 |
+
|
| 2503 |
+
# Rule 7.4:
|
| 2504 |
+
|
| 2505 |
+
# Rule 7.4.1:
|
| 2506 |
+
|
| 2507 |
+
# Rule 7.4.2:
|
| 2508 |
+
|
| 2509 |
+
# Rule 7.4.3:
|
| 2510 |
+
|
| 2511 |
+
# Rule 7.5:
|
| 2512 |
+
|
| 2513 |
+
# Rule 7.5.1:
|
| 2514 |
+
|
| 2515 |
+
# Rule 7.5.2:
|
| 2516 |
+
|
| 2517 |
+
# Rule 7.5.3:
|
| 2518 |
+
|
| 2519 |
+
# Rule 7.6:
|
| 2520 |
+
|
| 2521 |
+
# Rule 7.6.1:
|
| 2522 |
+
|
| 2523 |
+
# Rule 7.6.2:
|
| 2524 |
+
|
| 2525 |
+
# Rule 7.6.3:
|
| 2526 |
+
|
| 2527 |
+
# Rule 7.7:
|
| 2528 |
+
|
| 2529 |
+
# Rule 7.7.1:
|
| 2530 |
+
|
| 2531 |
+
# Rule 7.7.2:
|
| 2532 |
+
|
| 2533 |
+
# Rule 7.7.2.1: ???E?E?J?[?h??????N???E???1 ??I???AE
|
| 2534 |
+
|
| 2535 |
+
# Rule 7.7.2.2: ???E?E??D??????o?E?J?[?h??1 ???I???A??
|
| 2536 |
+
|
| 2537 |
+
# Rule 7.7.3:
|
| 2538 |
+
|
| 2539 |
+
# Rule 7.8:
|
| 2540 |
+
|
| 2541 |
+
# Rule 7.8.1:
|
| 2542 |
+
|
| 2543 |
+
# Rule 8:
|
| 2544 |
+
|
| 2545 |
+
# Rule 8.1:
|
| 2546 |
+
|
| 2547 |
+
# Rule 8.1.1:
|
| 2548 |
+
|
| 2549 |
+
# Rule 8.1.2:
|
| 2550 |
+
|
| 2551 |
+
# Rule 8.2:
|
| 2552 |
+
|
| 2553 |
+
# Rule 8.2.1:
|
| 2554 |
+
|
| 2555 |
+
# Rule 8.2.2:
|
| 2556 |
+
|
| 2557 |
+
# Rule 8.2.3:
|
| 2558 |
+
|
| 2559 |
+
# Rule 8.2.4:
|
| 2560 |
+
|
| 2561 |
+
# Rule 8.2.5:
|
| 2562 |
+
|
| 2563 |
+
# Rule 8.3:
|
| 2564 |
+
|
| 2565 |
+
# Rule 8.3.1:
|
| 2566 |
+
|
| 2567 |
+
# Rule 8.3.2:
|
| 2568 |
+
|
| 2569 |
+
# Rule 8.3.2.1: ?p?t?H?[?}???X?t?F?C?Y???A?E?U?v???C
|
| 2570 |
+
|
| 2571 |
+
# Rule 8.3.3:
|
| 2572 |
+
|
| 2573 |
+
# Rule 8.3.4:
|
| 2574 |
+
|
| 2575 |
+
# Rule 8.3.4.1: ???v???C???[???e???C?u??????E???????E
|
| 2576 |
+
|
| 2577 |
+
# Rule 8.3.5:
|
| 2578 |
+
|
| 2579 |
+
# Rule 8.3.6:
|
| 2580 |
+
|
| 2581 |
+
# Rule 8.3.7:
|
| 2582 |
+
|
| 2583 |
+
# Rule 8.3.8:
|
| 2584 |
+
|
| 2585 |
+
# Rule 8.3.9:
|
| 2586 |
+
|
| 2587 |
+
# Rule 8.3.10:
|
| 2588 |
+
|
| 2589 |
+
# Rule 8.3.11:
|
| 2590 |
+
|
| 2591 |
+
# Rule 8.3.12:
|
| 2592 |
+
|
| 2593 |
+
# Rule 8.3.13:
|
| 2594 |
+
|
| 2595 |
+
# Rule 8.3.14:
|
| 2596 |
+
|
| 2597 |
+
# Rule 8.3.15:
|
| 2598 |
+
|
| 2599 |
+
# Rule 8.3.15.1: ???????C?u???L?n?[?g????A??????C?`E
|
| 2600 |
+
|
| 2601 |
+
# Rule 8.3.15.1.1: ???E??A?e
|
| 2602 |
+
|
| 2603 |
+
# Rule 8.3.15.1.2: ????????E???C?u?J?[?h?E?E??E
|
| 2604 |
+
|
| 2605 |
+
# Rule 8.3.16:
|
| 2606 |
+
|
| 2607 |
+
# Rule 8.3.17:
|
| 2608 |
+
|
| 2609 |
+
# Rule 8.4:
|
| 2610 |
+
|
| 2611 |
+
# Rule 8.4.1:
|
| 2612 |
+
|
| 2613 |
+
# Rule 8.4.2:
|
| 2614 |
+
|
| 2615 |
+
# Rule 8.4.2.1: ???E??A?e?v???C???[????g??G?[????
|
| 2616 |
+
|
| 2617 |
+
# Rule 8.4.3:
|
| 2618 |
+
|
| 2619 |
+
# Rule 8.4.3.1: ??????v???C???[???????E???C?u?J?[?h?u
|
| 2620 |
+
|
| 2621 |
+
# Rule 8.4.3.2: ?????v???C???[????C?u?J?[?h?u?????
|
| 2622 |
+
|
| 2623 |
+
# Rule 8.4.3.3: ??????v???C???[????C?u?J?[?h?u?????
|
| 2624 |
+
|
| 2625 |
+
# Rule 8.4.4:
|
| 2626 |
+
|
| 2627 |
+
# Rule 8.4.5:
|
| 2628 |
+
|
| 2629 |
+
# Rule 8.4.6:
|
| 2630 |
+
|
| 2631 |
+
# Rule 8.4.6.1: ??????v???C???[???????E???C?u?J?[?h?u
|
| 2632 |
+
|
| 2633 |
+
# Rule 8.4.6.2: ??E??????v???C???[????C?u?J?[?h?u????
|
| 2634 |
+
|
| 2635 |
+
# Rule 8.4.7:
|
| 2636 |
+
|
| 2637 |
+
# Rule 8.4.7.1: ??????v???C???[???????????E?????E
|
| 2638 |
+
|
| 2639 |
+
# Rule 8.4.8:
|
| 2640 |
+
|
| 2641 |
+
# Rule 8.4.9:
|
| 2642 |
+
|
| 2643 |
+
# Rule 8.4.10:
|
| 2644 |
+
|
| 2645 |
+
# Rule 8.4.11:
|
| 2646 |
+
|
| 2647 |
+
# Rule 8.4.12:
|
| 2648 |
+
|
| 2649 |
+
# Rule 8.4.13: 8.4.7 ???????A?????v???C???[?????E?????C
|
| 2650 |
+
|
| 2651 |
+
# Rule 8.4.14:
|
| 2652 |
+
|
| 2653 |
+
# Rule 9:
|
| 2654 |
+
|
| 2655 |
+
# Rule 9.1:
|
| 2656 |
+
|
| 2657 |
+
# Rule 9.1.1:
|
| 2658 |
+
|
| 2659 |
+
# Rule 9.1.1.1: ?N???E????A?E???C?^?C?~???O???^?????
|
| 2660 |
+
|
| 2661 |
+
# Rule 9.1.1.1.1: ?N???E???E?A?J?[?h?????E
|
| 2662 |
+
|
| 2663 |
+
# Rule 9.1.1.2: ?????E????A????\?????????????E
|
| 2664 |
+
|
| 2665 |
+
# Rule 9.1.1.2.1: ?????E???E?A?J?[?h?????E
|
| 2666 |
+
|
| 2667 |
+
# Rule 9.1.1.3: ???E????A????\????L???????A??
|
| 2668 |
+
|
| 2669 |
+
# Rule 9.1.1.3.1: ???E???E?A?J?[?h?????E
|
| 2670 |
+
|
| 2671 |
+
# Rule 9.2:
|
| 2672 |
+
|
| 2673 |
+
# Rule 9.2.1:
|
| 2674 |
+
|
| 2675 |
+
# Rule 9.2.1.1: ?e?P??????f???A??????????E??E???????E
|
| 2676 |
+
|
| 2677 |
+
# Rule 9.2.1.2: ?e?p??????f???A????E???????i?????
|
| 2678 |
+
|
| 2679 |
+
# Rule 9.2.1.3: ?e?u??????f???A?Q?[???????????????
|
| 2680 |
+
|
| 2681 |
+
# Rule 9.2.1.3.1: ?\???e?i?s??A?E??????A???????E??E
|
| 2682 |
+
|
| 2683 |
+
# Rule 9.2.1.3.2: ?\???e?i?s??A?E??????A??????[?I
|
| 2684 |
+
|
| 2685 |
+
# Rule 9.3:
|
| 2686 |
+
|
| 2687 |
+
# Rule 9.3.1:
|
| 2688 |
+
|
| 2689 |
+
# Rule 9.3.2:
|
| 2690 |
+
|
| 2691 |
+
# Rule 9.3.3:
|
| 2692 |
+
|
| 2693 |
+
# Rule 9.3.4:
|
| 2694 |
+
|
| 2695 |
+
# Rule 9.3.4.1: ?????E????E?????E????v???C????E
|
| 2696 |
+
|
| 2697 |
+
# Rule 9.3.4.1.1: ????J?[?h?E?v???C?????E?J?[?h???
|
| 2698 |
+
|
| 2699 |
+
# Rule 9.3.4.2: ?J?[?h?^?C?v???????o?E?????J?[?h?E?\?E
|
| 2700 |
+
|
| 2701 |
+
# Rule 9.3.4.3: ?J?[?h?^?C?v?????C?u?????J?[?h?E?\???E?AE
|
| 2702 |
+
|
| 2703 |
+
# Rule 9.4:
|
| 2704 |
+
|
| 2705 |
+
# Rule 9.4.1:
|
| 2706 |
+
|
| 2707 |
+
# Rule 9.4.2:
|
| 2708 |
+
|
| 2709 |
+
# Rule 9.4.2.1: ?R?X?g???E????s??????????A?e?L?X?g?E
|
| 2710 |
+
|
| 2711 |
+
# Rule 9.4.2.2: ?R?X?g?E??E????????E?S?????x?????????E
|
| 2712 |
+
|
| 2713 |
+
# Rule 9.4.3:
|
| 2714 |
+
|
| 2715 |
+
# Rule 9.5:
|
| 2716 |
+
|
| 2717 |
+
# Rule 9.5.1:
|
| 2718 |
+
|
| 2719 |
+
# Rule 9.5.1.1: ?`?F?`E???^?C?~???O????????A??????[????
|
| 2720 |
+
|
| 2721 |
+
# Rule 9.5.2:
|
| 2722 |
+
|
| 2723 |
+
# Rule 9.5.3:
|
| 2724 |
+
|
| 2725 |
+
# Rule 9.5.3.1: ??????E???s????????[?????E??????
|
| 2726 |
+
|
| 2727 |
+
# Rule 9.5.3.2: ?v???C???[???E?X?^?[?????E???????
|
| 2728 |
+
|
| 2729 |
+
# Rule 9.5.3.3: ??A?N?`E???u?E???C???[???E?X?^?[?????E
|
| 2730 |
+
|
| 2731 |
+
# Rule 9.5.3.4: ?`?F?`E???^?C?~???O???I?E??????AE
|
| 2732 |
+
|
| 2733 |
+
# Rule 9.5.4:
|
| 2734 |
+
|
| 2735 |
+
# Rule 9.5.4.1: ?`?F?`E???^?C?~???O????????????B?`?F?`E???^?C
|
| 2736 |
+
|
| 2737 |
+
# Rule 9.5.4.2: ?v???C?^?C?~???O?????????E?v???C???[??
|
| 2738 |
+
|
| 2739 |
+
# Rule 9.5.4.3: ?v???C?^?C?~???O??^??????E???C???[??E
|
| 2740 |
+
|
| 2741 |
+
# Rule 9.6:
|
| 2742 |
+
|
| 2743 |
+
# Rule 9.6.1:
|
| 2744 |
+
|
| 2745 |
+
# Rule 9.6.2:
|
| 2746 |
+
|
| 2747 |
+
# Rule 9.6.2.1: ?v???C????\????D??J?[?h????E??????
|
| 2748 |
+
|
| 2749 |
+
# Rule 9.6.2.1.1: ?v???C???????J?[?h???????A????E
|
| 2750 |
+
|
| 2751 |
+
# Rule 9.6.2.1.2: ????E???s??????AE
|
| 2752 |
+
|
| 2753 |
+
# Rule 9.6.2.1.2.1: ???E??A????^?[????X?`E?E?W??
|
| 2754 |
+
|
| 2755 |
+
# Rule 9.6.2.1.3: ?v???C???????E????????A????
|
| 2756 |
+
|
| 2757 |
+
# Rule 9.6.2.2: ?J?[?h??\???????E?I?????E???????
|
| 2758 |
+
|
| 2759 |
+
# Rule 9.6.2.3: ?v???C???????R?X?g????????A????R
|
| 2760 |
+
|
| 2761 |
+
# Rule 9.6.2.3.1: ?v???C???????????o?E??J?[?h?????
|
| 2762 |
+
|
| 2763 |
+
# Rule 9.6.2.3.2: ?????o?E???E???C?????A?x???????E
|
| 2764 |
+
|
| 2765 |
+
# Rule 9.6.2.3.2.1: ???????R?X?g???????E?E??
|
| 2766 |
+
|
| 2767 |
+
# Rule 9.6.2.4: ?J?[?h??\???E???????s??????AE
|
| 2768 |
+
|
| 2769 |
+
# Rule 9.6.2.4.1: ?v???C????????????o?E???????A??
|
| 2770 |
+
|
| 2771 |
+
# Rule 9.6.2.4.2: ?v???C????????N???E??????E???
|
| 2772 |
+
|
| 2773 |
+
# Rule 9.6.2.4.2.1: ?\???E??????????????o?E?J?[
|
| 2774 |
+
|
| 2775 |
+
# Rule 9.6.3:
|
| 2776 |
+
|
| 2777 |
+
# Rule 9.6.3.1: ?I??????w??????E?????A??????\
|
| 2778 |
+
|
| 2779 |
+
# Rule 9.6.3.1.1: ?I??????f?`???I???f??f?`???I
|
| 2780 |
+
|
| 2781 |
+
# Rule 9.6.3.1.2: ?I??????w??????E??????A?w?E
|
| 2782 |
+
|
| 2783 |
+
# Rule 9.6.3.1.3: ?I??????w??????E??????A???E
|
| 2784 |
+
|
| 2785 |
+
# Rule 9.6.3.1.4: ?I????E???E?J??????E????E?????J??E
|
| 2786 |
+
|
| 2787 |
+
# Rule 9.7:
|
| 2788 |
+
|
| 2789 |
+
# Rule 9.7.1:
|
| 2790 |
+
|
| 2791 |
+
# Rule 9.7.2:
|
| 2792 |
+
|
| 2793 |
+
# Rule 9.7.2.1: ?????E???E?U?????????E?????????
|
| 2794 |
+
|
| 2795 |
+
# Rule 9.7.3:
|
| 2796 |
+
|
| 2797 |
+
# Rule 9.7.3.1: ??E??????E?????E???E?v???C???????A?E
|
| 2798 |
+
|
| 2799 |
+
# Rule 9.7.3.1.1: ?????E????C???R?X?g???x?????????
|
| 2800 |
+
|
| 2801 |
+
# Rule 9.7.3.2: ?I???E??????E?????E????v???C?????
|
| 2802 |
+
|
| 2803 |
+
# Rule 9.7.3.2.1: ?????E????C???R?X?g???x?????????
|
| 2804 |
+
|
| 2805 |
+
# Rule 9.7.4:
|
| 2806 |
+
|
| 2807 |
+
# Rule 9.7.4.1: ??????U?????????E????A????\?E
|
| 2808 |
+
|
| 2809 |
+
# Rule 9.7.4.1.1: ?J?[?h?????J???Y????E?J???A??
|
| 2810 |
+
|
| 2811 |
+
# Rule 9.7.4.1.2: ?J?[?h???X?`E?E?W????F???O?E???
|
| 2812 |
+
|
| 2813 |
+
# Rule 9.7.4.1.3: ??L?????????O?E?A?E?J???Y??
|
| 2814 |
+
|
| 2815 |
+
# Rule 9.7.4.2: ????J?[?h????????U???\????????A??
|
| 2816 |
+
|
| 2817 |
+
# Rule 9.7.5:
|
| 2818 |
+
|
| 2819 |
+
# Rule 9.7.5.1: ?????U????A?????????????????E????E??
|
| 2820 |
+
|
| 2821 |
+
# Rule 9.7.6:
|
| 2822 |
+
|
| 2823 |
+
# Rule 9.7.6.1: ???U????A????????????????????1
|
| 2824 |
+
|
| 2825 |
+
# Rule 9.7.7:
|
| 2826 |
+
|
| 2827 |
+
# Rule 9.8:
|
| 2828 |
+
|
| 2829 |
+
# Rule 9.8.1:
|
| 2830 |
+
|
| 2831 |
+
# Rule 9.9:
|
| 2832 |
+
|
| 2833 |
+
# Rule 9.9.1:
|
| 2834 |
+
|
| 2835 |
+
# Rule 9.9.1.1: ?J?[?h?E?g??\?L??????E???E?????A????
|
| 2836 |
+
|
| 2837 |
+
# Rule 9.9.1.2: ????A?E???^????E??????E?L???????/
|
| 2838 |
+
|
| 2839 |
+
# Rule 9.9.1.3: ????A?p???????E??E???E??????l???X??E
|
| 2840 |
+
|
| 2841 |
+
# Rule 9.9.1.4: ????A?p???????E??E???E??????l??????E
|
| 2842 |
+
|
| 2843 |
+
# Rule 9.9.1.4.1: ?n?E?g??u???[?h?E?????????E????
|
| 2844 |
+
|
| 2845 |
+
# Rule 9.9.1.5: ????A?p???????E??E???E??????l???X??E
|
| 2846 |
+
|
| 2847 |
+
# Rule 9.9.1.5.1: ?n?E?g??u???[?h?E??????????Z????E
|
| 2848 |
+
|
| 2849 |
+
# Rule 9.9.1.6: ????E9.9.1.2X-9.9.1.4 ??K?p??E?E?O??I??
|
| 2850 |
+
|
| 2851 |
+
# Rule 9.9.1.7: ????E9.9.1.2X-9.9.1.6 ??K?p??E?E?O??I??
|
| 2852 |
+
|
| 2853 |
+
# Rule 9.9.1.7.1: ?p???????E???????????E??????
|
| 2854 |
+
|
| 2855 |
+
# Rule 9.9.1.7.2: ?????O?E?\???E???E?A?????v??
|
| 2856 |
+
|
| 2857 |
+
# Rule 9.9.2:
|
| 2858 |
+
|
| 2859 |
+
# Rule 9.9.3:
|
| 2860 |
+
|
| 2861 |
+
# Rule 9.9.3.1: ?????E?E????????J?[?h????????????E
|
| 2862 |
+
|
| 2863 |
+
# Rule 9.10:
|
| 2864 |
+
|
| 2865 |
+
# Rule 9.10.1:
|
| 2866 |
+
|
| 2867 |
+
# Rule 9.10.1.1: ???????A?u????????E?E???????????
|
| 2868 |
+
|
| 2869 |
+
# Rule 9.10.2:
|
| 2870 |
+
|
| 2871 |
+
# Rule 9.10.2.1: ?e????????????J?[?h??\???????
|
| 2872 |
+
|
| 2873 |
+
# Rule 9.10.2.2: ?e????????????Q?[??????s?????E
|
| 2874 |
+
|
| 2875 |
+
# Rule 9.10.2.3: ??????????????A?e?u???????E??
|
| 2876 |
+
|
| 2877 |
+
# Rule 9.10.3:
|
| 2878 |
+
|
| 2879 |
+
# Rule 9.11:
|
| 2880 |
+
|
| 2881 |
+
# Rule 9.11.1:
|
| 2882 |
+
|
| 2883 |
+
# Rule 9.12:
|
| 2884 |
+
|
| 2885 |
+
# Rule 9.12.1:
|
| 2886 |
+
|
| 2887 |
+
# Rule 9.12.2:
|
| 2888 |
+
|
| 2889 |
+
# Rule 10:
|
| 2890 |
+
|
| 2891 |
+
# Rule 10.1:
|
| 2892 |
+
|
| 2893 |
+
# Rule 10.1.1:
|
| 2894 |
+
|
| 2895 |
+
# Rule 10.1.2:
|
| 2896 |
+
|
| 2897 |
+
# Rule 10.1.3:
|
| 2898 |
+
|
| 2899 |
+
# Rule 10.2:
|
| 2900 |
+
|
| 2901 |
+
# Rule 10.2.1:
|
| 2902 |
+
|
| 2903 |
+
# Rule 10.2.2:
|
| 2904 |
+
|
| 2905 |
+
# Rule 10.2.2.1: ??E??????v???C???[????C???`E???L?u??E
|
| 2906 |
+
|
| 2907 |
+
# Rule 10.2.2.2: ???C???`E???L?u???????H?????E??????
|
| 2908 |
+
|
| 2909 |
+
# Rule 10.2.3:
|
| 2910 |
+
|
| 2911 |
+
# Rule 10.2.4:
|
| 2912 |
+
|
| 2913 |
+
# Rule 10.3:
|
| 2914 |
+
|
| 2915 |
+
# Rule 10.3.1:
|
| 2916 |
+
|
| 2917 |
+
# Rule 10.4:
|
| 2918 |
+
|
| 2919 |
+
# Rule 10.4.1:
|
| 2920 |
+
|
| 2921 |
+
# Rule 10.5:
|
| 2922 |
+
|
| 2923 |
+
# Rule 10.5.1:
|
| 2924 |
+
|
| 2925 |
+
# Rule 10.5.2:
|
| 2926 |
+
|
| 2927 |
+
# Rule 10.5.3:
|
| 2928 |
+
|
| 2929 |
+
# Rule 10.5.4:
|
| 2930 |
+
|
| 2931 |
+
# Rule 10.6:
|
| 2932 |
+
|
| 2933 |
+
# Rule 10.6.1:
|
| 2934 |
+
|
| 2935 |
+
# Rule 11:
|
| 2936 |
+
|
| 2937 |
+
# Rule 11.1:
|
| 2938 |
+
|
| 2939 |
+
# Rule 11.1.1:
|
| 2940 |
+
|
| 2941 |
+
# Rule 11.1.2:
|
| 2942 |
+
|
| 2943 |
+
# Rule 11.1.3:
|
| 2944 |
+
|
| 2945 |
+
# Rule 11.2:
|
| 2946 |
+
|
| 2947 |
+
# Rule 11.2.2:
|
| 2948 |
+
|
| 2949 |
+
# Rule 11.2.3:
|
| 2950 |
+
|
| 2951 |
+
# Rule 11.3:
|
| 2952 |
+
|
| 2953 |
+
# Rule 11.3.1: [Icon] ??A?????o?E???????o?E?G???A??u????E
|
| 2954 |
+
|
| 2955 |
+
# Rule 11.3.2:
|
| 2956 |
+
|
| 2957 |
+
# Rule 11.4:
|
| 2958 |
+
|
| 2959 |
+
# Rule 11.4.1: [Icon] ??A???C?u???J?n??????????E
|
| 2960 |
+
|
| 2961 |
+
# Rule 11.4.2:
|
| 2962 |
+
|
| 2963 |
+
# Rule 11.4.2.1: ?p?t?H?[?}???X?t?F?C?Y???A???v???C???[
|
| 2964 |
+
|
| 2965 |
+
# Rule 11.5:
|
| 2966 |
+
|
| 2967 |
+
# Rule 11.5.1: [Icon] ??A???C?u???????????????U??
|
| 2968 |
+
|
| 2969 |
+
# Rule 11.5.2:
|
| 2970 |
+
|
| 2971 |
+
# Rule 11.6:
|
| 2972 |
+
|
| 2973 |
+
# Rule 11.6.1: [Icon] ??A?E???E?v???C???????A?E???E?E
|
| 2974 |
+
|
| 2975 |
+
# Rule 11.6.2:
|
| 2976 |
+
|
| 2977 |
+
# Rule 11.6.3:
|
| 2978 |
+
|
| 2979 |
+
# Rule 11.6.4:
|
| 2980 |
+
|
| 2981 |
+
# Rule 11.7:
|
| 2982 |
+
|
| 2983 |
+
# Rule 11.7.1: [Icon] ??A?E???E?v???C???????A?E???E?E
|
| 2984 |
+
|
| 2985 |
+
# Rule 11.7.2:
|
| 2986 |
+
|
| 2987 |
+
# Rule 11.7.3:
|
| 2988 |
+
|
| 2989 |
+
# Rule 11.7.4:
|
| 2990 |
+
|
| 2991 |
+
# Rule 11.8:
|
| 2992 |
+
|
| 2993 |
+
# Rule 11.8.1: [Icon] ??A?E???E?v???C???????A?E???E?E
|
| 2994 |
+
|
| 2995 |
+
# Rule 11.8.2:
|
| 2996 |
+
|
| 2997 |
+
# Rule 11.8.3:
|
| 2998 |
+
|
| 2999 |
+
# Rule 11.8.4:
|
| 3000 |
+
|
| 3001 |
+
# Rule 11.9:
|
| 3002 |
+
|
| 3003 |
+
# Rule 11.9.1:
|
| 3004 |
+
|
| 3005 |
+
# Rule 11.9.2:
|
| 3006 |
+
|
| 3007 |
+
# Rule 11.10:
|
| 3008 |
+
|
| 3009 |
+
# Rule 11.10.1:
|
| 3010 |
+
|
| 3011 |
+
# Rule 11.10.2:
|
| 3012 |
+
|
| 3013 |
+
# Rule 12:
|
| 3014 |
+
|
| 3015 |
+
# Rule 12.1:
|
| 3016 |
+
|
| 3017 |
+
# Rule 12.1.1:
|
| 3018 |
+
|
| 3019 |
+
# Rule 12.1.1.1: ?A?N?`E???u?E???C???[?E?E.2?E??E?A????z???E
|
| 3020 |
+
|
| 3021 |
+
# Rule 12.1.1.2: ?A?N?`E???u?E???C???[???????E?s?????E
|
| 3022 |
+
|
| 3023 |
+
# Rule 12.1.1.3: ?????E?E??????A??????E?v???C???[??
|
| 3024 |
+
|
| 3025 |
+
# Rule 2025:
|
| 3026 |
+
|
| 3027 |
+
# --- END OF INDEX ---
|
engine/game/mixins/__pycache__/action_mixin.cpython-312.pyc
ADDED
|
Binary file (21 kB). View file
|
|
|
engine/game/mixins/__pycache__/effect_mixin.cpython-312.pyc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:458a917bf0e585b701ddee3dcf0a668e57ade6e9b592b05f6312e116cb4a382e
|
| 3 |
+
size 249282
|
engine/game/mixins/__pycache__/phase_mixin.cpython-312.pyc
ADDED
|
Binary file (28.7 kB). View file
|
|
|
engine/game/mixins/action_mixin.py
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
from typing import TYPE_CHECKING, Any
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
from engine.game.enums import Phase
|
| 7 |
+
from engine.models.ability import EffectType, TriggerType
|
| 8 |
+
|
| 9 |
+
if TYPE_CHECKING:
|
| 10 |
+
pass
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class ActionMixin:
|
| 14 |
+
"""
|
| 15 |
+
Mixin for GameState that handles player actions and core mechanics.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
def _resolve_deck_refresh(self, player: Any) -> None:
|
| 19 |
+
"""Rule 10.2: If deck is empty, shuffle waiting room to make new deck."""
|
| 20 |
+
if not player.main_deck and player.discard:
|
| 21 |
+
# Shuffle discard into deck
|
| 22 |
+
player.main_deck = list(player.discard)
|
| 23 |
+
player.discard = []
|
| 24 |
+
random.shuffle(player.main_deck)
|
| 25 |
+
player.deck_refreshed_this_turn = True
|
| 26 |
+
if hasattr(self, "log_rule"):
|
| 27 |
+
self.log_rule(
|
| 28 |
+
"Rule 10.2",
|
| 29 |
+
f"Player {player.player_id} Refreshed Deck from Discard ({len(player.main_deck)} cards).",
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
def _draw_cards(self, player: Any, count: int) -> None:
|
| 33 |
+
for _ in range(count):
|
| 34 |
+
if hasattr(self, "_process_rule_checks"):
|
| 35 |
+
self._process_rule_checks()
|
| 36 |
+
|
| 37 |
+
# Check for refresh before drawing
|
| 38 |
+
if not player.main_deck:
|
| 39 |
+
self._resolve_deck_refresh(player)
|
| 40 |
+
|
| 41 |
+
if player.main_deck:
|
| 42 |
+
player.hand.append(player.main_deck.pop(0))
|
| 43 |
+
player.hand_added_turn.append(self.turn_number)
|
| 44 |
+
else:
|
| 45 |
+
# Deck still empty after attempt to refresh (Rule 10.2.1.2: If both empty, cannot draw)
|
| 46 |
+
pass
|
| 47 |
+
|
| 48 |
+
if hasattr(self, "_process_rule_checks"):
|
| 49 |
+
self._process_rule_checks()
|
| 50 |
+
|
| 51 |
+
def _play_member(self, hand_idx: int, area_idx: int) -> None:
|
| 52 |
+
p = self.active_player
|
| 53 |
+
if "placement" in p.restrictions:
|
| 54 |
+
return
|
| 55 |
+
card_id = p.hand.pop(hand_idx)
|
| 56 |
+
if hand_idx < len(p.hand_added_turn):
|
| 57 |
+
added_turn = p.hand_added_turn.pop(hand_idx)
|
| 58 |
+
else:
|
| 59 |
+
# Fallback if list is desynced (shouldn't happen but prevented crash)
|
| 60 |
+
added_turn = 0
|
| 61 |
+
|
| 62 |
+
# Safety check: verify card exists in database before accessing
|
| 63 |
+
card_id_int = int(card_id)
|
| 64 |
+
if card_id_int not in self.member_db:
|
| 65 |
+
# Put the card back in hand and return to prevent crash
|
| 66 |
+
p.hand.insert(hand_idx, card_id)
|
| 67 |
+
p.hand_added_turn.insert(hand_idx, added_turn if hand_idx < len(p.hand_added_turn) else 0)
|
| 68 |
+
return
|
| 69 |
+
|
| 70 |
+
card = self.member_db[card_id_int]
|
| 71 |
+
if hasattr(self, "log_rule"):
|
| 72 |
+
self.log_rule("Rule 7.7.2.2", f"Player {p.player_id} plays {card.name} to Slot {area_idx}.")
|
| 73 |
+
# Calculate slot-specific cost reduction
|
| 74 |
+
slot_reduction = sum(
|
| 75 |
+
ce["effect"].value
|
| 76 |
+
for ce in p.continuous_effects
|
| 77 |
+
if ce["effect"].effect_type == EffectType.REDUCE_COST and (ce.get("target_slot", -1) in (-1, area_idx))
|
| 78 |
+
)
|
| 79 |
+
base_cost = max(0, card.cost - slot_reduction)
|
| 80 |
+
cost = base_cost
|
| 81 |
+
|
| 82 |
+
is_baton = p.stage[area_idx] >= 0
|
| 83 |
+
if is_baton:
|
| 84 |
+
extra_baton = sum(
|
| 85 |
+
ce["effect"].value
|
| 86 |
+
for ce in p.continuous_effects
|
| 87 |
+
if ce["effect"].effect_type == EffectType.BATON_TOUCH_MOD
|
| 88 |
+
)
|
| 89 |
+
effective_baton_limit = p.baton_touch_limit + extra_baton
|
| 90 |
+
|
| 91 |
+
if p.baton_touch_count >= effective_baton_limit:
|
| 92 |
+
# Should be caught by get_legal_actions, but safety first
|
| 93 |
+
p.hand.insert(hand_idx, card_id)
|
| 94 |
+
p.hand_added_turn.insert(hand_idx, added_turn)
|
| 95 |
+
return
|
| 96 |
+
|
| 97 |
+
prev_card_id = int(p.stage[area_idx])
|
| 98 |
+
if prev_card_id in self.member_db:
|
| 99 |
+
prev_card = self.member_db[prev_card_id]
|
| 100 |
+
else:
|
| 101 |
+
prev_card = None
|
| 102 |
+
|
| 103 |
+
if prev_card:
|
| 104 |
+
if hasattr(self, "log_rule"):
|
| 105 |
+
self.log_rule("Rule 9.6.2.3.2", f"Baton Touch! Cost reduced by {prev_card.cost}.")
|
| 106 |
+
cost = max(0, cost - prev_card.cost)
|
| 107 |
+
p.baton_touch_count += 1
|
| 108 |
+
|
| 109 |
+
# Rule 9.9.1.2: Check for ON_LEAVES triggers before/as card leaves stage
|
| 110 |
+
for ability in prev_card.abilities:
|
| 111 |
+
trig = getattr(ability, "trigger", "NO_TRIGGER")
|
| 112 |
+
if trig == TriggerType.ON_LEAVES:
|
| 113 |
+
self.triggered_abilities.append(
|
| 114 |
+
(
|
| 115 |
+
p.player_id,
|
| 116 |
+
ability,
|
| 117 |
+
{"area": area_idx, "card_id": p.stage[area_idx], "from_zone": "stage"},
|
| 118 |
+
)
|
| 119 |
+
)
|
| 120 |
+
else:
|
| 121 |
+
# If prev_card is None, just increment baton touch count without cost adjustment
|
| 122 |
+
p.baton_touch_count += 1
|
| 123 |
+
|
| 124 |
+
p.discard.append(p.stage[area_idx])
|
| 125 |
+
if p.stage_energy_count[area_idx] > 0:
|
| 126 |
+
p.energy_deck.extend(p.stage_energy[area_idx])
|
| 127 |
+
p.clear_stage_energy(area_idx)
|
| 128 |
+
untapped = [i for i, tapped in enumerate(p.tapped_energy) if not tapped]
|
| 129 |
+
if len(untapped) < cost:
|
| 130 |
+
p.hand.insert(hand_idx, card_id)
|
| 131 |
+
p.hand_added_turn.insert(hand_idx, added_turn)
|
| 132 |
+
return
|
| 133 |
+
for i in range(cost):
|
| 134 |
+
p.tapped_energy[untapped[i]] = True
|
| 135 |
+
p.stage[area_idx] = card_id
|
| 136 |
+
self.prev_cid = prev_card_id if "prev_card_id" in locals() else -1
|
| 137 |
+
p.members_played_this_turn[area_idx] = True # Rule 9.6.2.1.2.1: Cannot play into this slot again this turn
|
| 138 |
+
for ability in card.abilities:
|
| 139 |
+
if ability.trigger == TriggerType.ON_PLAY:
|
| 140 |
+
if hasattr(self, "log_rule"):
|
| 141 |
+
self.log_rule("Rule 11.3", f"Triggering [登場] (On Play) abilities for {card.name}.")
|
| 142 |
+
# print(f"DEBUG: Queuing ON_PLAY trigger for {card.name}")
|
| 143 |
+
self.triggered_abilities.append((p.player_id, ability, {"area": area_idx, "card_id": card_id}))
|
| 144 |
+
if hasattr(self, "_check_remote_triggers"):
|
| 145 |
+
self._check_remote_triggers(TriggerType.ON_PLAY, {"card_id": card_id, "area": area_idx})
|
| 146 |
+
|
| 147 |
+
if hasattr(self, "_process_rule_checks"):
|
| 148 |
+
self._process_rule_checks()
|
| 149 |
+
|
| 150 |
+
def _activate_member_ability(self, area: int) -> None:
|
| 151 |
+
p = self.active_player
|
| 152 |
+
card_id = int(p.stage[area])
|
| 153 |
+
if card_id < 0 or card_id not in self.member_db:
|
| 154 |
+
return
|
| 155 |
+
member = self.member_db[card_id]
|
| 156 |
+
ability = None
|
| 157 |
+
ability_idx = -1
|
| 158 |
+
for abi_idx, ab in enumerate(member.abilities):
|
| 159 |
+
if ab.trigger == TriggerType.ACTIVATED:
|
| 160 |
+
abi_key = f"{card_id}-{abi_idx}"
|
| 161 |
+
if ab.is_once_per_turn and abi_key in p.used_abilities:
|
| 162 |
+
continue
|
| 163 |
+
ability = ab
|
| 164 |
+
ability_idx = abi_idx
|
| 165 |
+
break
|
| 166 |
+
if not ability:
|
| 167 |
+
return
|
| 168 |
+
|
| 169 |
+
if hasattr(self, "log_rule"):
|
| 170 |
+
self.log_rule("Rule 7.7.2.1", f"Player {p.player_id} activates ability of {member.name} (Slot {area}).")
|
| 171 |
+
|
| 172 |
+
# Set resolution context for metadata tracking
|
| 173 |
+
self.current_resolving_ability = ability
|
| 174 |
+
self.current_resolving_member = member.name
|
| 175 |
+
self.current_resolving_member_id = card_id
|
| 176 |
+
|
| 177 |
+
if not self._pay_costs(p, ability.costs, source_area=area):
|
| 178 |
+
# Defer execution until cost paid (via choice)
|
| 179 |
+
abi_key = f"{card_id}-{ability_idx}"
|
| 180 |
+
self.pending_activation = {
|
| 181 |
+
"ability": ability,
|
| 182 |
+
"context": {"area": area, "card_id": card_id, "source_card_id": card_id},
|
| 183 |
+
"abi_key": abi_key,
|
| 184 |
+
}
|
| 185 |
+
return
|
| 186 |
+
total = len(ability.effects)
|
| 187 |
+
from engine.models.ability import ResolvingEffect
|
| 188 |
+
|
| 189 |
+
for i, effect in enumerate(reversed(ability.effects)):
|
| 190 |
+
step = total - i
|
| 191 |
+
self.pending_effects.insert(0, ResolvingEffect(effect, card_id, step, total))
|
| 192 |
+
|
| 193 |
+
if ability.is_once_per_turn:
|
| 194 |
+
p.used_abilities.add(f"{card_id}-{ability_idx}")
|
| 195 |
+
while self.pending_effects and not self.pending_choices:
|
| 196 |
+
self._resolve_pending_effect(0, context={"area": area, "card_id": card_id, "source_card_id": card_id})
|
| 197 |
+
|
| 198 |
+
def _execute_mulligan(self) -> None:
|
| 199 |
+
p = self.active_player
|
| 200 |
+
if hasattr(self, "log_rule"):
|
| 201 |
+
count = len(p.mulligan_selection) if hasattr(p, "mulligan_selection") else 0
|
| 202 |
+
self.log_rule("Rule 6.2.1.6", f"Player {p.player_id} finished mulligan ({count} cards).")
|
| 203 |
+
if hasattr(p, "mulligan_selection") and p.mulligan_selection:
|
| 204 |
+
cards_to_return = []
|
| 205 |
+
for idx in sorted(p.mulligan_selection, reverse=True):
|
| 206 |
+
if idx < len(p.hand):
|
| 207 |
+
cards_to_return.append(p.hand.pop(idx))
|
| 208 |
+
if idx < len(p.hand_added_turn):
|
| 209 |
+
p.hand_added_turn.pop(idx)
|
| 210 |
+
for _ in range(len(cards_to_return)):
|
| 211 |
+
if p.main_deck:
|
| 212 |
+
p.hand.append(p.main_deck.pop(0))
|
| 213 |
+
p.hand_added_turn.append(self.turn_number)
|
| 214 |
+
p.main_deck.extend(cards_to_return)
|
| 215 |
+
random.shuffle(p.main_deck)
|
| 216 |
+
|
| 217 |
+
if hasattr(p, "mulligan_selection"):
|
| 218 |
+
p.mulligan_selection.clear()
|
| 219 |
+
|
| 220 |
+
# Phase transition: P1 -> P2 -> ACTIVE
|
| 221 |
+
if self.phase == Phase.MULLIGAN_P1:
|
| 222 |
+
self.current_player = 1 - self.first_player
|
| 223 |
+
self.phase = Phase.MULLIGAN_P2
|
| 224 |
+
else:
|
| 225 |
+
self.current_player = self.first_player
|
| 226 |
+
self.phase = Phase.ACTIVE
|
| 227 |
+
|
| 228 |
+
def _check_hearts_meet_requirement(self, have: np.ndarray, need: np.ndarray) -> bool:
|
| 229 |
+
"""
|
| 230 |
+
Check if 'have' hearts satisfy 'need' requirements.
|
| 231 |
+
have/need: shape (7,) [Pink, Green, Yellow, Purple, Red, Blue, Any/Wildcard]
|
| 232 |
+
index 6 in 'have' is Wildcard (can be any color).
|
| 233 |
+
index 6 in 'need' is 'Any' requirement (can be satisfied by any color).
|
| 234 |
+
"""
|
| 235 |
+
# 1. Check specific color requirements (0-5)
|
| 236 |
+
# Calculate deficit for each color
|
| 237 |
+
deficits = np.maximum(0, need[:6] - have[:6])
|
| 238 |
+
total_deficit = np.sum(deficits)
|
| 239 |
+
|
| 240 |
+
# Check if we have enough Wildcards to cover the deficit
|
| 241 |
+
wildcards_have = have[6] if len(have) > 6 else 0
|
| 242 |
+
if wildcards_have < total_deficit:
|
| 243 |
+
return False
|
| 244 |
+
|
| 245 |
+
# 2. Check 'Any' requirement (index 6)
|
| 246 |
+
any_need = need[6] if len(need) > 6 else 0
|
| 247 |
+
if any_need <= 0:
|
| 248 |
+
return True
|
| 249 |
+
|
| 250 |
+
# Remaining Wildcards after covering deficit
|
| 251 |
+
remaining_wildcards = wildcards_have - total_deficit
|
| 252 |
+
|
| 253 |
+
# Surplus specific hearts (those not used for specific requirements)
|
| 254 |
+
surplus_specific = np.sum(np.maximum(0, have[:6] - need[:6]))
|
| 255 |
+
|
| 256 |
+
total_available_for_any = remaining_wildcards + surplus_specific
|
| 257 |
+
|
| 258 |
+
return total_available_for_any >= any_need
|
| 259 |
+
|
| 260 |
+
def _consume_hearts(self, have: np.ndarray, need: np.ndarray) -> None:
|
| 261 |
+
"""
|
| 262 |
+
Consume 'need' from 'have' in-place.
|
| 263 |
+
Assumes _check_hearts_meet_requirement returned True.
|
| 264 |
+
"""
|
| 265 |
+
# 1. Consume for specific requirements
|
| 266 |
+
for i in range(6):
|
| 267 |
+
n = need[i] if i < len(need) else 0
|
| 268 |
+
if n > 0:
|
| 269 |
+
# Use specific color first
|
| 270 |
+
take_specific = min(have[i], n)
|
| 271 |
+
have[i] -= take_specific
|
| 272 |
+
remaining_need = n - take_specific
|
| 273 |
+
|
| 274 |
+
# Use Wildcards for remainder
|
| 275 |
+
if remaining_need > 0 and len(have) > 6:
|
| 276 |
+
take_wild = min(have[6], remaining_need)
|
| 277 |
+
have[6] -= take_wild
|
| 278 |
+
|
| 279 |
+
# 2. Consume for 'Any' requirement
|
| 280 |
+
any_need = need[6] if len(need) > 6 else 0
|
| 281 |
+
if any_need > 0:
|
| 282 |
+
# First consume surplus specific colors
|
| 283 |
+
for i in range(6):
|
| 284 |
+
if any_need <= 0:
|
| 285 |
+
break
|
| 286 |
+
if have[i] > 0:
|
| 287 |
+
take = min(have[i], any_need)
|
| 288 |
+
have[i] -= take
|
| 289 |
+
any_need -= take
|
| 290 |
+
|
| 291 |
+
# Then consume Wildcards
|
| 292 |
+
if any_need > 0 and len(have) > 6:
|
| 293 |
+
take = min(have[6], any_need)
|
| 294 |
+
have[6] -= take
|
| 295 |
+
|
| 296 |
+
def _set_live_card(self, hand_idx: int) -> None:
|
| 297 |
+
"""Set a card face-down in live zone"""
|
| 298 |
+
p = self.active_player
|
| 299 |
+
if hand_idx < 0 or hand_idx >= len(p.hand) or len(p.live_zone) >= 3:
|
| 300 |
+
return
|
| 301 |
+
|
| 302 |
+
card_id = p.hand[hand_idx] # Look before pop for logging context
|
| 303 |
+
from engine.game.state_utils import get_base_id
|
| 304 |
+
|
| 305 |
+
base_id = get_base_id(card_id)
|
| 306 |
+
|
| 307 |
+
if base_id in self.live_db:
|
| 308 |
+
card_desc = f"{self.live_db[base_id].name} ({self.live_db[base_id].card_no})"
|
| 309 |
+
elif base_id in self.member_db:
|
| 310 |
+
card_desc = f"手札[{hand_idx}] ({self.member_db[base_id].card_no})"
|
| 311 |
+
else:
|
| 312 |
+
card_desc = f"Card #{card_id}"
|
| 313 |
+
|
| 314 |
+
if hasattr(self, "log_rule"):
|
| 315 |
+
self.log_rule("Rule 8.2.2", f"Player {p.player_id} sets {card_desc} to Live Zone.")
|
| 316 |
+
|
| 317 |
+
p.hand.pop(hand_idx)
|
| 318 |
+
if hand_idx < len(p.hand_added_turn):
|
| 319 |
+
p.hand_added_turn.pop(hand_idx)
|
| 320 |
+
p.live_zone.append(card_id)
|
| 321 |
+
p.live_zone_revealed.append(False)
|
| 322 |
+
# Rule 8.2.2 modification: Draw happens at end of Live Set phase, not immediately
|
| 323 |
+
# self._draw_cards(p, 1)
|
| 324 |
+
p.live_cards_set_this_turn += 1
|
| 325 |
+
|
| 326 |
+
def _execute_action(self, action: int) -> None:
|
| 327 |
+
"""Internal: execute action on this state (mutates self)"""
|
| 328 |
+
p = self.active_player
|
| 329 |
+
if self.phase in (Phase.MULLIGAN_P1, Phase.MULLIGAN_P2):
|
| 330 |
+
if action == 0:
|
| 331 |
+
self._execute_mulligan()
|
| 332 |
+
elif 300 <= action <= 359:
|
| 333 |
+
card_idx = action - 300
|
| 334 |
+
if card_idx < len(p.hand):
|
| 335 |
+
if not hasattr(p, "mulligan_selection"):
|
| 336 |
+
p.mulligan_selection = set()
|
| 337 |
+
if card_idx in p.mulligan_selection:
|
| 338 |
+
p.mulligan_selection.remove(card_idx)
|
| 339 |
+
else:
|
| 340 |
+
p.mulligan_selection.add(card_idx)
|
| 341 |
+
return
|
| 342 |
+
|
| 343 |
+
if getattr(self, "pending_choices", []):
|
| 344 |
+
if action == 0:
|
| 345 |
+
choice_type, params = self.pending_choices[0]
|
| 346 |
+
if choice_type == "CONTINUE_LIVE_RESULT":
|
| 347 |
+
self.pending_choices.pop(0)
|
| 348 |
+
self._finish_live_result()
|
| 349 |
+
return
|
| 350 |
+
if choice_type.startswith("CONTINUE"):
|
| 351 |
+
self.pending_choices.pop(0)
|
| 352 |
+
return
|
| 353 |
+
if True: # Handle action 0 as cancel/fail for both optional and mandatory (if forced)
|
| 354 |
+
self.pending_choices.pop(0)
|
| 355 |
+
|
| 356 |
+
# If look_and_choose declined, move looked cards to discard if on_fail is set
|
| 357 |
+
if params.get("reason") == "look_and_choose" and getattr(self, "looked_cards", []):
|
| 358 |
+
if params.get("on_fail") == "discard":
|
| 359 |
+
p.discard.extend(self.looked_cards)
|
| 360 |
+
self.looked_cards = []
|
| 361 |
+
|
| 362 |
+
if params.get("reason") in ("cost", "effect"):
|
| 363 |
+
self.pending_effects.clear()
|
| 364 |
+
# If we had chained choices (rare for cost), they are now invalid ideally
|
| 365 |
+
# But pending_choices might contain next steps?
|
| 366 |
+
# For "cost", the whole ability aborts, so safe to clear.
|
| 367 |
+
self.pending_choices.clear()
|
| 368 |
+
# Clear resolution state
|
| 369 |
+
self.looked_cards = []
|
| 370 |
+
self.current_resolving_member = None
|
| 371 |
+
self.current_resolving_member_id = -1
|
| 372 |
+
return
|
| 373 |
+
if action >= 500:
|
| 374 |
+
self._handle_choice(action)
|
| 375 |
+
return
|
| 376 |
+
|
| 377 |
+
if self.phase == Phase.ACTIVE:
|
| 378 |
+
self._do_active_phase()
|
| 379 |
+
elif self.phase == Phase.ENERGY:
|
| 380 |
+
self._do_energy_phase()
|
| 381 |
+
elif self.phase == Phase.DRAW:
|
| 382 |
+
self._do_draw_phase()
|
| 383 |
+
elif self.phase == Phase.MAIN:
|
| 384 |
+
if action == 0:
|
| 385 |
+
self._end_main_phase()
|
| 386 |
+
elif 1 <= action <= 180:
|
| 387 |
+
adj = action - 1
|
| 388 |
+
self._play_member(adj // 3, adj % 3)
|
| 389 |
+
elif 200 <= action <= 202:
|
| 390 |
+
self._activate_member_ability(action - 200)
|
| 391 |
+
elif self.phase == Phase.LIVE_SET:
|
| 392 |
+
if action == 0:
|
| 393 |
+
self._end_live_set()
|
| 394 |
+
elif 400 <= action <= 459:
|
| 395 |
+
self._set_live_card(action - 400)
|
| 396 |
+
elif self.phase == Phase.PERFORMANCE_P1:
|
| 397 |
+
if 900 <= action <= 902:
|
| 398 |
+
self._do_performance(0, live_idx=action - 900)
|
| 399 |
+
else:
|
| 400 |
+
self._do_performance(0)
|
| 401 |
+
elif self.phase == Phase.PERFORMANCE_P2:
|
| 402 |
+
if 900 <= action <= 902:
|
| 403 |
+
self._do_performance(1, live_idx=action - 900)
|
| 404 |
+
else:
|
| 405 |
+
self._do_performance(1)
|
| 406 |
+
elif self.phase == Phase.LIVE_RESULT:
|
| 407 |
+
self._do_live_result()
|
engine/game/mixins/effect_mixin.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
engine/game/mixins/phase_mixin.py
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
from typing import TYPE_CHECKING
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
from engine.game.enums import Phase
|
| 7 |
+
from engine.models.ability import EffectType, TriggerType
|
| 8 |
+
|
| 9 |
+
if TYPE_CHECKING:
|
| 10 |
+
pass
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class PhaseMixin:
|
| 14 |
+
"""
|
| 15 |
+
Mixin for GameState that handles turn and phase transitions.
|
| 16 |
+
|
| 17 |
+
Phase State Machine (Rule 7):
|
| 18 |
+
MULLIGAN_P1 -> MULLIGAN_P2 -> ACTIVE -> ENERGY -> DRAW -> MAIN
|
| 19 |
+
-> LIVE_SET (both players) -> PERFORMANCE_P1 -> PERFORMANCE_P2
|
| 20 |
+
-> LIVE_RESULT -> ACTIVE (next turn)
|
| 21 |
+
|
| 22 |
+
Each phase handler (e.g. _do_active_phase) advances self.phase to the next
|
| 23 |
+
phase before returning. Do not manually set self.phase after calling a handler.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def _do_active_phase(self) -> None:
|
| 27 |
+
p = self.active_player
|
| 28 |
+
if hasattr(self, "log_rule"):
|
| 29 |
+
self.log_rule("Rule 7.4.1", f"Active Phase: Untapping all members and energy for Player {p.player_id}.")
|
| 30 |
+
if isinstance(p.members_played_this_turn, list):
|
| 31 |
+
p.members_played_this_turn.clear()
|
| 32 |
+
else:
|
| 33 |
+
p.members_played_this_turn.fill(False)
|
| 34 |
+
p.untap_all()
|
| 35 |
+
self.phase = Phase.ENERGY
|
| 36 |
+
|
| 37 |
+
def _do_energy_phase(self) -> None:
|
| 38 |
+
p = self.active_player
|
| 39 |
+
if hasattr(self, "log_rule"):
|
| 40 |
+
self.log_rule(
|
| 41 |
+
"Rule 7.5.2", f"Energy Phase: Player {p.player_id} moves 1 card from Energy Deck to Energy Zone."
|
| 42 |
+
)
|
| 43 |
+
if p.energy_deck:
|
| 44 |
+
p.energy_zone.append(p.energy_deck.pop(0))
|
| 45 |
+
self.phase = Phase.DRAW
|
| 46 |
+
|
| 47 |
+
def _do_draw_phase(self) -> None:
|
| 48 |
+
p = self.active_player
|
| 49 |
+
if hasattr(self, "log_rule"):
|
| 50 |
+
self.log_rule("Rule 7.6.2", f"Draw Phase: Player {p.player_id} draws 1 card.")
|
| 51 |
+
self._draw_cards(p, 1)
|
| 52 |
+
self.phase = Phase.MAIN
|
| 53 |
+
|
| 54 |
+
def _advance_performance(self) -> None:
|
| 55 |
+
if self.first_player == 0:
|
| 56 |
+
if self.phase == Phase.PERFORMANCE_P1:
|
| 57 |
+
self.phase = Phase.PERFORMANCE_P2
|
| 58 |
+
self.current_player = 1
|
| 59 |
+
else:
|
| 60 |
+
self.phase = Phase.LIVE_RESULT
|
| 61 |
+
else:
|
| 62 |
+
if self.phase == Phase.PERFORMANCE_P2:
|
| 63 |
+
self.phase = Phase.PERFORMANCE_P1
|
| 64 |
+
self.current_player = 0
|
| 65 |
+
else:
|
| 66 |
+
self.phase = Phase.LIVE_RESULT
|
| 67 |
+
|
| 68 |
+
def _end_main_phase(self) -> None:
|
| 69 |
+
"""End player's main phase. If first player, switch to second player's turn.
|
| 70 |
+
If second player, advance to LIVE_SET phase."""
|
| 71 |
+
if hasattr(self, "log_rule"):
|
| 72 |
+
self.log_rule("Rule 7.7.3", f"Main Phase End: Player {self.current_player} ends Main Phase.")
|
| 73 |
+
if self.current_player == self.first_player:
|
| 74 |
+
# Switch to Player 2's turn: untap their cards and run their phases
|
| 75 |
+
p2 = 1 - self.first_player
|
| 76 |
+
self.players[p2].tapped_energy[:] = False
|
| 77 |
+
self.players[p2].tapped_members[:] = False
|
| 78 |
+
self.players[p2].members_played_this_turn[:] = False
|
| 79 |
+
self.current_player = p2
|
| 80 |
+
# Clear any stale looked_cards from ability resolution (safety)
|
| 81 |
+
self.looked_cards = []
|
| 82 |
+
# Chain through ACTIVE -> ENERGY -> DRAW -> MAIN
|
| 83 |
+
# Each handler sets self.phase to the next phase before returning
|
| 84 |
+
self.phase = Phase.ACTIVE
|
| 85 |
+
self._do_active_phase() # Sets phase to ENERGY
|
| 86 |
+
self._do_energy_phase() # Sets phase to DRAW
|
| 87 |
+
self._do_draw_phase() # Sets phase to MAIN
|
| 88 |
+
else:
|
| 89 |
+
# Both players have had their main phase, move to LIVE_SET
|
| 90 |
+
self.phase = Phase.LIVE_SET
|
| 91 |
+
self.current_player = self.first_player
|
| 92 |
+
# Clear any stale looked_cards from ability resolution
|
| 93 |
+
self.looked_cards = []
|
| 94 |
+
|
| 95 |
+
def _end_live_set(self) -> None:
|
| 96 |
+
"""End player's live set phase. First player sets first, then second player.
|
| 97 |
+
After both, advance to performance phase (first player performs first)."""
|
| 98 |
+
p = self.active_player
|
| 99 |
+
# Draw cards equal to the number of cards set this turn (Rule 8.2.2 modified timing)
|
| 100 |
+
if p.live_cards_set_this_turn > 0:
|
| 101 |
+
if hasattr(self, "log_rule"):
|
| 102 |
+
self.log_rule(
|
| 103 |
+
"Rule 8.2.2", f"Live Set End: Player {p.player_id} draws {p.live_cards_set_this_turn} cards."
|
| 104 |
+
)
|
| 105 |
+
self._draw_cards(p, p.live_cards_set_this_turn)
|
| 106 |
+
p.live_cards_set_this_turn = 0
|
| 107 |
+
|
| 108 |
+
if self.current_player == self.first_player:
|
| 109 |
+
# Switch to second player for their live set
|
| 110 |
+
self.current_player = 1 - self.first_player
|
| 111 |
+
else:
|
| 112 |
+
# Both players have set lives, begin performance
|
| 113 |
+
# Performance order matches first_player: P1 performs first if first_player=0
|
| 114 |
+
self.performance_results = {}
|
| 115 |
+
if self.first_player == 0:
|
| 116 |
+
self.phase = Phase.PERFORMANCE_P1
|
| 117 |
+
self.current_player = 0
|
| 118 |
+
else:
|
| 119 |
+
self.phase = Phase.PERFORMANCE_P2
|
| 120 |
+
self.current_player = 1
|
| 121 |
+
|
| 122 |
+
def _do_performance(self, player_idx: int, live_idx: int = -1) -> None:
|
| 123 |
+
"""Execute performance phase for a player.
|
| 124 |
+
If live_idx >= 0, only that specific live card is performed.
|
| 125 |
+
Otherwise (default -1), all cards in the live zone are performed (all-or-nothing).
|
| 126 |
+
"""
|
| 127 |
+
p = self.players[player_idx]
|
| 128 |
+
|
| 129 |
+
# Phase 1: Ability Trigger (Run once)
|
| 130 |
+
if not p.performance_abilities_processed:
|
| 131 |
+
p.performance_abilities_processed = True
|
| 132 |
+
|
| 133 |
+
p.live_zone_revealed = [True] * len(p.live_zone)
|
| 134 |
+
|
| 135 |
+
# Rule 8.3.4: "puts all cards that are not Live Cards into their Waiting Room"
|
| 136 |
+
# Filter live_zone: Keep only if in live_db
|
| 137 |
+
new_live_zone = []
|
| 138 |
+
for cid in p.live_zone:
|
| 139 |
+
if cid in self.live_db:
|
| 140 |
+
new_live_zone.append(cid)
|
| 141 |
+
else:
|
| 142 |
+
if hasattr(self, "log_rule"):
|
| 143 |
+
self.log_rule("Rule 8.3.4", f"Discarding non-live card {cid} from Live Zone.")
|
| 144 |
+
p.discard.append(cid)
|
| 145 |
+
p.live_zone = new_live_zone
|
| 146 |
+
|
| 147 |
+
triggers_found = False
|
| 148 |
+
for card_id in p.live_zone:
|
| 149 |
+
for ab in self.live_db[card_id].abilities:
|
| 150 |
+
if ab.trigger == TriggerType.ON_LIVE_START:
|
| 151 |
+
if hasattr(self, "log_rule"):
|
| 152 |
+
self.log_rule(
|
| 153 |
+
"Rule 11.4",
|
| 154 |
+
f"Triggering [ライブ開始時] (Live Start) abilities for {self.live_db[card_id].name}.",
|
| 155 |
+
)
|
| 156 |
+
self.triggered_abilities.append((player_idx, ab, {"card_id": card_id}))
|
| 157 |
+
triggers_found = True
|
| 158 |
+
|
| 159 |
+
for i, card_id in enumerate(p.stage):
|
| 160 |
+
if card_id >= 0 and not p.tapped_members[i] and card_id in self.member_db:
|
| 161 |
+
for ab in self.member_db[card_id].abilities:
|
| 162 |
+
if ab.trigger == TriggerType.ON_LIVE_START:
|
| 163 |
+
self.triggered_abilities.append((player_idx, ab, {"area": i}))
|
| 164 |
+
triggers_found = True
|
| 165 |
+
|
| 166 |
+
# If abilities triggered, return to main loop to process them
|
| 167 |
+
if triggers_found:
|
| 168 |
+
return
|
| 169 |
+
|
| 170 |
+
# Phase 2: Wait for resolution
|
| 171 |
+
if self.triggered_abilities or self.pending_choices or self.pending_effects:
|
| 172 |
+
return
|
| 173 |
+
|
| 174 |
+
# Phase 3: Calculation checks
|
| 175 |
+
if p.cannot_live:
|
| 176 |
+
for card_id in p.live_zone:
|
| 177 |
+
p.discard.append(card_id)
|
| 178 |
+
p.live_zone = []
|
| 179 |
+
p.performance_abilities_processed = False
|
| 180 |
+
self._advance_performance()
|
| 181 |
+
return
|
| 182 |
+
|
| 183 |
+
if not p.live_zone:
|
| 184 |
+
# Create empty performance result for consistency
|
| 185 |
+
self.performance_results[player_idx] = {
|
| 186 |
+
"success": False,
|
| 187 |
+
"member_contributions": [],
|
| 188 |
+
"yell_cards": [],
|
| 189 |
+
"total_hearts": [0] * 7,
|
| 190 |
+
"lives": [],
|
| 191 |
+
}
|
| 192 |
+
p.performance_abilities_processed = False
|
| 193 |
+
self._advance_performance()
|
| 194 |
+
return
|
| 195 |
+
|
| 196 |
+
total_blades = p.get_total_blades(self.member_db)
|
| 197 |
+
|
| 198 |
+
# Apply cheer_mod (RE_CHEER or cheer_mod rule)
|
| 199 |
+
extra_reveals = sum(
|
| 200 |
+
ce["effect"].value
|
| 201 |
+
for ce in p.continuous_effects
|
| 202 |
+
if ce["effect"].effect_type == EffectType.META_RULE and ce["effect"].params.get("type") == "cheer_mod"
|
| 203 |
+
)
|
| 204 |
+
total_blades = max(0, total_blades + extra_reveals)
|
| 205 |
+
|
| 206 |
+
print(f"DEBUG: Total Blades (cheer reveal count): {total_blades}")
|
| 207 |
+
|
| 208 |
+
# Collect blade breakdown for result
|
| 209 |
+
blade_breakdown = []
|
| 210 |
+
for i in range(3):
|
| 211 |
+
if p.stage[i] >= 0:
|
| 212 |
+
blade_breakdown.extend(p.get_blades_breakdown(i, self.member_db))
|
| 213 |
+
blade_breakdown.extend(p.get_global_blades_breakdown())
|
| 214 |
+
|
| 215 |
+
if hasattr(self, "log_rule"):
|
| 216 |
+
self.log_rule("Rule 8.3.11", f"Player {player_idx} performs Yell ({total_blades} blades).")
|
| 217 |
+
self.yell_cards = []
|
| 218 |
+
for _ in range(total_blades):
|
| 219 |
+
if not p.main_deck and p.discard:
|
| 220 |
+
random.shuffle(p.discard)
|
| 221 |
+
p.main_deck, p.discard = p.discard, []
|
| 222 |
+
if p.main_deck:
|
| 223 |
+
self.yell_cards.append(p.main_deck.pop(0))
|
| 224 |
+
draw_bonus, yell_score_bonus = 0, 0
|
| 225 |
+
total_hearts = np.zeros(7, dtype=np.int32)
|
| 226 |
+
member_contributions = []
|
| 227 |
+
heart_breakdown = []
|
| 228 |
+
for i in range(3):
|
| 229 |
+
cid = p.stage[i]
|
| 230 |
+
if cid >= 0:
|
| 231 |
+
hraw = p.get_effective_hearts(i, self.member_db)
|
| 232 |
+
total_hearts[: len(hraw)] += hraw
|
| 233 |
+
heart_breakdown.extend(p.get_hearts_breakdown(i, self.member_db))
|
| 234 |
+
card = self.member_db[cid]
|
| 235 |
+
|
| 236 |
+
# Rule 8.4.1: Include Stage Member volume icons
|
| 237 |
+
vol = getattr(card, "volume_icons", 0)
|
| 238 |
+
yell_score_bonus += vol
|
| 239 |
+
|
| 240 |
+
member_contributions.append(
|
| 241 |
+
{
|
| 242 |
+
"source_id": cid,
|
| 243 |
+
"source": card.name,
|
| 244 |
+
"img": card.img_path,
|
| 245 |
+
"hearts": hraw.tolist(),
|
| 246 |
+
"blades": int(p.get_effective_blades(i, self.member_db)),
|
| 247 |
+
"volume_icons": vol,
|
| 248 |
+
"draw_icons": getattr(card, "draw_icons", 0),
|
| 249 |
+
}
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
yell_card_details = []
|
| 253 |
+
for card_id in self.yell_cards:
|
| 254 |
+
card = self.member_db.get(card_id) or self.live_db.get(card_id)
|
| 255 |
+
if card:
|
| 256 |
+
draw_bonus += getattr(card, "draw_icons", 0)
|
| 257 |
+
vol = getattr(card, "volume_icons", 0)
|
| 258 |
+
yell_score_bonus += vol
|
| 259 |
+
|
| 260 |
+
details = {
|
| 261 |
+
"id": card_id,
|
| 262 |
+
"name": card.name,
|
| 263 |
+
"img": card.img_path,
|
| 264 |
+
"blade_hearts": [0] * 7,
|
| 265 |
+
"volume_icons": vol,
|
| 266 |
+
"draw_icons": getattr(card, "draw_icons", 0),
|
| 267 |
+
}
|
| 268 |
+
# Rule 8.4.1: Include Blade Hearts (including ALL Blade) from revealed cards
|
| 269 |
+
bh = getattr(card, "blade_hearts", None)
|
| 270 |
+
if bh is not None:
|
| 271 |
+
bh_padded = np.zeros(7, dtype=np.int32)
|
| 272 |
+
bh_padded[: len(bh)] = bh[:7]
|
| 273 |
+
total_hearts[: len(bh_padded)] += bh_padded
|
| 274 |
+
details["blade_hearts"] = bh_padded.tolist()
|
| 275 |
+
yell_card_details.append(details)
|
| 276 |
+
|
| 277 |
+
# Apply global TRANSFORM_COLOR to the final heart pool
|
| 278 |
+
transform_logs = []
|
| 279 |
+
for ce in p.continuous_effects:
|
| 280 |
+
eff = ce["effect"]
|
| 281 |
+
if eff.effect_type == EffectType.TRANSFORM_COLOR:
|
| 282 |
+
# Value v is the amount or filter? Usually it's "all of type X become Y"
|
| 283 |
+
# Params: from_color (int 1-6), to_color (int 1-6)
|
| 284 |
+
src = eff.params.get("from_color", eff.params.get("color"))
|
| 285 |
+
dest = eff.params.get("to_color")
|
| 286 |
+
if src and dest:
|
| 287 |
+
try:
|
| 288 |
+
s_idx, d_idx = int(src) - 1, int(dest) - 1
|
| 289 |
+
if 0 <= s_idx < 6 and 0 <= d_idx < 6:
|
| 290 |
+
transfer = total_hearts[s_idx]
|
| 291 |
+
total_hearts[d_idx] += transfer
|
| 292 |
+
total_hearts[s_idx] = 0
|
| 293 |
+
heart_breakdown.append(
|
| 294 |
+
{
|
| 295 |
+
"source": ce.get("source_name", "Effect"),
|
| 296 |
+
"type": "transform",
|
| 297 |
+
"text": f"Transform {src} Yells to {dest}",
|
| 298 |
+
}
|
| 299 |
+
)
|
| 300 |
+
transform_logs.append(
|
| 301 |
+
{
|
| 302 |
+
"source": ce.get("source_name", "Effect"),
|
| 303 |
+
"desc": f"Transform {src} Yells to {dest}",
|
| 304 |
+
"type": "transform",
|
| 305 |
+
"source_id": ce.get("source_id", -1),
|
| 306 |
+
}
|
| 307 |
+
)
|
| 308 |
+
except:
|
| 309 |
+
pass
|
| 310 |
+
|
| 311 |
+
self._draw_cards(p, draw_bonus)
|
| 312 |
+
|
| 313 |
+
# Save pre-consumption hearts for display
|
| 314 |
+
display_hearts = total_hearts.copy()
|
| 315 |
+
|
| 316 |
+
# --- ALL-OR-NOTHING CHECK ---
|
| 317 |
+
total_req = np.zeros(7, dtype=np.int32)
|
| 318 |
+
live_reqs = [] # Store individual reqs for UI and logic
|
| 319 |
+
requirement_logs = []
|
| 320 |
+
|
| 321 |
+
# Determine target lives
|
| 322 |
+
target_lives = []
|
| 323 |
+
if 0 <= live_idx < len(p.live_zone):
|
| 324 |
+
target_lives = [p.live_zone[live_idx]]
|
| 325 |
+
else:
|
| 326 |
+
target_lives = p.live_zone
|
| 327 |
+
|
| 328 |
+
for card_id in target_lives:
|
| 329 |
+
live = self.live_db[card_id]
|
| 330 |
+
req = live.required_hearts.copy()
|
| 331 |
+
for ce in p.continuous_effects:
|
| 332 |
+
if ce["effect"].effect_type == EffectType.REDUCE_HEART_REQ:
|
| 333 |
+
val = ce["effect"].value
|
| 334 |
+
color = ce["effect"].params.get("color")
|
| 335 |
+
if color == "any" or not color:
|
| 336 |
+
req[6] = max(0, req[6] - val)
|
| 337 |
+
else:
|
| 338 |
+
try:
|
| 339 |
+
# color param might be 1-6
|
| 340 |
+
c_idx = int(color) - 1
|
| 341 |
+
if 0 <= c_idx < 6:
|
| 342 |
+
req[c_idx] = max(0, req[c_idx] - val)
|
| 343 |
+
except:
|
| 344 |
+
pass
|
| 345 |
+
|
| 346 |
+
# Log requirement reduction
|
| 347 |
+
red_vec = np.zeros(7, dtype=np.int32)
|
| 348 |
+
if color == "any" or not color:
|
| 349 |
+
red_vec[6] = val
|
| 350 |
+
else:
|
| 351 |
+
try:
|
| 352 |
+
c_idx = int(color) - 1
|
| 353 |
+
if 0 <= c_idx < 6:
|
| 354 |
+
red_vec[c_idx] = val
|
| 355 |
+
except:
|
| 356 |
+
pass
|
| 357 |
+
|
| 358 |
+
if np.any(red_vec > 0):
|
| 359 |
+
requirement_logs.append(
|
| 360 |
+
{
|
| 361 |
+
"source": ce.get("source_name", "Effect"),
|
| 362 |
+
"value": (-red_vec).tolist(),
|
| 363 |
+
"type": "req_mod",
|
| 364 |
+
"source_id": ce.get("source_id", -1),
|
| 365 |
+
}
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
# Pad to 7 just in case
|
| 369 |
+
req_padded = np.zeros(7, dtype=np.int32)
|
| 370 |
+
req_padded[: len(req)] = req[:7]
|
| 371 |
+
|
| 372 |
+
total_req += req_padded
|
| 373 |
+
live_reqs.append((card_id, req_padded))
|
| 374 |
+
|
| 375 |
+
temp_hearts = total_hearts.copy()
|
| 376 |
+
live_details = []
|
| 377 |
+
passed_lives_acc = []
|
| 378 |
+
any_failed = False
|
| 379 |
+
|
| 380 |
+
for card_id, req in live_reqs:
|
| 381 |
+
l_passed = self._check_hearts_meet_requirement(temp_hearts, req)
|
| 382 |
+
status_text = "PASSED" if l_passed else "FAILED"
|
| 383 |
+
if hasattr(self, "log_rule"):
|
| 384 |
+
card_name = self.live_db[card_id].name if card_id in self.live_db else f"Card {card_id}"
|
| 385 |
+
self.log_rule("Rule 8.3.15", f"P{player_idx} Live Card '{card_name}': {status_text}")
|
| 386 |
+
|
| 387 |
+
if not any_failed and l_passed:
|
| 388 |
+
# SUCCESS for this card
|
| 389 |
+
before = temp_hearts.copy()
|
| 390 |
+
self._consume_hearts(temp_hearts, req)
|
| 391 |
+
filled = before - temp_hearts
|
| 392 |
+
|
| 393 |
+
live_details.append(
|
| 394 |
+
{
|
| 395 |
+
"id": card_id,
|
| 396 |
+
"name": self.live_db[card_id].name,
|
| 397 |
+
"img": self.live_db[card_id].img_path,
|
| 398 |
+
"required": req.tolist(),
|
| 399 |
+
"filled": filled.tolist(),
|
| 400 |
+
"passed": True,
|
| 401 |
+
"score": self.live_db[card_id].score,
|
| 402 |
+
}
|
| 403 |
+
)
|
| 404 |
+
passed_lives_acc.append(card_id)
|
| 405 |
+
else:
|
| 406 |
+
# FAILED for this card or previous card failed
|
| 407 |
+
any_failed = True
|
| 408 |
+
live_details.append(
|
| 409 |
+
{
|
| 410 |
+
"id": card_id,
|
| 411 |
+
"name": self.live_db[card_id].name,
|
| 412 |
+
"img": self.live_db[card_id].img_path,
|
| 413 |
+
"required": req.tolist(),
|
| 414 |
+
"filled": [0] * 7,
|
| 415 |
+
"passed": False,
|
| 416 |
+
"score": self.live_db[card_id].score,
|
| 417 |
+
}
|
| 418 |
+
)
|
| 419 |
+
|
| 420 |
+
all_passed = not any_failed and len(passed_lives_acc) == len(live_reqs)
|
| 421 |
+
|
| 422 |
+
# Log results
|
| 423 |
+
if hasattr(self, "log_rule"):
|
| 424 |
+
res_str = "PASSED" if all_passed else "FAILED"
|
| 425 |
+
self.log_rule(
|
| 426 |
+
"Rule 8.3.15",
|
| 427 |
+
f"Performance P{player_idx} {res_str} - Lives Passed: {len(passed_lives_acc)}/{len(live_reqs)}",
|
| 428 |
+
)
|
| 429 |
+
|
| 430 |
+
# Rule 8.3.16: If ANY failed, ALL go to discard.
|
| 431 |
+
if all_passed:
|
| 432 |
+
p.passed_lives = passed_lives_acc
|
| 433 |
+
p.live_zone = [] # All moved to passed_lives
|
| 434 |
+
else:
|
| 435 |
+
p.passed_lives = []
|
| 436 |
+
if hasattr(self, "log_rule"):
|
| 437 |
+
self.log_rule(
|
| 438 |
+
"Rule 8.3.16", f"P{player_idx} Performance Failure: Discarding {len(p.live_zone)} live cards."
|
| 439 |
+
)
|
| 440 |
+
p.discard.extend(p.live_zone)
|
| 441 |
+
p.live_zone = []
|
| 442 |
+
|
| 443 |
+
p.live_score_bonus += yell_score_bonus
|
| 444 |
+
p.yell_score_count = yell_score_bonus
|
| 445 |
+
|
| 446 |
+
self.performance_results[player_idx] = {
|
| 447 |
+
"success": all_passed,
|
| 448 |
+
"lives_passed_count": len(passed_lives_acc) if all_passed else 0,
|
| 449 |
+
"member_contributions": member_contributions,
|
| 450 |
+
"yell_cards": yell_card_details,
|
| 451 |
+
"total_hearts": display_hearts.tolist(),
|
| 452 |
+
"lives": live_details,
|
| 453 |
+
"breakdown": {
|
| 454 |
+
"blades": blade_breakdown,
|
| 455 |
+
"hearts": heart_breakdown,
|
| 456 |
+
"requirements": requirement_logs,
|
| 457 |
+
"transforms": transform_logs,
|
| 458 |
+
"score_modifiers": [], # Will be populated in _do_live_result
|
| 459 |
+
},
|
| 460 |
+
"yell_score_bonus": yell_score_bonus,
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
# Store in history
|
| 464 |
+
if not hasattr(self, "performance_history"):
|
| 465 |
+
self.performance_history = []
|
| 466 |
+
hist_entry = self.performance_results[player_idx].copy()
|
| 467 |
+
hist_entry["player_id"] = player_idx
|
| 468 |
+
hist_entry["turn"] = self.turn_number
|
| 469 |
+
self.performance_history.append(hist_entry)
|
| 470 |
+
|
| 471 |
+
p.performance_abilities_processed = False
|
| 472 |
+
self._advance_performance()
|
| 473 |
+
|
| 474 |
+
def _clear_expired_effects(self, expiry_type: str) -> None:
|
| 475 |
+
for p in self.players:
|
| 476 |
+
p.continuous_effects = [e for e in p.continuous_effects if e.get("expiry") != expiry_type]
|
| 477 |
+
if expiry_type == "LIVE_END":
|
| 478 |
+
p.cannot_live = False
|
| 479 |
+
p.live_score_bonus = 0
|
| 480 |
+
p.live_success_triggered = False
|
| 481 |
+
|
| 482 |
+
def _do_live_result(self) -> None:
|
| 483 |
+
"""
|
| 484 |
+
Rule 8.4: Determine live winner and handle success.
|
| 485 |
+
"""
|
| 486 |
+
if hasattr(self, "performance_results"):
|
| 487 |
+
self.last_performance_results = self.performance_results.copy()
|
| 488 |
+
|
| 489 |
+
p0, p1 = self.players[0], self.players[1]
|
| 490 |
+
|
| 491 |
+
# Rule 8.4.4: Success event triggers before winner determination
|
| 492 |
+
# Trigger ON_LIVE_SUCCESS for both Stage Members and performed Live Cards
|
| 493 |
+
for pid, p in enumerate(self.players):
|
| 494 |
+
if p.passed_lives and not p.live_success_triggered:
|
| 495 |
+
p.live_success_triggered = True
|
| 496 |
+
if hasattr(self, "log_rule"):
|
| 497 |
+
self.log_rule("Rule 11.5", f"P{pid} Live Success Event: Triggering [ライブ成功時] abilities.")
|
| 498 |
+
# Stage Members
|
| 499 |
+
for i, cid in enumerate(p.stage):
|
| 500 |
+
if cid >= 0 and cid in self.member_db:
|
| 501 |
+
for ab in self.member_db[cid].abilities:
|
| 502 |
+
if ab.trigger == TriggerType.ON_LIVE_SUCCESS:
|
| 503 |
+
self.triggered_abilities.append((pid, ab, {"area": i}))
|
| 504 |
+
# Live Cards (Rule 11.5 explicitly mentions both can have success abilities)
|
| 505 |
+
for cid in p.passed_lives:
|
| 506 |
+
if cid in self.live_db:
|
| 507 |
+
for ab in self.live_db[cid].abilities:
|
| 508 |
+
if ab.trigger == TriggerType.ON_LIVE_SUCCESS:
|
| 509 |
+
self.triggered_abilities.append((pid, ab, {"card_id": cid}))
|
| 510 |
+
|
| 511 |
+
# If abilities triggered, return to process them (allowing score mods to apply)
|
| 512 |
+
if self.triggered_abilities:
|
| 513 |
+
return
|
| 514 |
+
|
| 515 |
+
# Calculate base score from passed_lives (Rule 8.4.1)
|
| 516 |
+
p0_base = sum(self.live_db[c].score for c in p0.passed_lives if c in self.live_db)
|
| 517 |
+
p1_base = sum(self.live_db[c].score for c in p1.passed_lives if c in self.live_db)
|
| 518 |
+
|
| 519 |
+
# Apply score modifiers (Rule 11)
|
| 520 |
+
for pid, p in enumerate([p0, p1]):
|
| 521 |
+
player_score_mods = []
|
| 522 |
+
for ce in p.continuous_effects:
|
| 523 |
+
if ce["effect"].effect_type == EffectType.MODIFY_SCORE_RULE:
|
| 524 |
+
val = ce["effect"].value
|
| 525 |
+
if ce["effect"].params and ce["effect"].params.get("multiplier_source") == "yell_score_icon":
|
| 526 |
+
val *= p.yell_score_count
|
| 527 |
+
p.live_score_bonus += val
|
| 528 |
+
|
| 529 |
+
# Capture for breakdown
|
| 530 |
+
if pid in self.performance_results:
|
| 531 |
+
self.performance_results[pid]["breakdown"]["score_modifiers"].append(
|
| 532 |
+
{
|
| 533 |
+
"source": ce.get("source_name", "Effect"),
|
| 534 |
+
"value": val,
|
| 535 |
+
"desc": f"Score modifier from {ce.get('source_name', 'Effect')}",
|
| 536 |
+
"source_id": ce.get("source_id", -1),
|
| 537 |
+
}
|
| 538 |
+
)
|
| 539 |
+
|
| 540 |
+
p0_total, p1_total = p0_base + p0.live_score_bonus, p1_base + p1.live_score_bonus
|
| 541 |
+
if hasattr(self, "log_rule"):
|
| 542 |
+
self.log_rule("Rule 8.4.6", f"Live Judgment: P0={p0_total}, P1={p1_total}")
|
| 543 |
+
|
| 544 |
+
# Determine Winners (Rule 8.4.6)
|
| 545 |
+
winners = []
|
| 546 |
+
if p0_total > 0 or p1_total > 0:
|
| 547 |
+
if p0_total > p1_total:
|
| 548 |
+
winners = [0]
|
| 549 |
+
elif p1_total > p0_total:
|
| 550 |
+
winners = [1]
|
| 551 |
+
else:
|
| 552 |
+
winners = [0, 1]
|
| 553 |
+
|
| 554 |
+
choices = []
|
| 555 |
+
is_tie = len(winners) == 2
|
| 556 |
+
|
| 557 |
+
for w in winners:
|
| 558 |
+
p = self.players[w]
|
| 559 |
+
if p.passed_lives:
|
| 560 |
+
# Rule 8.4.7.1: Penalty for ties with multiple cards
|
| 561 |
+
if is_tie and len(p.passed_lives) >= 2:
|
| 562 |
+
if hasattr(self, "log_rule"):
|
| 563 |
+
self.log_rule(
|
| 564 |
+
"Rule 8.4.7.1", f"Player {w} Tie Penalty: {len(p.passed_lives)} cards; 0 points awarded."
|
| 565 |
+
)
|
| 566 |
+
continue
|
| 567 |
+
|
| 568 |
+
# Rule 8.4.7.2 / 8.4.7.3: Move exactly one card to success lives
|
| 569 |
+
if len(p.passed_lives) == 1:
|
| 570 |
+
cid = p.passed_lives[0]
|
| 571 |
+
if hasattr(self, "log_rule"):
|
| 572 |
+
self.log_rule(
|
| 573 |
+
"Rule 11.5",
|
| 574 |
+
f"Triggering [ライブ成功時] (Live Success) abilities for {self.live_db[cid].name}.",
|
| 575 |
+
)
|
| 576 |
+
p.success_lives.append(cid)
|
| 577 |
+
p.passed_lives.pop(0)
|
| 578 |
+
if hasattr(self, "log_rule"):
|
| 579 |
+
self.log_rule("Rule 8.4.7.2", f"Player {w} obtained 1 Success Live: {self.live_db[cid].name}")
|
| 580 |
+
else:
|
| 581 |
+
# Multi-card success (when not a tie): must choose one (Rule 8.4.7.3)
|
| 582 |
+
choices.append(
|
| 583 |
+
(
|
| 584 |
+
"SELECT_SUCCESS_LIVE",
|
| 585 |
+
{
|
| 586 |
+
"cards": p.passed_lives.copy(),
|
| 587 |
+
"player_id": w,
|
| 588 |
+
"source_card_id": p.passed_lives[0],
|
| 589 |
+
# Simplified choice text
|
| 590 |
+
"effect_description": "獲得するライブカードを1枚選んでください",
|
| 591 |
+
"source_member": "ライブ判定",
|
| 592 |
+
},
|
| 593 |
+
)
|
| 594 |
+
)
|
| 595 |
+
|
| 596 |
+
for c in reversed(choices):
|
| 597 |
+
if c not in self.pending_choices:
|
| 598 |
+
self.pending_choices.insert(0, c)
|
| 599 |
+
|
| 600 |
+
if self.pending_choices:
|
| 601 |
+
choice_player = self.pending_choices[0][1].get("player_id")
|
| 602 |
+
self.current_player = choice_player
|
| 603 |
+
return
|
| 604 |
+
|
| 605 |
+
# Cleanup (Rule 8.4.8)
|
| 606 |
+
for w, p in enumerate(self.players):
|
| 607 |
+
if p.passed_lives:
|
| 608 |
+
if hasattr(self, "log_rule"):
|
| 609 |
+
self.log_rule(
|
| 610 |
+
"Rule 8.4.8",
|
| 611 |
+
f"P{w} Live Result Cleanup: Discarding {len(p.passed_lives)} surplus passed cards.",
|
| 612 |
+
)
|
| 613 |
+
p.discard.extend(p.passed_lives)
|
| 614 |
+
p.passed_lives = []
|
| 615 |
+
p.live_score_bonus = 0
|
| 616 |
+
|
| 617 |
+
# Determine who goes first next turn (Rule 8.4.10)
|
| 618 |
+
# Winner goes first. If both won (tie), host/active or specified logic.
|
| 619 |
+
if len(winners) == 1:
|
| 620 |
+
self.first_player = winners[0]
|
| 621 |
+
|
| 622 |
+
# Reset turn effects
|
| 623 |
+
for p in self.players:
|
| 624 |
+
p.continuous_effects = [e for e in p.continuous_effects if e.get("expiry") != "TURN_END"]
|
| 625 |
+
p.cannot_live = False
|
| 626 |
+
p.used_abilities.clear()
|
| 627 |
+
p.moved_members_this_turn.clear()
|
| 628 |
+
|
| 629 |
+
# Advance turn
|
| 630 |
+
self._finish_live_result()
|
| 631 |
+
|
| 632 |
+
def _finish_live_result(self) -> None:
|
| 633 |
+
"""Advance to the next turn after live results are finalized."""
|
| 634 |
+
# Q171: "Until end of live" effects expire at the end of the Live Result phase.
|
| 635 |
+
self._clear_expired_effects("LIVE_END")
|
| 636 |
+
|
| 637 |
+
self.turn_number += 1
|
| 638 |
+
self.action_count_this_turn = 0
|
| 639 |
+
self.first_player = (self.first_player + 1) % len(self.players)
|
| 640 |
+
self.current_player = self.first_player
|
| 641 |
+
self.phase = Phase.ACTIVE
|
| 642 |
+
|
| 643 |
+
if hasattr(self, "performance_results"):
|
| 644 |
+
self.performance_results.clear()
|
| 645 |
+
|
| 646 |
+
for p in self.players:
|
| 647 |
+
# Rule 8.4.11: Move any unperformed live cards to discard
|
| 648 |
+
p.discard.extend(p.live_zone)
|
| 649 |
+
p.live_zone = []
|
| 650 |
+
p.live_zone_revealed = []
|
| 651 |
+
p.performance_abilities_processed = False
|
| 652 |
+
|
| 653 |
+
self.check_win_condition()
|
| 654 |
+
self._do_active_phase()
|
engine/game/numba_utils.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Numba Utils for Love Live Card Game
|
| 3 |
+
JIT-compiled functions for high-performance game logic.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
try:
|
| 7 |
+
from numba import njit
|
| 8 |
+
|
| 9 |
+
JIT_AVAILABLE = True
|
| 10 |
+
except ImportError:
|
| 11 |
+
# Fallback to python decorator if numba is missing (for safety)
|
| 12 |
+
JIT_AVAILABLE = False
|
| 13 |
+
|
| 14 |
+
def njit(func):
|
| 15 |
+
return func
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
import numpy as np
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@njit
|
| 22 |
+
def calc_main_phase_masks(
|
| 23 |
+
hand: np.ndarray,
|
| 24 |
+
stage: np.ndarray,
|
| 25 |
+
available_energy: int,
|
| 26 |
+
total_reduction: int,
|
| 27 |
+
baton_touch_allowed: bool,
|
| 28 |
+
members_played_this_turn: np.ndarray,
|
| 29 |
+
member_costs: np.ndarray, # Direct lookup array: cost = member_costs[card_id]
|
| 30 |
+
mask: np.ndarray,
|
| 31 |
+
) -> None:
|
| 32 |
+
"""
|
| 33 |
+
JIT-compiled Main Phase legal action calculation.
|
| 34 |
+
"""
|
| 35 |
+
# Iterate through hand to find playable members
|
| 36 |
+
hand_len = len(hand)
|
| 37 |
+
for i in range(hand_len):
|
| 38 |
+
card_id = hand[i]
|
| 39 |
+
|
| 40 |
+
# Check if valid card ID for array lookup
|
| 41 |
+
if card_id < 0 or card_id >= len(member_costs):
|
| 42 |
+
continue
|
| 43 |
+
|
| 44 |
+
base_cost = member_costs[card_id]
|
| 45 |
+
if base_cost == -1: # Not a member
|
| 46 |
+
continue
|
| 47 |
+
|
| 48 |
+
# Check each of the 3 stage areas
|
| 49 |
+
for area in range(3):
|
| 50 |
+
if members_played_this_turn[area]:
|
| 51 |
+
continue
|
| 52 |
+
|
| 53 |
+
active_cost = base_cost - total_reduction
|
| 54 |
+
if active_cost < 0:
|
| 55 |
+
active_cost = 0
|
| 56 |
+
|
| 57 |
+
# Baton Touch Rule
|
| 58 |
+
stage_card_id = stage[area]
|
| 59 |
+
if baton_touch_allowed and stage_card_id != -1:
|
| 60 |
+
if stage_card_id < len(member_costs):
|
| 61 |
+
existing_cost = member_costs[stage_card_id]
|
| 62 |
+
if existing_cost != -1:
|
| 63 |
+
active_cost = active_cost - existing_cost
|
| 64 |
+
if active_cost < 0:
|
| 65 |
+
active_cost = 0
|
| 66 |
+
|
| 67 |
+
# Check affordability
|
| 68 |
+
if active_cost <= available_energy:
|
| 69 |
+
action_idx = 1 + (i * 3) + area
|
| 70 |
+
if action_idx < 1000:
|
| 71 |
+
mask[action_idx] = 1
|
engine/game/player_state.py
ADDED
|
@@ -0,0 +1,827 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Dict, List
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
from engine.game.state_utils import StateMixin
|
| 6 |
+
from engine.models.ability import Condition, ConditionType, EffectType, TargetType, TriggerType
|
| 7 |
+
from engine.models.card import MemberCard
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class PlayerState(StateMixin):
|
| 11 |
+
"""
|
| 12 |
+
Player state (Rule 3)
|
| 13 |
+
Contains areas, zones, and tracking for a single player.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
__slots__ = (
|
| 17 |
+
"player_id",
|
| 18 |
+
"hand",
|
| 19 |
+
"main_deck",
|
| 20 |
+
"energy_deck",
|
| 21 |
+
"discard",
|
| 22 |
+
"energy_zone",
|
| 23 |
+
"success_lives",
|
| 24 |
+
"live_zone",
|
| 25 |
+
"live_zone_revealed",
|
| 26 |
+
"stage",
|
| 27 |
+
"stage_energy_vec",
|
| 28 |
+
"stage_energy_count",
|
| 29 |
+
"tapped_energy",
|
| 30 |
+
"tapped_members",
|
| 31 |
+
"members_played_this_turn",
|
| 32 |
+
"mulligan_selection",
|
| 33 |
+
"baton_touch_limit",
|
| 34 |
+
"baton_touch_count",
|
| 35 |
+
"negate_next_effect",
|
| 36 |
+
"restrictions",
|
| 37 |
+
"live_score_bonus",
|
| 38 |
+
"passed_lives",
|
| 39 |
+
"cannot_live",
|
| 40 |
+
"used_abilities",
|
| 41 |
+
"meta_rules",
|
| 42 |
+
"continuous_effects",
|
| 43 |
+
"continuous_effects_vec",
|
| 44 |
+
"continuous_effects_ptr",
|
| 45 |
+
"hand_buffer",
|
| 46 |
+
"moved_members_this_turn",
|
| 47 |
+
"hand_added_turn",
|
| 48 |
+
"deck_refreshed_this_turn",
|
| 49 |
+
"performance_abilities_processed",
|
| 50 |
+
"rested_members",
|
| 51 |
+
"revealed_hand",
|
| 52 |
+
"yell_score_count",
|
| 53 |
+
"fast_mode",
|
| 54 |
+
"members_tapped_by_opponent_this_turn",
|
| 55 |
+
"live_cards_set_this_turn",
|
| 56 |
+
"live_success_triggered",
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
def __init__(self, player_id: int):
|
| 60 |
+
self.player_id = player_id
|
| 61 |
+
self.hand: List[int] = []
|
| 62 |
+
self.hand_added_turn: List[int] = []
|
| 63 |
+
self.main_deck: List[int] = []
|
| 64 |
+
self.energy_deck: List[int] = []
|
| 65 |
+
self.discard: List[int] = []
|
| 66 |
+
self.energy_zone: List[int] = []
|
| 67 |
+
self.success_lives: List[int] = []
|
| 68 |
+
self.live_zone: List[int] = []
|
| 69 |
+
self.live_zone_revealed: List[bool] = []
|
| 70 |
+
self.stage: np.ndarray = np.full(3, -1, dtype=np.int32)
|
| 71 |
+
self.stage_energy_vec: np.ndarray = np.zeros((3, 32), dtype=np.int32)
|
| 72 |
+
self.stage_energy_count: np.ndarray = np.zeros(3, dtype=np.int32)
|
| 73 |
+
self.tapped_energy: np.ndarray = np.zeros(100, dtype=bool)
|
| 74 |
+
self.tapped_members: np.ndarray = np.zeros(3, dtype=bool)
|
| 75 |
+
self.members_played_this_turn: np.ndarray = np.zeros(3, dtype=bool)
|
| 76 |
+
self.mulligan_selection: set = set()
|
| 77 |
+
self.baton_touch_limit: int = 1
|
| 78 |
+
self.baton_touch_count: int = 0
|
| 79 |
+
self.negate_next_effect: bool = False
|
| 80 |
+
self.restrictions: set[str] = set()
|
| 81 |
+
self.live_score_bonus: int = 0
|
| 82 |
+
self.passed_lives: List[int] = []
|
| 83 |
+
self.cannot_live: bool = False
|
| 84 |
+
self.used_abilities: set[str] = set()
|
| 85 |
+
self.moved_members_this_turn: set[int] = set()
|
| 86 |
+
self.continuous_effects: List[Dict[str, Any]] = []
|
| 87 |
+
self.continuous_effects_vec: np.ndarray = np.zeros((32, 10), dtype=np.int32)
|
| 88 |
+
self.continuous_effects_ptr: int = 0
|
| 89 |
+
self.meta_rules: set[str] = set()
|
| 90 |
+
self.fast_mode: bool = False
|
| 91 |
+
self.hand_buffer: np.ndarray = np.zeros(100, dtype=np.int32)
|
| 92 |
+
self.deck_refreshed_this_turn: bool = False
|
| 93 |
+
self.performance_abilities_processed: bool = False
|
| 94 |
+
self.rested_members: np.ndarray = np.zeros(3, dtype=bool)
|
| 95 |
+
self.revealed_hand: bool = False
|
| 96 |
+
self.yell_score_count: int = 0
|
| 97 |
+
self.live_cards_set_this_turn: int = 0
|
| 98 |
+
self.live_success_triggered: bool = False
|
| 99 |
+
self.members_tapped_by_opponent_this_turn: set[int] = set()
|
| 100 |
+
|
| 101 |
+
@property
|
| 102 |
+
def score(self) -> int:
|
| 103 |
+
"""
|
| 104 |
+
Game Score (Rule 1.2 / 8.4.7)
|
| 105 |
+
- This is the number of cards in the success_lives zone.
|
| 106 |
+
- Points are obtained during Rule 8.4 Live Judgment phase.
|
| 107 |
+
- Only 1 success live card can be added per judgment turn.
|
| 108 |
+
"""
|
| 109 |
+
return len(self.success_lives)
|
| 110 |
+
|
| 111 |
+
@property
|
| 112 |
+
def energy_count(self) -> int:
|
| 113 |
+
return len(self.energy_zone)
|
| 114 |
+
|
| 115 |
+
@energy_count.setter
|
| 116 |
+
def energy_count(self, value: int):
|
| 117 |
+
current = len(self.energy_zone)
|
| 118 |
+
if value < current:
|
| 119 |
+
# Assume cost payment: Move from Energy Zone to Discard
|
| 120 |
+
diff = current - value
|
| 121 |
+
for _ in range(diff):
|
| 122 |
+
if self.energy_zone:
|
| 123 |
+
card_id = self.energy_zone.pop()
|
| 124 |
+
self.discard.append(card_id)
|
| 125 |
+
elif value > current:
|
| 126 |
+
# Cannot magically add empty energy without cards
|
| 127 |
+
pass
|
| 128 |
+
|
| 129 |
+
@property
|
| 130 |
+
def stage_energy(self) -> List[List[int]]:
|
| 131 |
+
"""Legacy compatibility property. Returns a copy of the energy state."""
|
| 132 |
+
res = []
|
| 133 |
+
for i in range(3):
|
| 134 |
+
count = self.stage_energy_count[i]
|
| 135 |
+
res.append(list(self.stage_energy_vec[i, :count]))
|
| 136 |
+
return res
|
| 137 |
+
|
| 138 |
+
def add_stage_energy(self, slot_idx: int, card_id: int) -> None:
|
| 139 |
+
"""Add energy to a slot using flat arrays."""
|
| 140 |
+
count = self.stage_energy_count[slot_idx]
|
| 141 |
+
if count < 32:
|
| 142 |
+
self.stage_energy_vec[slot_idx, count] = card_id
|
| 143 |
+
self.stage_energy_count[slot_idx] = count + 1
|
| 144 |
+
|
| 145 |
+
def clear_stage_energy(self, slot_idx: int) -> None:
|
| 146 |
+
"""Clear energy from a slot."""
|
| 147 |
+
self.stage_energy_count[slot_idx] = 0
|
| 148 |
+
|
| 149 |
+
def _reset(self, player_id: int) -> None:
|
| 150 |
+
"""Reset state for pool reuse."""
|
| 151 |
+
self.player_id = player_id
|
| 152 |
+
self.hand.clear()
|
| 153 |
+
self.main_deck.clear()
|
| 154 |
+
self.energy_deck.clear()
|
| 155 |
+
self.discard.clear()
|
| 156 |
+
self.energy_zone.clear()
|
| 157 |
+
self.success_lives.clear()
|
| 158 |
+
self.live_zone.clear()
|
| 159 |
+
self.live_zone_revealed.clear()
|
| 160 |
+
self.stage.fill(-1)
|
| 161 |
+
self.stage_energy_vec.fill(0)
|
| 162 |
+
self.stage_energy_count.fill(0)
|
| 163 |
+
self.tapped_energy.fill(False)
|
| 164 |
+
self.tapped_members.fill(False)
|
| 165 |
+
self.members_played_this_turn.fill(False)
|
| 166 |
+
self.mulligan_selection.clear()
|
| 167 |
+
self.baton_touch_limit = 1
|
| 168 |
+
self.baton_touch_count = 0
|
| 169 |
+
self.negate_next_effect = False
|
| 170 |
+
self.restrictions.clear()
|
| 171 |
+
self.live_score_bonus = 0
|
| 172 |
+
self.passed_lives.clear()
|
| 173 |
+
self.cannot_live = False
|
| 174 |
+
self.used_abilities.clear()
|
| 175 |
+
self.continuous_effects.clear()
|
| 176 |
+
self.continuous_effects_vec.fill(0)
|
| 177 |
+
self.continuous_effects_ptr = 0
|
| 178 |
+
self.meta_rules.clear()
|
| 179 |
+
self.hand_added_turn.clear()
|
| 180 |
+
self.deck_refreshed_this_turn = False
|
| 181 |
+
self.performance_abilities_processed = False
|
| 182 |
+
self.rested_members.fill(False)
|
| 183 |
+
self.revealed_hand = False
|
| 184 |
+
self.revealed_hand = False
|
| 185 |
+
self.moved_members_this_turn.clear()
|
| 186 |
+
self.members_tapped_by_opponent_this_turn.clear()
|
| 187 |
+
self.live_cards_set_this_turn = 0
|
| 188 |
+
self.live_success_triggered = False
|
| 189 |
+
|
| 190 |
+
def copy_slots_to(self, target: "PlayerState") -> None:
|
| 191 |
+
"""Hardcoded field copy for maximum performance."""
|
| 192 |
+
target.player_id = self.player_id
|
| 193 |
+
target.hand = self.hand[:]
|
| 194 |
+
target.hand_added_turn = self.hand_added_turn[:]
|
| 195 |
+
target.main_deck = self.main_deck[:]
|
| 196 |
+
target.energy_deck = self.energy_deck[:]
|
| 197 |
+
target.discard = self.discard[:]
|
| 198 |
+
target.energy_zone = self.energy_zone[:]
|
| 199 |
+
target.success_lives = self.success_lives[:]
|
| 200 |
+
target.live_zone = self.live_zone[:]
|
| 201 |
+
target.live_zone_revealed = self.live_zone_revealed[:]
|
| 202 |
+
target.baton_touch_limit = self.baton_touch_limit
|
| 203 |
+
target.baton_touch_count = self.baton_touch_count
|
| 204 |
+
target.negate_next_effect = self.negate_next_effect
|
| 205 |
+
target.live_score_bonus = self.live_score_bonus
|
| 206 |
+
target.cannot_live = self.cannot_live
|
| 207 |
+
target.deck_refreshed_this_turn = self.deck_refreshed_this_turn
|
| 208 |
+
target.performance_abilities_processed = self.performance_abilities_processed
|
| 209 |
+
target.revealed_hand = self.revealed_hand
|
| 210 |
+
target.continuous_effects_ptr = self.continuous_effects_ptr
|
| 211 |
+
target.live_cards_set_this_turn = self.live_cards_set_this_turn
|
| 212 |
+
target.live_success_triggered = self.live_success_triggered
|
| 213 |
+
|
| 214 |
+
def copy(self) -> "PlayerState":
|
| 215 |
+
new = PlayerState(self.player_id)
|
| 216 |
+
self.copy_to(new)
|
| 217 |
+
return new
|
| 218 |
+
|
| 219 |
+
def copy_to(self, new: "PlayerState") -> None:
|
| 220 |
+
# 1. Scalar/List fields
|
| 221 |
+
self.copy_slots_to(new)
|
| 222 |
+
# 2. NumPy arrays (memcpy speed)
|
| 223 |
+
np.copyto(new.stage, self.stage)
|
| 224 |
+
np.copyto(new.stage_energy_vec, self.stage_energy_vec)
|
| 225 |
+
np.copyto(new.stage_energy_count, self.stage_energy_count)
|
| 226 |
+
np.copyto(new.tapped_energy, self.tapped_energy)
|
| 227 |
+
np.copyto(new.tapped_members, self.tapped_members)
|
| 228 |
+
np.copyto(new.rested_members, self.rested_members)
|
| 229 |
+
np.copyto(new.continuous_effects_vec, self.continuous_effects_vec)
|
| 230 |
+
|
| 231 |
+
# 3. Sets and complex structures (slowest)
|
| 232 |
+
np.copyto(new.members_played_this_turn, self.members_played_this_turn)
|
| 233 |
+
new.used_abilities = set(self.used_abilities)
|
| 234 |
+
new.restrictions = set(self.restrictions)
|
| 235 |
+
new.mulligan_selection = set(self.mulligan_selection)
|
| 236 |
+
new.meta_rules = set(self.meta_rules)
|
| 237 |
+
new.meta_rules = set(self.meta_rules)
|
| 238 |
+
new.moved_members_this_turn = set(self.moved_members_this_turn)
|
| 239 |
+
new.members_tapped_by_opponent_this_turn = set(self.members_tapped_by_opponent_this_turn)
|
| 240 |
+
new.passed_lives = list(self.passed_lives)
|
| 241 |
+
|
| 242 |
+
# Legacy continuous_effects (only copy if needed or for AI skip)
|
| 243 |
+
if hasattr(self, "fast_mode") and self.fast_mode:
|
| 244 |
+
new.continuous_effects = []
|
| 245 |
+
else:
|
| 246 |
+
new.continuous_effects = [dict(e) for e in self.continuous_effects]
|
| 247 |
+
|
| 248 |
+
def untap_all(self) -> None:
|
| 249 |
+
self.tapped_energy[:] = False
|
| 250 |
+
self.tapped_members[:] = False
|
| 251 |
+
self.live_cards_set_this_turn = 0
|
| 252 |
+
|
| 253 |
+
def count_untapped_energy(self) -> int:
|
| 254 |
+
return int(np.count_nonzero(~self.tapped_energy[: len(self.energy_zone)]))
|
| 255 |
+
|
| 256 |
+
return breakdown
|
| 257 |
+
|
| 258 |
+
def get_blades_breakdown(self, slot_idx: int, card_db: Dict[int, MemberCard]) -> List[Dict[str, Any]]:
|
| 259 |
+
"""Calculate blades breakdown for a slot (Rule 9.9)."""
|
| 260 |
+
card_id = self.stage[slot_idx]
|
| 261 |
+
if card_id < 0:
|
| 262 |
+
return [{"source": f"Slot {slot_idx + 1}", "value": 0, "type": "empty", "source_id": -1}]
|
| 263 |
+
|
| 264 |
+
# Check if member is tapped (inactive)
|
| 265 |
+
if self.tapped_members[slot_idx]:
|
| 266 |
+
from engine.game.state_utils import get_base_id
|
| 267 |
+
|
| 268 |
+
base_id = get_base_id(int(card_id))
|
| 269 |
+
name = card_db[base_id].name if base_id in card_db else "Unknown"
|
| 270 |
+
return [{"source": f"{name} (Resting)", "value": 0, "type": "inactive", "source_id": int(card_id)}]
|
| 271 |
+
|
| 272 |
+
from engine.game.state_utils import get_base_id
|
| 273 |
+
|
| 274 |
+
base_id = get_base_id(int(card_id))
|
| 275 |
+
if base_id not in card_db:
|
| 276 |
+
return [{"source": "Unknown Card", "value": 0, "type": "error"}]
|
| 277 |
+
|
| 278 |
+
member = card_db[base_id]
|
| 279 |
+
breakdown = [{"source": member.name, "value": int(member.blades), "type": "base", "source_id": int(card_id)}]
|
| 280 |
+
|
| 281 |
+
# Collect effects
|
| 282 |
+
applied_effects = [] # List of (source_name, effect)
|
| 283 |
+
for ce in self.continuous_effects:
|
| 284 |
+
# ONLY include effects targeting this specific slot.
|
| 285 |
+
# Global effects (target_slot == -1) are handled at the Player level to avoid overcounting.
|
| 286 |
+
if ce.get("target_slot") == slot_idx:
|
| 287 |
+
src = ce.get("source_name", "Effect")
|
| 288 |
+
if "condition_text" in ce:
|
| 289 |
+
src += f" ({ce['condition_text']})"
|
| 290 |
+
applied_effects.append((src, ce["effect"]))
|
| 291 |
+
|
| 292 |
+
for ab in member.abilities:
|
| 293 |
+
if ab.trigger == TriggerType.CONSTANT:
|
| 294 |
+
if all(self._check_condition_for_constant(ab_cond, slot_idx, card_db) for ab_cond in ab.conditions):
|
| 295 |
+
for eff in ab.effects:
|
| 296 |
+
# Construct a helpful source string
|
| 297 |
+
src = member.name
|
| 298 |
+
if ab.conditions:
|
| 299 |
+
cond_texts = []
|
| 300 |
+
for c in ab.conditions:
|
| 301 |
+
if c.type == ConditionType.TURN_1:
|
| 302 |
+
cond_texts.append("Turn 1")
|
| 303 |
+
elif c.type == ConditionType.COUNT_STAGE:
|
| 304 |
+
cond_texts.append(f"Stage {c.params.get('value', 0)}+")
|
| 305 |
+
elif c.type == ConditionType.COUNT_HAND:
|
| 306 |
+
cond_texts.append(f"Hand {c.params.get('value', 0)}+")
|
| 307 |
+
elif c.type == ConditionType.LIFE_LEAD:
|
| 308 |
+
cond_texts.append("Life Lead")
|
| 309 |
+
else:
|
| 310 |
+
cond_texts.append("Cond")
|
| 311 |
+
src += f" ({', '.join(cond_texts)})"
|
| 312 |
+
else:
|
| 313 |
+
src += " (Constant)"
|
| 314 |
+
applied_effects.append((src, eff))
|
| 315 |
+
|
| 316 |
+
# Layer 4: SET
|
| 317 |
+
for source, eff in applied_effects:
|
| 318 |
+
if eff.effect_type == EffectType.SET_BLADES:
|
| 319 |
+
breakdown = [
|
| 320 |
+
{"source": source, "value": int(eff.value), "type": "set", "source_id": ce.get("source_id", -1)}
|
| 321 |
+
]
|
| 322 |
+
|
| 323 |
+
# Layer 4: ADD / BUFF
|
| 324 |
+
for source, eff in applied_effects:
|
| 325 |
+
if eff.effect_type in (EffectType.ADD_BLADES, EffectType.BUFF_POWER):
|
| 326 |
+
val = eff.value
|
| 327 |
+
val_desc = ""
|
| 328 |
+
if eff.params.get("multiplier"):
|
| 329 |
+
if eff.params.get("per_live"):
|
| 330 |
+
val *= len(self.success_lives)
|
| 331 |
+
val_desc = f" ({len(self.success_lives)} Lives)"
|
| 332 |
+
elif eff.params.get("per_energy"):
|
| 333 |
+
val *= len(self.energy_zone)
|
| 334 |
+
val_desc = f" ({len(self.energy_zone)} Energy)"
|
| 335 |
+
elif eff.params.get("per_member"):
|
| 336 |
+
val *= np.sum(self.stage >= 0)
|
| 337 |
+
val_desc = f" ({np.sum(self.stage >= 0)} Members)"
|
| 338 |
+
|
| 339 |
+
final_source = source + val_desc
|
| 340 |
+
breakdown.append(
|
| 341 |
+
{
|
| 342 |
+
"source": final_source,
|
| 343 |
+
"value": int(val),
|
| 344 |
+
"type": "mod",
|
| 345 |
+
"source_id": ce.get("source_id", -1) if "ce" in locals() else int(card_id),
|
| 346 |
+
}
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
return breakdown
|
| 350 |
+
|
| 351 |
+
def get_global_blades_breakdown(self) -> List[Dict[str, Any]]:
|
| 352 |
+
"""Calculate breakdown for global (player-wide) blade effects."""
|
| 353 |
+
breakdown = []
|
| 354 |
+
applied_effects = []
|
| 355 |
+
for ce in self.continuous_effects:
|
| 356 |
+
if ce.get("target_slot") == -1:
|
| 357 |
+
src = ce.get("source_name", "Effect")
|
| 358 |
+
if "condition_text" in ce:
|
| 359 |
+
src += f" ({ce['condition_text']})"
|
| 360 |
+
applied_effects.append((ce, src, ce["effect"]))
|
| 361 |
+
|
| 362 |
+
for ce, source, eff in applied_effects:
|
| 363 |
+
if eff.effect_type in (EffectType.ADD_BLADES, EffectType.BUFF_POWER):
|
| 364 |
+
val = eff.value
|
| 365 |
+
val_desc = ""
|
| 366 |
+
if eff.params.get("multiplier"):
|
| 367 |
+
if eff.params.get("per_live"):
|
| 368 |
+
val *= len(self.success_lives)
|
| 369 |
+
val_desc = f" ({len(self.success_lives)} Lives)"
|
| 370 |
+
elif eff.params.get("per_energy"):
|
| 371 |
+
val *= len(self.energy_zone)
|
| 372 |
+
val_desc = f" ({len(self.energy_zone)} Energy)"
|
| 373 |
+
elif eff.params.get("per_member"):
|
| 374 |
+
val *= np.sum(self.stage >= 0)
|
| 375 |
+
val_desc = f" ({np.sum(self.stage >= 0)} Members)"
|
| 376 |
+
|
| 377 |
+
final_source = source + val_desc
|
| 378 |
+
breakdown.append(
|
| 379 |
+
{
|
| 380 |
+
"source": final_source,
|
| 381 |
+
"value": int(val),
|
| 382 |
+
"type": "mod",
|
| 383 |
+
"source_id": ce.get("source_id", -1),
|
| 384 |
+
}
|
| 385 |
+
)
|
| 386 |
+
return breakdown
|
| 387 |
+
|
| 388 |
+
def get_effective_blades(self, slot_idx: int, card_db: Dict[int, MemberCard]) -> int:
|
| 389 |
+
breakdown = self.get_blades_breakdown(slot_idx, card_db)
|
| 390 |
+
total = sum(item["value"] for item in breakdown)
|
| 391 |
+
return max(0, total)
|
| 392 |
+
|
| 393 |
+
def _check_condition_for_constant(
|
| 394 |
+
self, cond: Condition, slot_idx: int, card_db: Dict[int, MemberCard] = None
|
| 395 |
+
) -> bool:
|
| 396 |
+
"""
|
| 397 |
+
Check if a condition is met for a constant ability.
|
| 398 |
+
slot_idx < 0 implies the card is not on stage (e.g. in hand for cost reduction).
|
| 399 |
+
"""
|
| 400 |
+
if cond.type == ConditionType.NONE:
|
| 401 |
+
return True
|
| 402 |
+
|
| 403 |
+
# Conditions that require being on stage
|
| 404 |
+
if slot_idx < 0:
|
| 405 |
+
if cond.type in (ConditionType.HAS_MOVED, ConditionType.IS_CENTER, ConditionType.GROUP_FILTER):
|
| 406 |
+
# For GROUP_FILTER, if it's checking SELF, we might need the card ID context which is not passed here properly.
|
| 407 |
+
# But for cost reduction, usually it's just Hand/Stage counts.
|
| 408 |
+
return False
|
| 409 |
+
|
| 410 |
+
if cond.type == ConditionType.HAS_MOVED:
|
| 411 |
+
# Check if this card moved this turn.
|
| 412 |
+
current_card_id = self.stage[slot_idx]
|
| 413 |
+
if current_card_id >= 0:
|
| 414 |
+
return current_card_id in self.moved_members_this_turn
|
| 415 |
+
return False
|
| 416 |
+
elif cond.type == ConditionType.TURN_1:
|
| 417 |
+
# This would require access to game state to check turn number
|
| 418 |
+
# For now, return True as a placeholder
|
| 419 |
+
return True
|
| 420 |
+
elif cond.type == ConditionType.IS_CENTER:
|
| 421 |
+
# Check if the slot is the center position (index 1 in 3-slot system)
|
| 422 |
+
return slot_idx == 1
|
| 423 |
+
elif cond.type == ConditionType.GROUP_FILTER:
|
| 424 |
+
# Check if the member belongs to the specified group
|
| 425 |
+
current_card_id = self.stage[slot_idx]
|
| 426 |
+
if current_card_id >= 0 and card_db:
|
| 427 |
+
from engine.game.state_utils import get_base_id
|
| 428 |
+
|
| 429 |
+
base_id = get_base_id(int(current_card_id))
|
| 430 |
+
if base_id in card_db:
|
| 431 |
+
member = card_db[base_id]
|
| 432 |
+
group_name = cond.params.get("group", "")
|
| 433 |
+
# This would need to compare member's group with the condition's group
|
| 434 |
+
# For now, return True as a placeholder
|
| 435 |
+
return True
|
| 436 |
+
return False
|
| 437 |
+
elif cond.type == ConditionType.COUNT_GROUP:
|
| 438 |
+
# Count members of a specific group in the stage
|
| 439 |
+
group_name = cond.params.get("group", "")
|
| 440 |
+
min_count = cond.params.get("min", 1)
|
| 441 |
+
zone = cond.params.get("zone", "STAGE")
|
| 442 |
+
|
| 443 |
+
count = 0
|
| 444 |
+
if zone == "STAGE" or zone == "OPPONENT_STAGE":
|
| 445 |
+
from engine.game.state_utils import get_base_id
|
| 446 |
+
|
| 447 |
+
for i in range(3):
|
| 448 |
+
card_id = self.stage[i]
|
| 449 |
+
if card_id >= 0 and card_db:
|
| 450 |
+
base_id = get_base_id(int(card_id))
|
| 451 |
+
if base_id in card_db:
|
| 452 |
+
member = card_db[base_id]
|
| 453 |
+
# Compare member's group with the condition's group
|
| 454 |
+
# For now, return True as a placeholder
|
| 455 |
+
count += 1
|
| 456 |
+
|
| 457 |
+
return count >= min_count
|
| 458 |
+
elif cond.type == ConditionType.OPPONENT_HAS:
|
| 459 |
+
# Placeholder for opponent has condition
|
| 460 |
+
return True
|
| 461 |
+
elif cond.type == ConditionType.COUNT_ENERGY:
|
| 462 |
+
min_energy = cond.params.get("min", 1)
|
| 463 |
+
return len(self.energy_zone) >= min_energy
|
| 464 |
+
else:
|
| 465 |
+
# Default lenient for other conditions
|
| 466 |
+
return True
|
| 467 |
+
|
| 468 |
+
def get_hearts_breakdown(self, slot_idx: int, card_db: Dict[int, MemberCard]) -> List[Dict[str, Any]]:
|
| 469 |
+
"""Calculate hearts breakdown for a slot, including continuous effects."""
|
| 470 |
+
card_id = self.stage[slot_idx]
|
| 471 |
+
if card_id < 0:
|
| 472 |
+
return [{"source": f"Slot {slot_idx + 1}", "value": [0] * 7, "type": "empty", "source_id": -1}]
|
| 473 |
+
|
| 474 |
+
# Check if member is tapped (inactive)
|
| 475 |
+
if self.tapped_members[slot_idx]:
|
| 476 |
+
from engine.game.state_utils import get_base_id
|
| 477 |
+
|
| 478 |
+
base_id = get_base_id(int(card_id))
|
| 479 |
+
name = card_db[base_id].name if base_id in card_db else "Unknown"
|
| 480 |
+
return [{"source": f"{name} (Resting)", "value": [0] * 7, "type": "inactive", "source_id": int(card_id)}]
|
| 481 |
+
|
| 482 |
+
from engine.game.state_utils import get_base_id
|
| 483 |
+
|
| 484 |
+
base_id = get_base_id(int(card_id))
|
| 485 |
+
if base_id not in card_db:
|
| 486 |
+
return [{"source": "Unknown Card", "value": [0] * 7, "type": "error"}]
|
| 487 |
+
|
| 488 |
+
member = card_db[base_id]
|
| 489 |
+
|
| 490 |
+
# Ensure base hearts are 7-dim
|
| 491 |
+
base_hearts = np.zeros(7, dtype=np.int32)
|
| 492 |
+
base_hearts[: len(member.hearts)] = member.hearts
|
| 493 |
+
|
| 494 |
+
breakdown = [{"source": member.name, "value": base_hearts.tolist(), "type": "base", "source_id": int(card_id)}]
|
| 495 |
+
|
| 496 |
+
# Collect effects
|
| 497 |
+
applied_effects = []
|
| 498 |
+
for ce in self.continuous_effects:
|
| 499 |
+
if ce.get("target_slot") in (-1, slot_idx):
|
| 500 |
+
src = ce.get("source_name", "Effect")
|
| 501 |
+
if "condition_text" in ce:
|
| 502 |
+
src += f" ({ce['condition_text']})"
|
| 503 |
+
applied_effects.append((src, ce["effect"]))
|
| 504 |
+
|
| 505 |
+
for ab in member.abilities:
|
| 506 |
+
if ab.trigger == TriggerType.CONSTANT:
|
| 507 |
+
if all(self._check_condition_for_constant(ab_cond, slot_idx, card_db) for ab_cond in ab.conditions):
|
| 508 |
+
for eff in ab.effects:
|
| 509 |
+
# Construct a helpful source string
|
| 510 |
+
src = member.name
|
| 511 |
+
if ab.conditions:
|
| 512 |
+
cond_texts = []
|
| 513 |
+
for c in ab.conditions:
|
| 514 |
+
if c.type == ConditionType.TURN_1:
|
| 515 |
+
cond_texts.append("Turn 1")
|
| 516 |
+
elif c.type == ConditionType.COUNT_STAGE:
|
| 517 |
+
cond_texts.append(f"Stage {c.params.get('value', 0)}+")
|
| 518 |
+
elif c.type == ConditionType.COUNT_HAND:
|
| 519 |
+
cond_texts.append(f"Hand {c.params.get('value', 0)}+")
|
| 520 |
+
elif c.type == ConditionType.LIFE_LEAD:
|
| 521 |
+
cond_texts.append("Life Lead")
|
| 522 |
+
elif c.type == ConditionType.HAS_MEMBER:
|
| 523 |
+
cond_texts.append("Has Member")
|
| 524 |
+
elif c.type == ConditionType.HAS_COLOR:
|
| 525 |
+
cond_texts.append("Has Color")
|
| 526 |
+
else:
|
| 527 |
+
cond_texts.append("Cond")
|
| 528 |
+
src += f" ({', '.join(cond_texts)})"
|
| 529 |
+
else:
|
| 530 |
+
src += " (Constant)"
|
| 531 |
+
applied_effects.append((src, eff))
|
| 532 |
+
|
| 533 |
+
# Apply Heart Modifications
|
| 534 |
+
for source, eff in applied_effects:
|
| 535 |
+
eff_val = np.zeros(7, dtype=np.int32)
|
| 536 |
+
|
| 537 |
+
if eff.effect_type == EffectType.SET_HEARTS:
|
| 538 |
+
breakdown = [
|
| 539 |
+
{
|
| 540 |
+
"source": source,
|
| 541 |
+
"value": [int(eff.value)] * 7,
|
| 542 |
+
"type": "set",
|
| 543 |
+
"source_id": ce.get("source_id", -1),
|
| 544 |
+
}
|
| 545 |
+
]
|
| 546 |
+
# Reset others? For SET, usually yes.
|
| 547 |
+
continue
|
| 548 |
+
|
| 549 |
+
if eff.effect_type == EffectType.ADD_HEARTS:
|
| 550 |
+
color_map = {1: 0, 2: 1, 3: 2, 4: 3, 5: 4, 6: 5} # P,R,Y,G,B,P
|
| 551 |
+
target_colors = []
|
| 552 |
+
if eff.params.get("color"):
|
| 553 |
+
c = eff.params["color"]
|
| 554 |
+
if c in color_map:
|
| 555 |
+
target_colors.append(color_map[c])
|
| 556 |
+
elif eff.params.get("all"):
|
| 557 |
+
target_colors = list(range(6))
|
| 558 |
+
|
| 559 |
+
amount = eff.value
|
| 560 |
+
# Multipliers
|
| 561 |
+
if eff.params.get("multiplier"):
|
| 562 |
+
if eff.params.get("per_live"):
|
| 563 |
+
amount *= len(self.success_lives)
|
| 564 |
+
elif eff.params.get("per_energy"):
|
| 565 |
+
amount *= len(self.energy_zone)
|
| 566 |
+
|
| 567 |
+
for c_idx in target_colors:
|
| 568 |
+
eff_val[c_idx] += amount
|
| 569 |
+
|
| 570 |
+
if not target_colors:
|
| 571 |
+
pass
|
| 572 |
+
|
| 573 |
+
# Only append if non-zero
|
| 574 |
+
if np.any(eff_val):
|
| 575 |
+
breakdown.append(
|
| 576 |
+
{
|
| 577 |
+
"source": source,
|
| 578 |
+
"value": eff_val.tolist(),
|
| 579 |
+
"type": "mod",
|
| 580 |
+
"source_id": ce.get("source_id", -1) if "ce" in locals() else int(card_id),
|
| 581 |
+
}
|
| 582 |
+
)
|
| 583 |
+
|
| 584 |
+
return breakdown
|
| 585 |
+
|
| 586 |
+
def get_effective_hearts(self, slot_idx: int, card_db: Dict[int, MemberCard]) -> np.ndarray:
|
| 587 |
+
breakdown = self.get_hearts_breakdown(slot_idx, card_db)
|
| 588 |
+
total = np.zeros(7, dtype=np.int32)
|
| 589 |
+
for item in breakdown:
|
| 590 |
+
total += np.array(item["value"], dtype=np.int32)
|
| 591 |
+
return np.maximum(0, total)
|
| 592 |
+
|
| 593 |
+
def get_total_blades(self, card_db: Dict[int, MemberCard]) -> int:
|
| 594 |
+
total = 0
|
| 595 |
+
# 1. Base + Slot-specific modifiers
|
| 596 |
+
for i, card_id in enumerate(self.stage):
|
| 597 |
+
if card_id >= 0 and not self.tapped_members[i]:
|
| 598 |
+
total += self.get_effective_blades(i, card_db)
|
| 599 |
+
|
| 600 |
+
# 2. Global modifiers
|
| 601 |
+
global_mods = self.get_global_blades_breakdown()
|
| 602 |
+
for mod in global_mods:
|
| 603 |
+
total += mod["value"]
|
| 604 |
+
|
| 605 |
+
return max(0, total)
|
| 606 |
+
|
| 607 |
+
def get_total_hearts(self, card_db: Dict[int, Any]) -> np.ndarray:
|
| 608 |
+
total = np.zeros(7, dtype=np.int32)
|
| 609 |
+
for i, card_id in enumerate(self.stage):
|
| 610 |
+
if card_id >= 0 and not self.tapped_members[i]:
|
| 611 |
+
total += self.get_effective_hearts(i, card_db)
|
| 612 |
+
return total
|
| 613 |
+
|
| 614 |
+
def get_performance_guide(self, live_db: Dict[int, Any], member_db: Dict[int, Any]) -> Dict[str, Any]:
|
| 615 |
+
"""
|
| 616 |
+
Calculate projected performance outcome for the user guide.
|
| 617 |
+
Now comprehensive: includes breakdown for all slots (active, resting, empty)
|
| 618 |
+
and requirement modifications.
|
| 619 |
+
"""
|
| 620 |
+
if not self.live_zone:
|
| 621 |
+
return {"can_perform": False, "reason": "No live cards"}
|
| 622 |
+
|
| 623 |
+
from engine.game.state_utils import get_base_id
|
| 624 |
+
|
| 625 |
+
# 1. Total Blades & Blade Breakdown
|
| 626 |
+
total_blades = 0
|
| 627 |
+
blade_breakdown = []
|
| 628 |
+
# Always iterate 0-2 to show all slots
|
| 629 |
+
for i in range(3):
|
| 630 |
+
# Breakdown method handles empty/inactive cases now
|
| 631 |
+
bd = self.get_blades_breakdown(i, member_db)
|
| 632 |
+
blade_breakdown.extend(bd)
|
| 633 |
+
if self.stage[i] >= 0 and not self.tapped_members[i]:
|
| 634 |
+
# Sum up effective blades from breakdown for Active members
|
| 635 |
+
slot_total = sum(item["value"] for item in bd if item.get("type") in ("base", "mod", "set"))
|
| 636 |
+
total_blades += max(0, slot_total)
|
| 637 |
+
|
| 638 |
+
# Apply cheer_mod
|
| 639 |
+
extra_reveals = sum(
|
| 640 |
+
ce["effect"].value
|
| 641 |
+
for ce in self.continuous_effects
|
| 642 |
+
if ce["effect"].effect_type == EffectType.META_RULE and ce["effect"].params.get("type") == "cheer_mod"
|
| 643 |
+
)
|
| 644 |
+
total_blades = max(0, total_blades + extra_reveals)
|
| 645 |
+
|
| 646 |
+
# 2. Total Hearts & Heart Breakdown
|
| 647 |
+
total_hearts = np.zeros(7, dtype=np.int32)
|
| 648 |
+
heart_breakdown = []
|
| 649 |
+
for i in range(3):
|
| 650 |
+
bd = self.get_hearts_breakdown(i, member_db)
|
| 651 |
+
heart_breakdown.extend(bd)
|
| 652 |
+
if self.stage[i] >= 0 and not self.tapped_members[i]:
|
| 653 |
+
# Sum up effective hearts from breakdown for Active members
|
| 654 |
+
for item in bd:
|
| 655 |
+
if item.get("type") in ("base", "mod", "set"):
|
| 656 |
+
total_hearts += np.array(item["value"], dtype=np.int32)
|
| 657 |
+
|
| 658 |
+
# 3. Apply TRANSFORM_COLOR (Global)
|
| 659 |
+
transform_log = []
|
| 660 |
+
for ce in self.continuous_effects:
|
| 661 |
+
if ce["effect"].effect_type == EffectType.TRANSFORM_COLOR:
|
| 662 |
+
eff = ce["effect"]
|
| 663 |
+
src_color = eff.params.get("from_color", eff.params.get("color")) # 1-based
|
| 664 |
+
dest_color = eff.params.get("to_color") # 1-based
|
| 665 |
+
if src_color and dest_color:
|
| 666 |
+
try:
|
| 667 |
+
# Handle possibly float/string values
|
| 668 |
+
s_idx = int(src_color) - 1
|
| 669 |
+
d_idx = int(dest_color) - 1
|
| 670 |
+
if 0 <= s_idx < 6 and 0 <= d_idx < 6:
|
| 671 |
+
amount_moved = total_hearts[s_idx]
|
| 672 |
+
total_hearts[d_idx] += amount_moved
|
| 673 |
+
total_hearts[s_idx] = 0
|
| 674 |
+
transform_log.append(
|
| 675 |
+
{
|
| 676 |
+
"source": ce.get("source_name", "Effect"),
|
| 677 |
+
"desc": f"Color Transform (Type {src_color} -> Type {dest_color})",
|
| 678 |
+
"type": "transform",
|
| 679 |
+
"source_id": ce.get("source_id", -1),
|
| 680 |
+
}
|
| 681 |
+
)
|
| 682 |
+
except:
|
| 683 |
+
pass
|
| 684 |
+
|
| 685 |
+
# 4. Process Lives & Requirements
|
| 686 |
+
lives = []
|
| 687 |
+
req_breakdown = [] # Log for requirement reductions
|
| 688 |
+
|
| 689 |
+
for live_id in self.live_zone:
|
| 690 |
+
l_base = get_base_id(live_id)
|
| 691 |
+
if l_base not in live_db:
|
| 692 |
+
continue
|
| 693 |
+
live_card = live_db[l_base]
|
| 694 |
+
|
| 695 |
+
# Base Requirement
|
| 696 |
+
req_breakdown.append(
|
| 697 |
+
{"source": live_card.name, "value": live_card.required_hearts.tolist(), "type": "base_req"}
|
| 698 |
+
)
|
| 699 |
+
|
| 700 |
+
# Copy requirement to modify
|
| 701 |
+
req = live_card.required_hearts.copy() # (7,)
|
| 702 |
+
|
| 703 |
+
# Apply REDUCE_HEART_REQ
|
| 704 |
+
for ce in self.continuous_effects:
|
| 705 |
+
eff = ce["effect"]
|
| 706 |
+
if eff.effect_type == EffectType.REDUCE_HEART_REQ:
|
| 707 |
+
reduction_val = np.zeros(7, dtype=np.int32)
|
| 708 |
+
target_color = eff.params.get("color")
|
| 709 |
+
val = eff.value
|
| 710 |
+
|
| 711 |
+
if target_color and target_color != "any":
|
| 712 |
+
try:
|
| 713 |
+
c_idx = int(target_color) - 1
|
| 714 |
+
if 0 <= c_idx < 6:
|
| 715 |
+
reduction_val[c_idx] = val
|
| 716 |
+
except:
|
| 717 |
+
pass
|
| 718 |
+
else:
|
| 719 |
+
# Any color reduction (index 6) matches "any" param or default
|
| 720 |
+
reduction_val[6] = val
|
| 721 |
+
|
| 722 |
+
# Log reduction
|
| 723 |
+
if np.any(reduction_val > 0):
|
| 724 |
+
req_breakdown.append(
|
| 725 |
+
{
|
| 726 |
+
"source": ce.get("source_name", "Effect"),
|
| 727 |
+
"value": (-reduction_val).tolist(),
|
| 728 |
+
"type": "req_mod",
|
| 729 |
+
"source_id": ce.get("source_id", -1),
|
| 730 |
+
}
|
| 731 |
+
)
|
| 732 |
+
req = np.maximum(0, req - reduction_val)
|
| 733 |
+
|
| 734 |
+
# Calculate Success (Greedy)
|
| 735 |
+
temp_hearts = total_hearts.copy()
|
| 736 |
+
|
| 737 |
+
# 1. Match specific colors
|
| 738 |
+
needed_specific = req[:6]
|
| 739 |
+
have_specific = temp_hearts[:6]
|
| 740 |
+
used_specific = np.minimum(needed_specific, have_specific)
|
| 741 |
+
|
| 742 |
+
temp_hearts[:6] -= used_specific
|
| 743 |
+
remaining_req = req.copy()
|
| 744 |
+
remaining_req[:6] -= used_specific
|
| 745 |
+
|
| 746 |
+
# 2. Match Any with remaining specific
|
| 747 |
+
needed_any = remaining_req[6]
|
| 748 |
+
have_any_from_specific = np.sum(temp_hearts[:6])
|
| 749 |
+
used_any_from_specific = min(needed_any, have_any_from_specific)
|
| 750 |
+
|
| 751 |
+
# 3. Match Any with Any
|
| 752 |
+
needed_any -= used_any_from_specific
|
| 753 |
+
have_wild = temp_hearts[6]
|
| 754 |
+
used_wild = min(needed_any, have_wild)
|
| 755 |
+
|
| 756 |
+
met = np.all(remaining_req[:6] == 0) and (needed_any - used_wild <= 0)
|
| 757 |
+
|
| 758 |
+
lives.append(
|
| 759 |
+
{
|
| 760 |
+
"name": live_card.name,
|
| 761 |
+
"img": live_card.img_path,
|
| 762 |
+
"score": int(live_card.score),
|
| 763 |
+
"req": req.tolist(),
|
| 764 |
+
"passed": bool(met),
|
| 765 |
+
"reason": "" if met else "Not met",
|
| 766 |
+
"base_score": int(live_card.score),
|
| 767 |
+
"bonus_score": self.live_score_bonus,
|
| 768 |
+
}
|
| 769 |
+
)
|
| 770 |
+
|
| 771 |
+
return {
|
| 772 |
+
"can_perform": True,
|
| 773 |
+
"total_blades": int(total_blades),
|
| 774 |
+
"total_hearts": total_hearts.tolist(),
|
| 775 |
+
"lives": lives,
|
| 776 |
+
"breakdown": {
|
| 777 |
+
"blades": blade_breakdown,
|
| 778 |
+
"hearts": heart_breakdown,
|
| 779 |
+
"requirements": req_breakdown,
|
| 780 |
+
"transforms": transform_log,
|
| 781 |
+
},
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
def get_member_cost(self, card_id: int, card_db: Dict[int, MemberCard]) -> int:
|
| 785 |
+
"""
|
| 786 |
+
Calculate effective cost of a member card in hand.
|
| 787 |
+
"""
|
| 788 |
+
from engine.game.state_utils import get_base_id
|
| 789 |
+
|
| 790 |
+
base_id = get_base_id(card_id)
|
| 791 |
+
if base_id not in card_db:
|
| 792 |
+
return 0
|
| 793 |
+
|
| 794 |
+
member = card_db[base_id]
|
| 795 |
+
cost = member.cost
|
| 796 |
+
|
| 797 |
+
# Apply global cost reduction effects
|
| 798 |
+
total_reduction = 0
|
| 799 |
+
for ce in self.continuous_effects:
|
| 800 |
+
if ce["effect"].effect_type == EffectType.REDUCE_COST:
|
| 801 |
+
total_reduction += ce["effect"].value
|
| 802 |
+
|
| 803 |
+
# Q129: Apply card's OWN constant abilities if they reduce cost in hand.
|
| 804 |
+
for ab in member.abilities:
|
| 805 |
+
if ab.trigger == TriggerType.CONSTANT:
|
| 806 |
+
for eff in ab.effects:
|
| 807 |
+
if eff.effect_type == EffectType.REDUCE_COST and eff.target == TargetType.SELF:
|
| 808 |
+
conditions_met = True
|
| 809 |
+
for cond in ab.conditions:
|
| 810 |
+
if not self._check_condition_for_constant(cond, slot_idx=-1, card_db=card_db):
|
| 811 |
+
conditions_met = False
|
| 812 |
+
break
|
| 813 |
+
|
| 814 |
+
if conditions_met:
|
| 815 |
+
val = eff.value
|
| 816 |
+
if eff.params.get("multiplier") and eff.params.get("per_hand_other"):
|
| 817 |
+
count = max(0, len(self.hand) - 1)
|
| 818 |
+
val *= count
|
| 819 |
+
total_reduction += val
|
| 820 |
+
|
| 821 |
+
return max(0, cost - total_reduction)
|
| 822 |
+
|
| 823 |
+
def to_dict(self, viewer_idx=0):
|
| 824 |
+
# We now have StateMixin.to_dict() but we might want this custom one for the UI.
|
| 825 |
+
# Actually, let's just use StateMixin.to_dict and enrich it if needed in serializer.py.
|
| 826 |
+
# This keeps PlayerState purely about state.
|
| 827 |
+
return super().to_dict()
|
engine/game/replay_manager.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
from typing import Any, Dict, List, Optional
|
| 3 |
+
|
| 4 |
+
from engine.game.game_state import GameState
|
| 5 |
+
from engine.game.serializer import serialize_state
|
| 6 |
+
|
| 7 |
+
try:
|
| 8 |
+
from engine.game.state_utils import create_uid
|
| 9 |
+
except ImportError:
|
| 10 |
+
# Fallback if state_utils was deleted (it shouldn't have been, but just in case)
|
| 11 |
+
# Reimplement create_uid if needed, or fix the file location
|
| 12 |
+
BASE_ID_MASK = 0xFFFFF
|
| 13 |
+
INSTANCE_SHIFT = 20
|
| 14 |
+
|
| 15 |
+
def create_uid(base_id: int, instance_index: int) -> int:
|
| 16 |
+
return (base_id & BASE_ID_MASK) | (instance_index << INSTANCE_SHIFT)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def optimize_history(
|
| 20 |
+
history: List[Dict[str, Any]],
|
| 21 |
+
member_db: Dict[int, Any],
|
| 22 |
+
live_db: Dict[int, Any],
|
| 23 |
+
energy_db: Dict[int, Any],
|
| 24 |
+
exclude_db_cards: bool = True,
|
| 25 |
+
seed: Optional[int] = None,
|
| 26 |
+
action_log: Optional[List[int]] = None,
|
| 27 |
+
deck_info: Optional[Dict[str, Any]] = None,
|
| 28 |
+
) -> Dict[str, Any]:
|
| 29 |
+
"""
|
| 30 |
+
Optimize replay history.
|
| 31 |
+
Args:
|
| 32 |
+
history: List of states
|
| 33 |
+
member_db: Database of member cards
|
| 34 |
+
live_db: Database of live cards
|
| 35 |
+
energy_db: Database of energy cards
|
| 36 |
+
exclude_db_cards: Use DB-backed optimization (Level 2)
|
| 37 |
+
seed: Random seed (Level 3)
|
| 38 |
+
action_log: List of action IDs (Level 3)
|
| 39 |
+
deck_info: Dict with 'p0_deck', 'p1_deck', etc. (Level 3)
|
| 40 |
+
"""
|
| 41 |
+
# Level 3: Action-Based Replay (Max Compression)
|
| 42 |
+
if seed is not None and action_log is not None and deck_info is not None:
|
| 43 |
+
return {
|
| 44 |
+
"level": 3,
|
| 45 |
+
"seed": seed,
|
| 46 |
+
"decks": deck_info,
|
| 47 |
+
"action_log": action_log,
|
| 48 |
+
# We don't save 'states' or 'registry' at all!
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
# Level 2: State-Based DB-Backed
|
| 52 |
+
registry = {}
|
| 53 |
+
|
| 54 |
+
def extract_static_data(card_data):
|
| 55 |
+
"""Extract static fields that don't change during gameplay."""
|
| 56 |
+
if not isinstance(card_data, dict):
|
| 57 |
+
return {}
|
| 58 |
+
|
| 59 |
+
# known static fields
|
| 60 |
+
static_fields = [
|
| 61 |
+
"name",
|
| 62 |
+
"card_no",
|
| 63 |
+
"type",
|
| 64 |
+
"cost",
|
| 65 |
+
"blade",
|
| 66 |
+
"img",
|
| 67 |
+
"hearts",
|
| 68 |
+
"blade_hearts",
|
| 69 |
+
"text",
|
| 70 |
+
"score",
|
| 71 |
+
"required_hearts",
|
| 72 |
+
]
|
| 73 |
+
|
| 74 |
+
return {k: card_data[k] for k in static_fields if k in card_data}
|
| 75 |
+
|
| 76 |
+
def optimize_object(obj):
|
| 77 |
+
"""recursively traverse and optimize payload."""
|
| 78 |
+
if isinstance(obj, list):
|
| 79 |
+
return [optimize_object(x) for x in obj]
|
| 80 |
+
elif isinstance(obj, dict):
|
| 81 |
+
# Check if this object looks like a serialized card
|
| 82 |
+
if "id" in obj and ("name" in obj or "type" in obj):
|
| 83 |
+
cid = obj["id"]
|
| 84 |
+
# If it's a known card (positive ID), register it
|
| 85 |
+
if isinstance(cid, int) and cid >= 0:
|
| 86 |
+
is_in_db = cid in member_db or cid in live_db or cid in energy_db
|
| 87 |
+
|
| 88 |
+
# Decide whether to add to registry
|
| 89 |
+
should_register = False
|
| 90 |
+
if not is_in_db:
|
| 91 |
+
should_register = True
|
| 92 |
+
elif not exclude_db_cards:
|
| 93 |
+
should_register = True
|
| 94 |
+
|
| 95 |
+
if should_register:
|
| 96 |
+
if cid not in registry:
|
| 97 |
+
registry[cid] = extract_static_data(obj)
|
| 98 |
+
|
| 99 |
+
# Return ONLY dynamic data + ID reference
|
| 100 |
+
dynamic_data = {"id": cid}
|
| 101 |
+
static_keys = registry[cid].keys()
|
| 102 |
+
for k, v in obj.items():
|
| 103 |
+
if k not in static_keys and k != "id":
|
| 104 |
+
dynamic_data[k] = optimize_object(v)
|
| 105 |
+
return dynamic_data
|
| 106 |
+
|
| 107 |
+
elif is_in_db:
|
| 108 |
+
# IT IS IN DB and we exclude it from registry
|
| 109 |
+
# We still strip static data, but we don't save it to file
|
| 110 |
+
# effectively assuming "registry[cid]" exists implicitly in DB
|
| 111 |
+
|
| 112 |
+
# We need to know which keys are static to strip them
|
| 113 |
+
# We can use a representative static extraction
|
| 114 |
+
static_keys = extract_static_data(obj).keys()
|
| 115 |
+
|
| 116 |
+
dynamic_data = {"id": cid}
|
| 117 |
+
for k, v in obj.items():
|
| 118 |
+
if k not in static_keys and k != "id":
|
| 119 |
+
dynamic_data[k] = optimize_object(v)
|
| 120 |
+
return dynamic_data
|
| 121 |
+
|
| 122 |
+
# Regular dict recursion
|
| 123 |
+
return {k: optimize_object(v) for k, v in obj.items()}
|
| 124 |
+
else:
|
| 125 |
+
return obj
|
| 126 |
+
|
| 127 |
+
optimized_states = optimize_object(history)
|
| 128 |
+
|
| 129 |
+
return {"registry": registry, "states": optimized_states}
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def inflate_history(
|
| 133 |
+
optimized_data: Dict[str, Any],
|
| 134 |
+
member_db: Dict[int, Any],
|
| 135 |
+
live_db: Dict[int, Any],
|
| 136 |
+
energy_db: Dict[int, Any],
|
| 137 |
+
) -> List[Dict[str, Any]]:
|
| 138 |
+
"""
|
| 139 |
+
Reconstruct full history state from optimized data using server DB.
|
| 140 |
+
"""
|
| 141 |
+
# Level 3 Inflation (Action Log -> State History)
|
| 142 |
+
if optimized_data.get("level") == 3 or "action_log" in optimized_data:
|
| 143 |
+
print("Inflating Level 3 Action Log replay...")
|
| 144 |
+
action_log = optimized_data.get("action_log", [])
|
| 145 |
+
seed = optimized_data.get("seed", 0)
|
| 146 |
+
deck_info = optimized_data.get("decks", {})
|
| 147 |
+
|
| 148 |
+
# 1. Reset Game with Seed
|
| 149 |
+
# Use local random instance to avoid messing with global random state if possible,
|
| 150 |
+
# but GameState uses random module globally.
|
| 151 |
+
# We must save and restore random state if we want to be clean, but python random is global.
|
| 152 |
+
# Ideally GameState should use a random instance.
|
| 153 |
+
# For now, we assume the caller handles global state implications or we just reset seed.
|
| 154 |
+
|
| 155 |
+
# NOTE: This modifies global random state!
|
| 156 |
+
random.seed(seed)
|
| 157 |
+
|
| 158 |
+
# 2. Init Game State (Headless)
|
| 159 |
+
GameState.member_db = member_db
|
| 160 |
+
GameState.live_db = live_db
|
| 161 |
+
# Energy DB is not static on GameState?
|
| 162 |
+
GameState.energy_db = energy_db # server.py sets this on instance or class?
|
| 163 |
+
# server.py says: GameState.energy_db = energy_db
|
| 164 |
+
|
| 165 |
+
# Create fresh state
|
| 166 |
+
temp_gs = GameState()
|
| 167 |
+
temp_gs.initialize_game()
|
| 168 |
+
|
| 169 |
+
# Set decks if available
|
| 170 |
+
if deck_info:
|
| 171 |
+
p0_deck = deck_info.get("p0_deck")
|
| 172 |
+
p1_deck = deck_info.get("p1_deck")
|
| 173 |
+
|
| 174 |
+
if p0_deck and len(p0_deck) > 0:
|
| 175 |
+
print(f"Loading custom deck for P0: {len(p0_deck)} cards")
|
| 176 |
+
p0 = temp_gs.players[0]
|
| 177 |
+
# Reset Deck & Hand
|
| 178 |
+
p0.main_deck = [int(x) for x in p0_deck]
|
| 179 |
+
p0.hand = []
|
| 180 |
+
p0.discard = []
|
| 181 |
+
# Draw initial hand (5 cards)
|
| 182 |
+
draw_count = min(5, len(p0.main_deck))
|
| 183 |
+
p0.hand = p0.main_deck[:draw_count]
|
| 184 |
+
p0.hand_added_turn = [1] * draw_count
|
| 185 |
+
p0.main_deck = p0.main_deck[draw_count:]
|
| 186 |
+
|
| 187 |
+
if p1_deck and len(p1_deck) > 0:
|
| 188 |
+
print(f"Loading custom deck for P1: {len(p1_deck)} cards")
|
| 189 |
+
p1 = temp_gs.players[1]
|
| 190 |
+
p1.main_deck = [int(x) for x in p1_deck]
|
| 191 |
+
p1.hand = []
|
| 192 |
+
p1.discard = []
|
| 193 |
+
draw_count = min(5, len(p1.main_deck))
|
| 194 |
+
p1.hand = p1.main_deck[:draw_count]
|
| 195 |
+
p1.hand_added_turn = [1] * draw_count
|
| 196 |
+
p1.main_deck = p1.main_deck[draw_count:]
|
| 197 |
+
|
| 198 |
+
reconstructed_history = []
|
| 199 |
+
|
| 200 |
+
# 3. Serialize Initial State
|
| 201 |
+
reconstructed_history.append(serialize_state(temp_gs))
|
| 202 |
+
|
| 203 |
+
# 4. Replay Actions
|
| 204 |
+
for action_id in action_log:
|
| 205 |
+
temp_gs.step(action_id)
|
| 206 |
+
reconstructed_history.append(serialize_state(temp_gs))
|
| 207 |
+
|
| 208 |
+
print(f"Reconstructed {len(reconstructed_history)} frames from {len(action_log)} actions.")
|
| 209 |
+
return reconstructed_history
|
| 210 |
+
|
| 211 |
+
# Level 2 Logic (State Inflation)
|
| 212 |
+
registry = optimized_data.get("registry", {})
|
| 213 |
+
states = optimized_data.get("states", [])
|
| 214 |
+
|
| 215 |
+
def get_static_data(cid):
|
| 216 |
+
"""Get static data from Registry OR Database"""
|
| 217 |
+
# 1. Registry (Custom cards / Legacy format)
|
| 218 |
+
if str(cid) in registry:
|
| 219 |
+
return registry[str(cid)]
|
| 220 |
+
if cid in registry:
|
| 221 |
+
return registry[cid]
|
| 222 |
+
|
| 223 |
+
# 2. Database
|
| 224 |
+
if cid in member_db:
|
| 225 |
+
m = member_db[cid]
|
| 226 |
+
# Reconstruct dictionary from object
|
| 227 |
+
ability_text = getattr(m, "ability_text", "")
|
| 228 |
+
if hasattr(m, "abilities") and m.abilities:
|
| 229 |
+
# Use raw Japanese text
|
| 230 |
+
# Clean wiki markup: {{icon.png|Text}} -> Text, [[Link|Text]] -> Text
|
| 231 |
+
import re
|
| 232 |
+
|
| 233 |
+
def clean_text(text):
|
| 234 |
+
text = re.sub(r"\{\{.*?\|(.*?)\}\}", r"\1", text) # {{icon|Text}} -> Text
|
| 235 |
+
text = re.sub(r"\[\[.*?\|(.*?)\]\]", r"\1", text) # [[Link|Text]] -> Text
|
| 236 |
+
return text
|
| 237 |
+
|
| 238 |
+
ability_lines = [clean_text(ab.raw_text) for ab in m.abilities]
|
| 239 |
+
ability_text = "\n".join(ability_lines)
|
| 240 |
+
|
| 241 |
+
return {
|
| 242 |
+
"name": m.name,
|
| 243 |
+
"card_no": m.card_no,
|
| 244 |
+
"type": "member",
|
| 245 |
+
"cost": m.cost,
|
| 246 |
+
"blade": m.blades,
|
| 247 |
+
"img": m.img_path,
|
| 248 |
+
"hearts": m.hearts.tolist(),
|
| 249 |
+
"blade_hearts": m.blade_hearts.tolist(),
|
| 250 |
+
"text": ability_text,
|
| 251 |
+
"color": "Unknown",
|
| 252 |
+
}
|
| 253 |
+
elif cid in live_db:
|
| 254 |
+
l = live_db[cid]
|
| 255 |
+
ability_text = getattr(l, "ability_text", "")
|
| 256 |
+
if hasattr(l, "abilities") and l.abilities:
|
| 257 |
+
import re
|
| 258 |
+
|
| 259 |
+
def clean_text(text):
|
| 260 |
+
text = re.sub(r"\{\{.*?\|(.*?)\}\}", r"\1", text)
|
| 261 |
+
text = re.sub(r"\[\[.*?\|(.*?)\]\]", r"\1", text)
|
| 262 |
+
return text
|
| 263 |
+
|
| 264 |
+
ability_lines = [clean_text(ab.raw_text) for ab in l.abilities]
|
| 265 |
+
ability_text = "\n".join(ability_lines)
|
| 266 |
+
|
| 267 |
+
return {
|
| 268 |
+
"name": l.name,
|
| 269 |
+
"card_no": l.card_no,
|
| 270 |
+
"type": "live",
|
| 271 |
+
"score": l.score,
|
| 272 |
+
"img": l.img_path,
|
| 273 |
+
"required_hearts": l.required_hearts.tolist(),
|
| 274 |
+
"text": ability_text,
|
| 275 |
+
}
|
| 276 |
+
elif cid in energy_db:
|
| 277 |
+
# EnergyCard is simple (just ID), so we hardcode display info
|
| 278 |
+
return {"name": "Energy", "type": "energy", "img": "assets/energy_card.png"}
|
| 279 |
+
|
| 280 |
+
return None
|
| 281 |
+
|
| 282 |
+
def inflate_object(obj):
|
| 283 |
+
if isinstance(obj, list):
|
| 284 |
+
return [inflate_object(x) for x in obj]
|
| 285 |
+
elif isinstance(obj, dict):
|
| 286 |
+
# Check for ID reference to inflate
|
| 287 |
+
if "id" in obj:
|
| 288 |
+
cid = obj["id"]
|
| 289 |
+
static_data = get_static_data(cid)
|
| 290 |
+
if static_data:
|
| 291 |
+
# Merge static data into this object (dynamic overrides static if conflict, though shouldn't happen)
|
| 292 |
+
# We create a new dict to Avoid mutating the source if it's reused
|
| 293 |
+
new_obj = static_data.copy()
|
| 294 |
+
for k, v in obj.items():
|
| 295 |
+
new_obj[k] = inflate_object(v)
|
| 296 |
+
return new_obj
|
| 297 |
+
|
| 298 |
+
return {k: inflate_object(v) for k, v in obj.items()}
|
| 299 |
+
else:
|
| 300 |
+
return obj
|
| 301 |
+
|
| 302 |
+
return inflate_object(states)
|
engine/game/serializer.py
ADDED
|
@@ -0,0 +1,689 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
|
| 3 |
+
from engine.game.desc_utils import get_action_desc
|
| 4 |
+
from engine.game.state_utils import get_base_id
|
| 5 |
+
from engine.models.ability import EFFECT_DESCRIPTIONS, EffectType, TriggerType
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def format_effect_description(effect):
|
| 9 |
+
# Logic copied/adapted from effect_mixin.py
|
| 10 |
+
template = EFFECT_DESCRIPTIONS.get(effect.effect_type, getattr(effect.effect_type, "name", str(effect.effect_type)))
|
| 11 |
+
context = effect.params.copy()
|
| 12 |
+
context["value"] = effect.value
|
| 13 |
+
|
| 14 |
+
# Custom overrides if needed (e.g. REDUCE_HEART_REQ)
|
| 15 |
+
if effect.effect_type == EffectType.REDUCE_HEART_REQ:
|
| 16 |
+
if effect.value < 0:
|
| 17 |
+
return f"Reduce Heart Requirement by {abs(effect.value)}"
|
| 18 |
+
else:
|
| 19 |
+
return f"Increase Heart Requirement by {effect.value}"
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
desc = template.format(**context)
|
| 23 |
+
except KeyError:
|
| 24 |
+
desc = template
|
| 25 |
+
|
| 26 |
+
# Add contextual suffixes
|
| 27 |
+
if effect.params.get("per_energy"):
|
| 28 |
+
desc += " per Energy"
|
| 29 |
+
if effect.params.get("per_member"):
|
| 30 |
+
desc += " per Member"
|
| 31 |
+
if effect.params.get("per_live"):
|
| 32 |
+
desc += " per Live"
|
| 33 |
+
|
| 34 |
+
return desc
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def get_card_modifiers(player, slot_idx, card_id, member_db, live_db):
|
| 38 |
+
modifiers = []
|
| 39 |
+
|
| 40 |
+
# 0. Player Level Restrictions (Virtual Modifiers)
|
| 41 |
+
if player.cannot_live:
|
| 42 |
+
modifiers.append({"description": "Cannot perform Live this turn", "source": "Game Rule", "expiry": "LIVE_END"})
|
| 43 |
+
|
| 44 |
+
# 1. Continuous Effects
|
| 45 |
+
for ce in player.continuous_effects:
|
| 46 |
+
target_slot = ce.get("target_slot", -1)
|
| 47 |
+
eff = ce["effect"]
|
| 48 |
+
|
| 49 |
+
# Determine relevance:
|
| 50 |
+
# - member on stage (slot_idx >= 0): matches target_slot or target_slot is -1
|
| 51 |
+
# - live card (slot_idx == -1): matches global effects or live-specific effects
|
| 52 |
+
is_relevant = False
|
| 53 |
+
if slot_idx >= 0:
|
| 54 |
+
if target_slot == -1 or target_slot == slot_idx:
|
| 55 |
+
is_relevant = True
|
| 56 |
+
else:
|
| 57 |
+
# For live cards/others, only show global or relevant types
|
| 58 |
+
if target_slot == -1 or eff.effect_type in (EffectType.REDUCE_HEART_REQ, EffectType.MODIFY_SCORE_RULE):
|
| 59 |
+
is_relevant = True
|
| 60 |
+
|
| 61 |
+
if is_relevant:
|
| 62 |
+
desc = format_effect_description(eff)
|
| 63 |
+
|
| 64 |
+
# Resolve Source
|
| 65 |
+
source_name = "Effect"
|
| 66 |
+
sid = ce.get("source_card_id")
|
| 67 |
+
if sid is not None:
|
| 68 |
+
base_sid = get_base_id(int(sid))
|
| 69 |
+
if base_sid in member_db:
|
| 70 |
+
source_name = member_db[base_sid].name
|
| 71 |
+
elif base_sid in live_db:
|
| 72 |
+
source_name = live_db[base_sid].name
|
| 73 |
+
|
| 74 |
+
modifiers.append({"description": desc, "source": source_name, "expiry": ce.get("expiry", "TURN_END")})
|
| 75 |
+
|
| 76 |
+
# 2. Intrinsic Constant Abilities
|
| 77 |
+
base_cid = get_base_id(int(card_id))
|
| 78 |
+
db = member_db if base_cid in member_db else live_db if base_cid in live_db else None
|
| 79 |
+
|
| 80 |
+
if db and base_cid in db:
|
| 81 |
+
card = db[base_cid]
|
| 82 |
+
if hasattr(card, "abilities"):
|
| 83 |
+
for ab in card.abilities:
|
| 84 |
+
if ab.trigger == TriggerType.CONSTANT:
|
| 85 |
+
# Check conditions
|
| 86 |
+
# Note: _check_condition_for_constant might not be available on card/db,
|
| 87 |
+
# but we assume the environment has it or fallback to True for display
|
| 88 |
+
# Serializer is often called without full GameState context for individual cards
|
| 89 |
+
# but here we have 'player'.
|
| 90 |
+
try:
|
| 91 |
+
if all(player._check_condition_for_constant(cond, slot_idx) for cond in ab.conditions):
|
| 92 |
+
for eff in ab.effects:
|
| 93 |
+
desc = format_effect_description(eff)
|
| 94 |
+
modifiers.append({"description": desc, "source": "Self", "expiry": "CONSTANT"})
|
| 95 |
+
except (AttributeError, TypeError):
|
| 96 |
+
# Fallback: display constant if it exists but condition check is impossible
|
| 97 |
+
for eff in ab.effects:
|
| 98 |
+
desc = format_effect_description(eff)
|
| 99 |
+
modifiers.append({"description": desc, "source": "Self", "expiry": "CONSTANT"})
|
| 100 |
+
|
| 101 |
+
return modifiers
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def serialize_card(cid, member_db, live_db, energy_db, is_viewable=True, peek=False):
|
| 105 |
+
if not is_viewable and not peek:
|
| 106 |
+
return {"id": int(cid), "name": "???", "type": "unknown", "img": "cards/back.png", "hidden": True}
|
| 107 |
+
|
| 108 |
+
card_data = {}
|
| 109 |
+
|
| 110 |
+
# Try direct lookup, then fallback to base ID for unique IDs
|
| 111 |
+
base_cid = get_base_id(int(cid))
|
| 112 |
+
|
| 113 |
+
if base_cid in member_db:
|
| 114 |
+
m = member_db[base_cid]
|
| 115 |
+
ability_text = getattr(m, "ability_text", "")
|
| 116 |
+
|
| 117 |
+
# Only reconstruct if no rich text is available
|
| 118 |
+
if not ability_text and hasattr(m, "abilities") and m.abilities:
|
| 119 |
+
ability_lines = []
|
| 120 |
+
for ab in m.abilities:
|
| 121 |
+
trigger_icon = {
|
| 122 |
+
TriggerType.ACTIVATED: "【起動】",
|
| 123 |
+
TriggerType.ON_PLAY: "【登場】",
|
| 124 |
+
TriggerType.CONSTANT: "【常時】",
|
| 125 |
+
TriggerType.ON_LIVE_START: "【ライブ開始】",
|
| 126 |
+
TriggerType.ON_LIVE_SUCCESS: "【ライブ成功時】",
|
| 127 |
+
}.get(ab.trigger, "【自動】")
|
| 128 |
+
ability_lines.append(f"{trigger_icon} {ab.raw_text}")
|
| 129 |
+
ability_text = "\n".join(ability_lines)
|
| 130 |
+
|
| 131 |
+
card_data = {
|
| 132 |
+
"id": int(cid), # Keep original ID (UID) for frontend tracking
|
| 133 |
+
"card_no": m.card_no,
|
| 134 |
+
"name": m.name,
|
| 135 |
+
"type": "member",
|
| 136 |
+
"cost": m.cost,
|
| 137 |
+
"blade": m.blades,
|
| 138 |
+
"img": m.img_path,
|
| 139 |
+
"hearts": m.hearts.tolist() if hasattr(m.hearts, "tolist") else list(m.hearts),
|
| 140 |
+
"blade_hearts": m.blade_hearts.tolist() if hasattr(m.blade_hearts, "tolist") else list(m.blade_hearts),
|
| 141 |
+
"text": ability_text,
|
| 142 |
+
"original_text": getattr(m, "original_text", ""),
|
| 143 |
+
}
|
| 144 |
+
elif base_cid in live_db:
|
| 145 |
+
l = live_db[base_cid]
|
| 146 |
+
ability_text = getattr(l, "ability_text", "")
|
| 147 |
+
|
| 148 |
+
# Only reconstruct if no rich text is available
|
| 149 |
+
if not ability_text and hasattr(l, "abilities") and l.abilities:
|
| 150 |
+
ability_lines = []
|
| 151 |
+
for ab in l.abilities:
|
| 152 |
+
trigger_icon = {TriggerType.ON_LIVE_START: "【ライブ開始】"}.get(ab.trigger, "【自動】")
|
| 153 |
+
ability_lines.append(f"{trigger_icon} {ab.raw_text}")
|
| 154 |
+
ability_text = "\n".join(ability_lines)
|
| 155 |
+
|
| 156 |
+
card_data = {
|
| 157 |
+
"id": int(cid),
|
| 158 |
+
"card_no": l.card_no,
|
| 159 |
+
"name": l.name,
|
| 160 |
+
"type": "live",
|
| 161 |
+
"score": l.score,
|
| 162 |
+
"img": l.img_path,
|
| 163 |
+
"required_hearts": l.required_hearts.tolist()
|
| 164 |
+
if hasattr(l.required_hearts, "tolist")
|
| 165 |
+
else list(l.required_hearts),
|
| 166 |
+
"text": ability_text,
|
| 167 |
+
"original_text": getattr(l, "original_text", ""),
|
| 168 |
+
}
|
| 169 |
+
elif base_cid in energy_db:
|
| 170 |
+
e = energy_db[base_cid]
|
| 171 |
+
card_data = {
|
| 172 |
+
"id": int(cid),
|
| 173 |
+
"card_no": getattr(e, "card_no", "ENERGY"),
|
| 174 |
+
"name": getattr(e, "name", "Energy"),
|
| 175 |
+
"type": "energy",
|
| 176 |
+
"img": getattr(e, "img_path", "assets/energy_card.png"),
|
| 177 |
+
}
|
| 178 |
+
else:
|
| 179 |
+
return {"id": int(cid), "name": f"Card {cid}", "type": "unknown", "img": None}
|
| 180 |
+
|
| 181 |
+
if not is_viewable and peek:
|
| 182 |
+
card_data["hidden"] = True
|
| 183 |
+
card_data["face_down"] = True
|
| 184 |
+
|
| 185 |
+
return card_data
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def serialize_player(p, game_state, player_idx, viewer_idx=0, is_viewable=True):
|
| 189 |
+
member_db = game_state.member_db
|
| 190 |
+
live_db = game_state.live_db
|
| 191 |
+
energy_db = getattr(game_state, "energy_db", {})
|
| 192 |
+
legal_mask = game_state.get_legal_actions()
|
| 193 |
+
|
| 194 |
+
expected_yells = 0
|
| 195 |
+
for i, card_id in enumerate(p.stage):
|
| 196 |
+
if card_id >= 0 and not p.tapped_members[i]:
|
| 197 |
+
base_cid = get_base_id(int(card_id))
|
| 198 |
+
if base_cid in member_db:
|
| 199 |
+
expected_yells += member_db[base_cid].blades
|
| 200 |
+
|
| 201 |
+
hand = []
|
| 202 |
+
for i, cid in enumerate(p.hand):
|
| 203 |
+
if is_viewable:
|
| 204 |
+
c = serialize_card(cid, member_db, live_db, energy_db)
|
| 205 |
+
is_new = False
|
| 206 |
+
if hasattr(p, "hand_added_turn") and i < len(p.hand_added_turn):
|
| 207 |
+
is_new = p.hand_added_turn[i] == game_state.turn_number
|
| 208 |
+
c["is_new"] = is_new
|
| 209 |
+
|
| 210 |
+
valid_actions = []
|
| 211 |
+
for area in range(3):
|
| 212 |
+
aid = 1 + i * 3 + area
|
| 213 |
+
if aid < len(legal_mask) and legal_mask[aid]:
|
| 214 |
+
valid_actions.append(aid)
|
| 215 |
+
for aid in [400 + i, 300 + i, 500 + i]:
|
| 216 |
+
if aid < len(legal_mask) and legal_mask[aid]:
|
| 217 |
+
valid_actions.append(aid)
|
| 218 |
+
c["valid_actions"] = valid_actions
|
| 219 |
+
hand.append(c)
|
| 220 |
+
else:
|
| 221 |
+
hand.append(serialize_card(cid, member_db, live_db, energy_db, is_viewable=False))
|
| 222 |
+
|
| 223 |
+
stage = []
|
| 224 |
+
for i, _ in enumerate(range(3)):
|
| 225 |
+
# Correctly access stage by index
|
| 226 |
+
cid = int(p.stage[i])
|
| 227 |
+
if cid >= 0:
|
| 228 |
+
c = serialize_card(cid, member_db, live_db, energy_db)
|
| 229 |
+
c["tapped"] = bool(p.tapped_members[i])
|
| 230 |
+
c["energy"] = int(p.stage_energy_count[i])
|
| 231 |
+
c["locked"] = bool(p.members_played_this_turn[i])
|
| 232 |
+
|
| 233 |
+
# Add modifiers
|
| 234 |
+
c["modifiers"] = get_card_modifiers(p, i, cid, member_db, live_db)
|
| 235 |
+
|
| 236 |
+
stage.append(c)
|
| 237 |
+
else:
|
| 238 |
+
stage.append(None)
|
| 239 |
+
|
| 240 |
+
discard = [serialize_card(cid, member_db, live_db, energy_db, is_viewable=True) for cid in p.discard]
|
| 241 |
+
|
| 242 |
+
energy = []
|
| 243 |
+
for i, cid in enumerate(p.energy_zone):
|
| 244 |
+
energy.append(
|
| 245 |
+
{
|
| 246 |
+
"id": i,
|
| 247 |
+
"tapped": bool(p.tapped_energy[i]),
|
| 248 |
+
"card": serialize_card(cid, member_db, live_db, energy_db, is_viewable=False),
|
| 249 |
+
}
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
# Calculate total hearts and blades for the player
|
| 253 |
+
total_blades = p.get_total_blades(member_db)
|
| 254 |
+
total_hearts = p.get_total_hearts(member_db) # Returns np.array(7)
|
| 255 |
+
|
| 256 |
+
# Track remaining hearts for live fulfillment calculation (Greedy allocation)
|
| 257 |
+
temp_hearts = total_hearts.copy()
|
| 258 |
+
|
| 259 |
+
live_zone = []
|
| 260 |
+
for i, cid in enumerate(p.live_zone):
|
| 261 |
+
is_revealed = bool(p.live_zone_revealed[i]) if i < len(p.live_zone_revealed) else False
|
| 262 |
+
card_obj = serialize_card(
|
| 263 |
+
cid, member_db, live_db, energy_db, is_viewable=is_revealed, peek=(player_idx == viewer_idx)
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
# Calculate heart progress for this live card
|
| 267 |
+
if cid in live_db:
|
| 268 |
+
l = live_db[cid]
|
| 269 |
+
req = l.required_hearts # np.array
|
| 270 |
+
filled = [0] * 7
|
| 271 |
+
|
| 272 |
+
if is_revealed or (player_idx == viewer_idx):
|
| 273 |
+
# Greedy fill logic matching PlayerState.get_performance_guide
|
| 274 |
+
# Colors 0-5
|
| 275 |
+
for c_idx in range(6):
|
| 276 |
+
have = temp_hearts[c_idx]
|
| 277 |
+
need = req[c_idx]
|
| 278 |
+
take = min(have, need)
|
| 279 |
+
filled[c_idx] = int(take)
|
| 280 |
+
temp_hearts[c_idx] -= take
|
| 281 |
+
|
| 282 |
+
# Any Color (Index 6)
|
| 283 |
+
req_any = req[6] if len(req) > 6 else 0
|
| 284 |
+
remaining_total = sum(temp_hearts[:6]) + temp_hearts[6]
|
| 285 |
+
take_any = min(remaining_total, req_any)
|
| 286 |
+
filled[6] = int(take_any)
|
| 287 |
+
# Note: We don't subtract from temp_hearts for 'any' because strict color matching is done,
|
| 288 |
+
# and 'any' sucks from the pool of remaining.
|
| 289 |
+
# But to be strictly correct for *subsequent* cards (if we supported multiple approvals at once),
|
| 290 |
+
# we should decrement. But the game usually checks one at a time or order matters.
|
| 291 |
+
# Use the logic from get_performance_guide:
|
| 292 |
+
# It doesn't actually decrement 'any' from specific colors in temp_hearts
|
| 293 |
+
# because 'any' is a wildcard check on the *sum*.
|
| 294 |
+
# Wait, get_performance_guide does:
|
| 295 |
+
# remaining_total = np.sum(temp_hearts[:6]) + temp_hearts[6]
|
| 296 |
+
|
| 297 |
+
card_obj["required_hearts"] = req.tolist()
|
| 298 |
+
card_obj["filled_hearts"] = filled
|
| 299 |
+
|
| 300 |
+
# Determine passed status
|
| 301 |
+
is_passed = True
|
| 302 |
+
for c_idx in range(6):
|
| 303 |
+
if filled[c_idx] < req[c_idx]:
|
| 304 |
+
is_passed = False
|
| 305 |
+
if filled[6] < req[6]:
|
| 306 |
+
is_passed = False
|
| 307 |
+
|
| 308 |
+
card_obj["is_cleared"] = is_passed
|
| 309 |
+
|
| 310 |
+
# Add modifiers for live card (e.g. requirement reduction)
|
| 311 |
+
card_obj["modifiers"] = get_card_modifiers(p, -1, cid, member_db, live_db)
|
| 312 |
+
|
| 313 |
+
live_zone.append(card_obj)
|
| 314 |
+
|
| 315 |
+
score = sum(live_db[cid].score for cid in p.success_lives if cid in live_db)
|
| 316 |
+
|
| 317 |
+
return {
|
| 318 |
+
"player_id": p.player_id,
|
| 319 |
+
"score": score,
|
| 320 |
+
"is_active": (game_state.current_player == player_idx),
|
| 321 |
+
"hand": hand,
|
| 322 |
+
"hand_count": len(p.hand),
|
| 323 |
+
"mulligan_selection": list(p.mulligan_selection) if is_viewable else [],
|
| 324 |
+
"deck_count": len(p.main_deck),
|
| 325 |
+
"energy_deck_count": len(p.energy_deck),
|
| 326 |
+
"discard": discard,
|
| 327 |
+
"discard_count": len(p.discard),
|
| 328 |
+
"energy": energy,
|
| 329 |
+
"energy_count": len(p.energy_zone),
|
| 330 |
+
"energy_untapped": int(p.count_untapped_energy()),
|
| 331 |
+
"live_zone": live_zone,
|
| 332 |
+
"live_zone_count": len(p.live_zone),
|
| 333 |
+
"stage": stage,
|
| 334 |
+
"success_lives": [serialize_card(cid, member_db, live_db, energy_db, is_viewable) for cid in p.success_lives],
|
| 335 |
+
"restrictions": list(p.restrictions),
|
| 336 |
+
"expected_yells": expected_yells,
|
| 337 |
+
"total_hearts": total_hearts.tolist(),
|
| 338 |
+
"total_blades": int(total_blades),
|
| 339 |
+
"hand_costs": [p.get_member_cost(cid, member_db) if cid in member_db else 0 for cid in p.hand],
|
| 340 |
+
"active_effects": [
|
| 341 |
+
{
|
| 342 |
+
"description": format_effect_description(ce["effect"]),
|
| 343 |
+
"source": (
|
| 344 |
+
member_db[get_base_id(int(ce["source_card_id"]))].name
|
| 345 |
+
if ce.get("source_card_id") is not None and get_base_id(int(ce["source_card_id"])) in member_db
|
| 346 |
+
else live_db[get_base_id(int(ce["source_card_id"]))].name
|
| 347 |
+
if ce.get("source_card_id") is not None and get_base_id(int(ce["source_card_id"])) in live_db
|
| 348 |
+
else "Effect"
|
| 349 |
+
),
|
| 350 |
+
"expiry": ce.get("expiry", "TURN_END"),
|
| 351 |
+
"source_card_id": int(ce.get("source_card_id", -1)) if ce.get("source_card_id") is not None else -1,
|
| 352 |
+
}
|
| 353 |
+
for ce in p.continuous_effects
|
| 354 |
+
],
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
def serialize_state(gs, viewer_idx=0, is_pvp=False, mode="pve"):
|
| 359 |
+
active_idx = gs.current_player
|
| 360 |
+
legal_mask = gs.get_legal_actions()
|
| 361 |
+
legal_actions = []
|
| 362 |
+
p = gs.active_player
|
| 363 |
+
member_db = gs.member_db
|
| 364 |
+
live_db = gs.live_db
|
| 365 |
+
energy_db = getattr(gs, "energy_db", {})
|
| 366 |
+
|
| 367 |
+
# Only populate legal actions if it is the viewer's turn, or if in PvP/Hotseat mode (show all)
|
| 368 |
+
show_actions = is_pvp or (viewer_idx == active_idx)
|
| 369 |
+
|
| 370 |
+
if show_actions:
|
| 371 |
+
for i, v in enumerate(legal_mask):
|
| 372 |
+
if v:
|
| 373 |
+
desc = get_action_desc(i, gs)
|
| 374 |
+
meta = {"id": i, "desc": desc, "name": desc, "description": desc}
|
| 375 |
+
|
| 376 |
+
# Enrich with metadata for UI
|
| 377 |
+
if 1 <= i <= 180:
|
| 378 |
+
meta["type"] = "PLAY"
|
| 379 |
+
meta["hand_idx"] = (i - 1) // 3
|
| 380 |
+
meta["area_idx"] = (i - 1) % 3
|
| 381 |
+
if meta["hand_idx"] < len(p.hand):
|
| 382 |
+
cid = p.hand[meta["hand_idx"]]
|
| 383 |
+
c = serialize_card(cid, member_db, live_db, energy_db)
|
| 384 |
+
hand_cost = p.get_member_cost(cid, member_db)
|
| 385 |
+
# Baton Touch Reduction (Rule 12)
|
| 386 |
+
net_cost = hand_cost
|
| 387 |
+
if p.stage[meta["area_idx"]] >= 0:
|
| 388 |
+
old_cid = get_base_id(int(p.stage[meta["area_idx"]]))
|
| 389 |
+
if old_cid in member_db:
|
| 390 |
+
net_cost = max(0, hand_cost - member_db[old_cid].cost)
|
| 391 |
+
|
| 392 |
+
meta.update(
|
| 393 |
+
{
|
| 394 |
+
"img": c["img"],
|
| 395 |
+
"name": c["name"],
|
| 396 |
+
"cost": int(net_cost),
|
| 397 |
+
"base_cost": int(hand_cost),
|
| 398 |
+
"card_no": c.get("card_no", "???"),
|
| 399 |
+
"text": c.get("text", ""),
|
| 400 |
+
}
|
| 401 |
+
)
|
| 402 |
+
if cid in member_db:
|
| 403 |
+
meta["triggers"] = [ab.trigger for ab in member_db[cid].abilities]
|
| 404 |
+
elif 200 <= i <= 202:
|
| 405 |
+
meta["type"] = "ABILITY"
|
| 406 |
+
meta["area_idx"] = i - 200
|
| 407 |
+
cid = p.stage[meta["area_idx"]]
|
| 408 |
+
if cid >= 0:
|
| 409 |
+
c = serialize_card(cid, member_db, live_db, energy_db)
|
| 410 |
+
meta.update(
|
| 411 |
+
{"img": c["img"], "name": c["name"], "text": c.get("text", ""), "source_card_id": int(cid)}
|
| 412 |
+
)
|
| 413 |
+
elif 300 <= i <= 359:
|
| 414 |
+
meta["type"] = "MULLIGAN"
|
| 415 |
+
meta["hand_idx"] = i - 300
|
| 416 |
+
elif 400 <= i <= 459:
|
| 417 |
+
meta["type"] = "LIVE_SET"
|
| 418 |
+
meta["hand_idx"] = i - 400
|
| 419 |
+
elif 500 <= i <= 559:
|
| 420 |
+
meta["type"] = "SELECT_HAND"
|
| 421 |
+
meta["hand_idx"] = i - 500
|
| 422 |
+
target_p_idx = active_idx
|
| 423 |
+
if gs.pending_choices:
|
| 424 |
+
target_p_idx = gs.pending_choices[0][1].get("player_id", active_idx)
|
| 425 |
+
meta["player_id"] = target_p_idx
|
| 426 |
+
target_p = gs.players[target_p_idx]
|
| 427 |
+
if meta["hand_idx"] < len(target_p.hand):
|
| 428 |
+
cid = target_p.hand[meta["hand_idx"]]
|
| 429 |
+
c = serialize_card(cid, member_db, live_db, energy_db)
|
| 430 |
+
meta.update(
|
| 431 |
+
{"img": c["img"], "name": c["name"], "text": c.get("text", ""), "source_card_id": int(cid)}
|
| 432 |
+
)
|
| 433 |
+
elif 560 <= i <= 562:
|
| 434 |
+
meta["type"] = "SELECT_STAGE"
|
| 435 |
+
meta["area_idx"] = i - 560
|
| 436 |
+
target_p_idx = active_idx
|
| 437 |
+
if gs.pending_choices:
|
| 438 |
+
target_p_idx = gs.pending_choices[0][1].get("player_id", active_idx)
|
| 439 |
+
meta["player_id"] = target_p_idx
|
| 440 |
+
target_p = gs.players[target_p_idx]
|
| 441 |
+
cid = target_p.stage[meta["area_idx"]]
|
| 442 |
+
if cid >= 0:
|
| 443 |
+
c = serialize_card(cid, member_db, live_db, energy_db)
|
| 444 |
+
meta.update(
|
| 445 |
+
{"img": c["img"], "name": c["name"], "text": c.get("text", ""), "source_card_id": int(cid)}
|
| 446 |
+
)
|
| 447 |
+
elif 590 <= i <= 599:
|
| 448 |
+
meta["type"] = "ABILITY_TRIGGER"
|
| 449 |
+
meta["index"] = i - 590
|
| 450 |
+
elif 600 <= i <= 659:
|
| 451 |
+
meta["type"] = "SELECT"
|
| 452 |
+
meta["index"] = i - 600
|
| 453 |
+
if gs.pending_choices:
|
| 454 |
+
ctype, cparams = gs.pending_choices[0]
|
| 455 |
+
if ctype == "TARGET_OPPONENT_MEMBER":
|
| 456 |
+
opp = gs.inactive_player
|
| 457 |
+
meta["player_id"] = opp.player_id
|
| 458 |
+
if meta["index"] < 3:
|
| 459 |
+
cid = opp.stage[meta["index"]]
|
| 460 |
+
if cid >= 0:
|
| 461 |
+
c = serialize_card(cid, member_db, live_db, energy_db)
|
| 462 |
+
meta.update(
|
| 463 |
+
{
|
| 464 |
+
"img": c["img"],
|
| 465 |
+
"name": c["name"],
|
| 466 |
+
"text": c.get("text", ""),
|
| 467 |
+
"source_card_id": int(cid),
|
| 468 |
+
}
|
| 469 |
+
)
|
| 470 |
+
elif ctype == "SELECT_FROM_LIST" or ctype == "SELECT_SUCCESS_LIVE":
|
| 471 |
+
cards = cparams.get("cards", [])
|
| 472 |
+
if meta["index"] < len(cards):
|
| 473 |
+
cid = cards[meta["index"]]
|
| 474 |
+
c = serialize_card(cid, member_db, live_db, energy_db)
|
| 475 |
+
meta.update(
|
| 476 |
+
{
|
| 477 |
+
"img": c["img"],
|
| 478 |
+
"name": c["name"],
|
| 479 |
+
"text": c.get("text", ""),
|
| 480 |
+
"source_card_id": int(cid),
|
| 481 |
+
}
|
| 482 |
+
)
|
| 483 |
+
elif 660 <= i <= 719:
|
| 484 |
+
meta["type"] = "SELECT_DISCARD"
|
| 485 |
+
meta["index"] = i - 660
|
| 486 |
+
if gs.pending_choices:
|
| 487 |
+
ctype, cparams = gs.pending_choices[0]
|
| 488 |
+
if ctype == "SELECT_FROM_DISCARD":
|
| 489 |
+
cards = cparams.get("cards", [])
|
| 490 |
+
if meta["index"] < len(cards):
|
| 491 |
+
cid = cards[meta["index"]]
|
| 492 |
+
c = serialize_card(cid, member_db, live_db, energy_db)
|
| 493 |
+
meta.update(
|
| 494 |
+
{
|
| 495 |
+
"img": c["img"],
|
| 496 |
+
"name": c["name"],
|
| 497 |
+
"text": c.get("text", ""),
|
| 498 |
+
"source_card_id": int(cid),
|
| 499 |
+
}
|
| 500 |
+
)
|
| 501 |
+
|
| 502 |
+
elif 570 <= i <= 579:
|
| 503 |
+
meta["type"] = "SELECT_MODE"
|
| 504 |
+
meta["index"] = i - 570
|
| 505 |
+
if gs.pending_choices:
|
| 506 |
+
ctype, cparams = gs.pending_choices[0]
|
| 507 |
+
if ctype == "MODAL" or ctype == "SELECT_MODE":
|
| 508 |
+
options = cparams.get("options", [])
|
| 509 |
+
if meta["index"] < len(options):
|
| 510 |
+
opt = options[meta["index"]]
|
| 511 |
+
# Option can be string or list/dict
|
| 512 |
+
desc = str(opt)
|
| 513 |
+
if isinstance(opt, (list, tuple)) and len(opt) > 0:
|
| 514 |
+
desc = str(opt[0]) # Crude fallback
|
| 515 |
+
meta["text"] = desc
|
| 516 |
+
meta["name"] = desc
|
| 517 |
+
elif 580 <= i <= 589:
|
| 518 |
+
meta["type"] = "COLOR_SELECT"
|
| 519 |
+
meta["index"] = i - 580
|
| 520 |
+
colors = ["Pink", "Red", "Yellow", "Green", "Blue", "Purple", "All", "None"]
|
| 521 |
+
if meta["index"] < len(colors):
|
| 522 |
+
meta["color"] = colors[meta["index"]]
|
| 523 |
+
meta["name"] = colors[meta["index"]]
|
| 524 |
+
elif 720 <= i <= 759:
|
| 525 |
+
meta["type"] = "SELECT_FORMATION"
|
| 526 |
+
meta["index"] = i - 720
|
| 527 |
+
if gs.pending_choices:
|
| 528 |
+
ctype, cparams = gs.pending_choices[0]
|
| 529 |
+
cards = cparams.get("cards", cparams.get("available_members", []))
|
| 530 |
+
if meta["index"] < len(cards):
|
| 531 |
+
cid = cards[meta["index"]]
|
| 532 |
+
c = serialize_card(cid, member_db, live_db, energy_db)
|
| 533 |
+
meta.update(
|
| 534 |
+
{
|
| 535 |
+
"img": c["img"],
|
| 536 |
+
"name": c["name"],
|
| 537 |
+
"text": c.get("text", ""),
|
| 538 |
+
"source_card_id": int(cid),
|
| 539 |
+
}
|
| 540 |
+
)
|
| 541 |
+
elif 760 <= i <= 819:
|
| 542 |
+
meta["type"] = "SELECT_SUCCESS_LIVE"
|
| 543 |
+
meta["index"] = i - 760
|
| 544 |
+
# Usually points to p.success_lives
|
| 545 |
+
target_p_idx = active_idx
|
| 546 |
+
if gs.pending_choices:
|
| 547 |
+
ctype, cparams = gs.pending_choices[0]
|
| 548 |
+
target_p_idx = cparams.get("player_id", active_idx)
|
| 549 |
+
target_p = gs.players[target_p_idx]
|
| 550 |
+
|
| 551 |
+
# If specific cards list provided in params, use that
|
| 552 |
+
cards = []
|
| 553 |
+
if gs.pending_choices:
|
| 554 |
+
_, cparams = gs.pending_choices[0]
|
| 555 |
+
cards = cparams.get("cards", [])
|
| 556 |
+
|
| 557 |
+
if not cards:
|
| 558 |
+
cards = target_p.success_lives
|
| 559 |
+
|
| 560 |
+
if meta["index"] < len(cards):
|
| 561 |
+
cid = cards[meta["index"]]
|
| 562 |
+
c = serialize_card(cid, member_db, live_db, energy_db)
|
| 563 |
+
meta.update(
|
| 564 |
+
{
|
| 565 |
+
"img": c["img"],
|
| 566 |
+
"name": c["name"],
|
| 567 |
+
"text": c.get("text", ""),
|
| 568 |
+
"source_card_id": int(cid),
|
| 569 |
+
}
|
| 570 |
+
)
|
| 571 |
+
elif 820 <= i <= 829:
|
| 572 |
+
meta["type"] = "TARGET_LIVE"
|
| 573 |
+
meta["index"] = i - 820
|
| 574 |
+
target_p_idx = active_idx
|
| 575 |
+
if gs.pending_choices:
|
| 576 |
+
_, cparams = gs.pending_choices[0]
|
| 577 |
+
target_p_idx = cparams.get("player_id", active_idx)
|
| 578 |
+
target_p = gs.players[target_p_idx]
|
| 579 |
+
if meta["index"] < len(target_p.live_zone):
|
| 580 |
+
cid = target_p.live_zone[meta["index"]]
|
| 581 |
+
c = serialize_card(cid, member_db, live_db, energy_db)
|
| 582 |
+
meta.update(
|
| 583 |
+
{
|
| 584 |
+
"img": c["img"],
|
| 585 |
+
"name": c["name"],
|
| 586 |
+
"text": c.get("text", ""),
|
| 587 |
+
"source_card_id": int(cid),
|
| 588 |
+
}
|
| 589 |
+
)
|
| 590 |
+
elif 830 <= i <= 849:
|
| 591 |
+
meta["type"] = "TARGET_ENERGY"
|
| 592 |
+
meta["index"] = i - 830
|
| 593 |
+
target_p_idx = active_idx
|
| 594 |
+
if gs.pending_choices:
|
| 595 |
+
_, cparams = gs.pending_choices[0]
|
| 596 |
+
target_p_idx = cparams.get("player_id", active_idx)
|
| 597 |
+
target_p = gs.players[target_p_idx]
|
| 598 |
+
if meta["index"] < len(target_p.energy_zone):
|
| 599 |
+
cid = target_p.energy_zone[meta["index"]]
|
| 600 |
+
c = serialize_card(cid, member_db, live_db, energy_db)
|
| 601 |
+
meta.update(
|
| 602 |
+
{
|
| 603 |
+
"img": c["img"],
|
| 604 |
+
"name": c["name"],
|
| 605 |
+
"text": c.get("text", ""),
|
| 606 |
+
"source_card_id": int(cid),
|
| 607 |
+
}
|
| 608 |
+
)
|
| 609 |
+
elif 850 <= i <= 909:
|
| 610 |
+
meta["type"] = "TARGET_REMOVED"
|
| 611 |
+
meta["index"] = i - 850
|
| 612 |
+
# Assuming removed_cards is on GameState
|
| 613 |
+
removed = getattr(gs, "removed_cards", [])
|
| 614 |
+
if meta["index"] < len(removed):
|
| 615 |
+
cid = removed[meta["index"]]
|
| 616 |
+
c = serialize_card(cid, member_db, live_db, energy_db)
|
| 617 |
+
meta.update(
|
| 618 |
+
{
|
| 619 |
+
"img": c["img"],
|
| 620 |
+
"name": c["name"],
|
| 621 |
+
"text": c.get("text", ""),
|
| 622 |
+
"source_card_id": int(cid),
|
| 623 |
+
}
|
| 624 |
+
)
|
| 625 |
+
|
| 626 |
+
legal_actions.append(meta)
|
| 627 |
+
|
| 628 |
+
pending_choice = None
|
| 629 |
+
if gs.pending_choices:
|
| 630 |
+
choice_type, params_raw = gs.pending_choices[0]
|
| 631 |
+
try:
|
| 632 |
+
params = json.loads(params_raw) if isinstance(params_raw, str) else params_raw
|
| 633 |
+
except:
|
| 634 |
+
params = {}
|
| 635 |
+
|
| 636 |
+
# Resolve Source Card Details
|
| 637 |
+
source_name = params.get("source_member", "Unknown")
|
| 638 |
+
source_img = None
|
| 639 |
+
source_id = params.get("source_card_id")
|
| 640 |
+
|
| 641 |
+
if source_id is not None:
|
| 642 |
+
if source_id in member_db:
|
| 643 |
+
m = member_db[source_id]
|
| 644 |
+
source_name = m.name
|
| 645 |
+
source_img = m.img_path
|
| 646 |
+
elif source_id in live_db:
|
| 647 |
+
l = live_db[source_id]
|
| 648 |
+
source_name = l.name
|
| 649 |
+
source_img = l.img_path
|
| 650 |
+
|
| 651 |
+
pending_choice = {
|
| 652 |
+
"type": choice_type,
|
| 653 |
+
"description": params.get("effect_description", ""),
|
| 654 |
+
"source_ability": params.get("source_ability", ""),
|
| 655 |
+
"source_member": source_name,
|
| 656 |
+
"source_img": source_img,
|
| 657 |
+
"source_card_id": int(source_id) if source_id is not None else -1,
|
| 658 |
+
"is_optional": params.get("is_optional", False),
|
| 659 |
+
"params": params,
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
# FINAL CHECK: Correctly indented return statement
|
| 663 |
+
return {
|
| 664 |
+
"turn": gs.turn_number,
|
| 665 |
+
"phase": int(gs.phase),
|
| 666 |
+
"active_player": int(active_idx),
|
| 667 |
+
"game_over": gs.game_over,
|
| 668 |
+
"winner": gs.winner,
|
| 669 |
+
"mode": mode,
|
| 670 |
+
"is_pvp": is_pvp,
|
| 671 |
+
"players": [
|
| 672 |
+
serialize_player(
|
| 673 |
+
gs.players[0], gs, player_idx=0, viewer_idx=viewer_idx, is_viewable=(viewer_idx == 0 or is_pvp)
|
| 674 |
+
),
|
| 675 |
+
serialize_player(
|
| 676 |
+
gs.players[1], gs, player_idx=1, viewer_idx=viewer_idx, is_viewable=(viewer_idx == 1 or is_pvp)
|
| 677 |
+
),
|
| 678 |
+
],
|
| 679 |
+
"legal_actions": legal_actions,
|
| 680 |
+
"pending_choice": pending_choice,
|
| 681 |
+
"rule_log": gs.rule_log[-200:], # Truncate log for transport
|
| 682 |
+
"performance_results": getattr(gs, "performance_results", {}),
|
| 683 |
+
"last_performance_results": getattr(gs, "last_performance_results", {}),
|
| 684 |
+
"performance_history": getattr(gs, "performance_history", []),
|
| 685 |
+
"looked_cards": [
|
| 686 |
+
serialize_card(cid, member_db, live_db, energy_db)
|
| 687 |
+
for cid in getattr(gs.get_player(active_idx), "looked_cards", [])
|
| 688 |
+
],
|
| 689 |
+
}
|
engine/game/state_utils.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import Any, Dict, Iterator
|
| 3 |
+
|
| 4 |
+
try:
|
| 5 |
+
pass
|
| 6 |
+
except:
|
| 7 |
+
pass
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class StateMixin:
|
| 13 |
+
"""Base class for checking equality of state objects."""
|
| 14 |
+
|
| 15 |
+
def __eq__(self, other):
|
| 16 |
+
return isinstance(self, type(other)) and self.__dict__ == other.__dict__
|
| 17 |
+
|
| 18 |
+
def copy_slots_to(self, target: "StateMixin") -> None:
|
| 19 |
+
"""Copy all fields defined in __slots__ to the target object."""
|
| 20 |
+
if not hasattr(self, "__slots__"):
|
| 21 |
+
return
|
| 22 |
+
|
| 23 |
+
for name in self.__slots__:
|
| 24 |
+
if hasattr(self, name):
|
| 25 |
+
val = getattr(self, name)
|
| 26 |
+
# Handle mutable types that need explicit copying
|
| 27 |
+
if isinstance(val, list):
|
| 28 |
+
setattr(target, name, val[:])
|
| 29 |
+
elif isinstance(val, set):
|
| 30 |
+
setattr(target, name, set(val))
|
| 31 |
+
elif isinstance(val, dict):
|
| 32 |
+
setattr(target, name, dict(val))
|
| 33 |
+
elif hasattr(val, "copy"):
|
| 34 |
+
setattr(target, name, val.copy())
|
| 35 |
+
else:
|
| 36 |
+
setattr(target, name, val)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# ----------------------------------------------------------------------------
|
| 40 |
+
# Unique ID (UID) System
|
| 41 |
+
# ----------------------------------------------------------------------------
|
| 42 |
+
# Masks: 20 bits for Base ID, 12 bits for Instance
|
| 43 |
+
BASE_ID_MASK = 0xFFFFF
|
| 44 |
+
INSTANCE_SHIFT = 20
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def get_base_id(uid: int) -> int:
|
| 48 |
+
"""Extract the base card definition ID from a potentially combined UID."""
|
| 49 |
+
return uid & BASE_ID_MASK
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def get_instance_index(uid: int) -> int:
|
| 53 |
+
"""Extract the instance index from a UID."""
|
| 54 |
+
return uid >> INSTANCE_SHIFT
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def create_uid(base_id: int, instance_index: int) -> int:
|
| 58 |
+
"""Create a unique instance ID from a base ID and an index."""
|
| 59 |
+
# Safety check: base_id must fit in 20 bits
|
| 60 |
+
if base_id > BASE_ID_MASK:
|
| 61 |
+
logger.warning(f"Base ID {base_id} exceeds mask {BASE_ID_MASK}!")
|
| 62 |
+
return (base_id & BASE_ID_MASK) | (instance_index << INSTANCE_SHIFT)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class MaskedDB(dict):
|
| 66 |
+
"""
|
| 67 |
+
A dictionary wrapper that automatically masks unique instance IDs (UIDs)
|
| 68 |
+
to retrieve data associated with the base ID.
|
| 69 |
+
|
| 70 |
+
This allows the game state to essentially 'ignore' the instance part of an ID
|
| 71 |
+
when looking up static card data.
|
| 72 |
+
"""
|
| 73 |
+
|
| 74 |
+
def __init__(self, data: Dict[int, Any]):
|
| 75 |
+
super().__init__(data)
|
| 76 |
+
self._data = data
|
| 77 |
+
|
| 78 |
+
def __getitem__(self, key: int) -> Any:
|
| 79 |
+
# Resolve UID to base ID
|
| 80 |
+
try:
|
| 81 |
+
base_id = int(key) & BASE_ID_MASK
|
| 82 |
+
if base_id in self._data:
|
| 83 |
+
return self._data[base_id]
|
| 84 |
+
# Fallback to string key for JSON-loaded data
|
| 85 |
+
return self._data[str(base_id)]
|
| 86 |
+
except (ValueError, TypeError):
|
| 87 |
+
raise KeyError(key)
|
| 88 |
+
|
| 89 |
+
def __contains__(self, key: object) -> bool:
|
| 90 |
+
try:
|
| 91 |
+
base_id = int(key) & BASE_ID_MASK
|
| 92 |
+
return base_id in self._data or str(base_id) in self._data
|
| 93 |
+
except (ValueError, TypeError):
|
| 94 |
+
return False
|
| 95 |
+
|
| 96 |
+
def get(self, key: int, default: Any = None) -> Any:
|
| 97 |
+
try:
|
| 98 |
+
if key is None:
|
| 99 |
+
return default
|
| 100 |
+
base_id = int(key) & BASE_ID_MASK
|
| 101 |
+
if base_id in self._data:
|
| 102 |
+
return self._data[base_id]
|
| 103 |
+
# Fallback to string key
|
| 104 |
+
return self._data.get(str(base_id), default)
|
| 105 |
+
except (ValueError, TypeError):
|
| 106 |
+
return default
|
| 107 |
+
|
| 108 |
+
def __len__(self) -> int:
|
| 109 |
+
return len(self._data)
|
| 110 |
+
|
| 111 |
+
def __iter__(self) -> Iterator[int]:
|
| 112 |
+
# Iterate over ORIGINAL base IDs (keys of _data)
|
| 113 |
+
return iter(self._data)
|
| 114 |
+
|
| 115 |
+
def keys(self):
|
| 116 |
+
return self._data.keys()
|
| 117 |
+
|
| 118 |
+
def values(self):
|
| 119 |
+
return self._data.values()
|
| 120 |
+
|
| 121 |
+
def items(self):
|
| 122 |
+
return self._data.items()
|
engine/lovecasim_engine.pyd
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4abb957604898cfdd25c8f289f9be3e4c05c841297f221046b4176144b182739
|
| 3 |
+
size 20523520
|
engine/models/__pycache__/ability.cpython-312.pyc
ADDED
|
Binary file (52.8 kB). View file
|
|
|
engine/models/__pycache__/card.cpython-312.pyc
ADDED
|
Binary file (6.07 kB). View file
|
|
|
engine/models/__pycache__/context_indices.cpython-312.pyc
ADDED
|
Binary file (1.28 kB). View file
|
|
|
engine/models/__pycache__/enums.cpython-312.pyc
ADDED
|
Binary file (6.87 kB). View file
|
|
|
engine/models/__pycache__/opcodes.cpython-312.pyc
ADDED
|
Binary file (4.05 kB). View file
|
|
|
engine/models/ability.py
ADDED
|
@@ -0,0 +1,1128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass, field
|
| 2 |
+
from enum import IntEnum
|
| 3 |
+
from typing import Any, Dict, List
|
| 4 |
+
|
| 5 |
+
from engine.models.opcodes import Opcode
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class TriggerType(IntEnum):
|
| 9 |
+
NONE = 0
|
| 10 |
+
ON_PLAY = 1 # 登場時
|
| 11 |
+
ON_LIVE_START = 2 # ライブ開始時
|
| 12 |
+
ON_LIVE_SUCCESS = 3 # ライブ成功時
|
| 13 |
+
TURN_START = 4
|
| 14 |
+
TURN_END = 5
|
| 15 |
+
CONSTANT = 6 # 常時
|
| 16 |
+
ACTIVATED = 7 # 起動
|
| 17 |
+
ON_LEAVES = 8 # 自動 - when member leaves stage/is discarded
|
| 18 |
+
ON_REVEAL = 9 # エールにより公開、公開されたとき
|
| 19 |
+
ON_POSITION_CHANGE = 10 # エリアを移動するたび
|
| 20 |
+
ON_ACTIVATE = 7 # Alias for ACTIVATED
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class TargetType(IntEnum):
|
| 24 |
+
SELF = 0
|
| 25 |
+
PLAYER = 1
|
| 26 |
+
OPPONENT = 2
|
| 27 |
+
ALL_PLAYERS = 3
|
| 28 |
+
MEMBER_SELF = 4
|
| 29 |
+
MEMBER_OTHER = 5
|
| 30 |
+
CARD_HAND = 6
|
| 31 |
+
CARD_DISCARD = 7
|
| 32 |
+
CARD_DECK_TOP = 8
|
| 33 |
+
OPPONENT_HAND = 9 # 相手の手札
|
| 34 |
+
MEMBER_SELECT = 10 # Select manual target
|
| 35 |
+
MEMBER_NAMED = 11 # Specific named member implementation
|
| 36 |
+
OPPONENT_MEMBER = 12 # Specific opponent member target
|
| 37 |
+
PLAYER_SELECT = 13 # 自分か相手を選ぶ
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class EffectType(IntEnum):
|
| 41 |
+
DRAW = 0
|
| 42 |
+
ADD_BLADES = 1
|
| 43 |
+
ADD_HEARTS = 2
|
| 44 |
+
REDUCE_COST = 3
|
| 45 |
+
LOOK_DECK = 4
|
| 46 |
+
RECOVER_LIVE = 5 # Recover Live from discard
|
| 47 |
+
BOOST_SCORE = 6
|
| 48 |
+
RECOVER_MEMBER = 7 # Recover Member from discard
|
| 49 |
+
BUFF_POWER = 8 # Generic power/heart buff
|
| 50 |
+
IMMUNITY = 9 # Cannot be targeted/chosen
|
| 51 |
+
MOVE_MEMBER = 10 # Move member to different area
|
| 52 |
+
SWAP_CARDS = 11 # Swap cards between zones
|
| 53 |
+
SEARCH_DECK = 12 # Search deck for specific card
|
| 54 |
+
ENERGY_CHARGE = 13 # Add cards to energy zone
|
| 55 |
+
SET_BLADES = 31 # Layer 4: Set blades to fixed value
|
| 56 |
+
SET_HEARTS = 32 # Layer 4: Set hearts to fixed value
|
| 57 |
+
FORMATION_CHANGE = 33 # Rule 11.10: Rearrange all members
|
| 58 |
+
NEGATE_EFFECT = 14 # Cancel/negate an effect
|
| 59 |
+
ORDER_DECK = 15 # Reorder cards in deck
|
| 60 |
+
META_RULE = 16 # Rule clarification text (no effect)
|
| 61 |
+
SELECT_MODE = 17 # Choose one of the following effects
|
| 62 |
+
MOVE_TO_DECK = 18 # Move card to top/bottom of deck
|
| 63 |
+
TAP_OPPONENT = 19 # Tap opponent's member
|
| 64 |
+
PLACE_UNDER = 20 # Place card under member
|
| 65 |
+
FLAVOR_ACTION = 99 # "Ask opponent what they like", etc.
|
| 66 |
+
RESTRICTION = 21 # Restriction on actions (Cannot Live, etc)
|
| 67 |
+
BATON_TOUCH_MOD = 22 # Modify baton touch rules (e.g. 2 members)
|
| 68 |
+
SET_SCORE = 23 # Set score to fixed value
|
| 69 |
+
SWAP_ZONE = 24 # Swap between zones (e.g. Hand <-> Live)
|
| 70 |
+
TRANSFORM_COLOR = 25 # Change all colors of type X to Y
|
| 71 |
+
REVEAL_CARDS = 26 # 公開 - reveal cards from zone
|
| 72 |
+
LOOK_AND_CHOOSE = 27 # 見る、その中から - look at cards, choose from them
|
| 73 |
+
CHEER_REVEAL = 28 # エールにより公開 - cards revealed via cheer mechanic
|
| 74 |
+
ACTIVATE_MEMBER = 29 # アクティブにする - untap/make active a member
|
| 75 |
+
ADD_TO_HAND = 30 # 手札に加える - add card to hand (from any zone)
|
| 76 |
+
COLOR_SELECT = 37 # Specify a heart color
|
| 77 |
+
REPLACE_EFFECT = 34 # Replacement effect (代わりに)
|
| 78 |
+
TRIGGER_REMOTE = 35 # Trigger ability from another zone (Cluster 5)
|
| 79 |
+
REDUCE_HEART_REQ = 36 # Need hearts reduced
|
| 80 |
+
MODIFY_SCORE_RULE = 38 # Modify how score is calculated (e.g. +1 per yell score)
|
| 81 |
+
PLAY_MEMBER_FROM_HAND = 39 # Play member from hand (e.g. Keke)
|
| 82 |
+
TAP_MEMBER = 40 # Tap a member (usually self or other on stage)
|
| 83 |
+
MOVE_TO_DISCARD = 41 # 控え室に置く
|
| 84 |
+
# --- Added to fix META_RULE fallback ---
|
| 85 |
+
GRANT_ABILITY = 42 # Grant an ability to a member
|
| 86 |
+
INCREASE_HEART_COST = 43 # Increase heart requirement
|
| 87 |
+
REDUCE_YELL_COUNT = 44 # Reduce yell count
|
| 88 |
+
PLAY_MEMBER_FROM_DISCARD = 45 # Play member from discard
|
| 89 |
+
PAY_ENERGY = 46 # Pay/tap energy as cost
|
| 90 |
+
SELECT_MEMBER = 47 # Select target member
|
| 91 |
+
DRAW_UNTIL = 48 # Draw until hand size = X
|
| 92 |
+
SELECT_PLAYER = 49 # Select target player
|
| 93 |
+
SELECT_LIVE = 50 # Select target live
|
| 94 |
+
REVEAL_UNTIL = 51 # Reveal until condition met
|
| 95 |
+
INCREASE_COST = 52 # Increase cost (e.g., play cost)
|
| 96 |
+
PREVENT_PLAY_TO_SLOT = 53 # Prevent play to specific slot
|
| 97 |
+
SWAP_AREA = 54 # Swap members between areas
|
| 98 |
+
TRANSFORM_HEART = 55 # Transform heart color/count
|
| 99 |
+
SELECT_CARDS = 56 # Select specific cards
|
| 100 |
+
OPPONENT_CHOOSE = 57 # Opponent must make a choice
|
| 101 |
+
PLAY_LIVE_FROM_DISCARD = 58 # Play Live card from discard
|
| 102 |
+
REDUCE_LIVE_SET_LIMIT = 59 # Modify limit of live cards set per turn
|
| 103 |
+
ACTIVATE_ENERGY = 81 # エネルギーをアクティブにする
|
| 104 |
+
PREVENT_ACTIVATE = 72 # Prevent activating abilities/effects
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class ConditionType(IntEnum):
|
| 108 |
+
NONE = 0
|
| 109 |
+
TURN_1 = 1 # Turn == 1
|
| 110 |
+
HAS_MEMBER = 2 # Specific member on stage
|
| 111 |
+
HAS_COLOR = 3 # Specific color on stage
|
| 112 |
+
COUNT_STAGE = 4 # Count members >= X
|
| 113 |
+
COUNT_HAND = 5
|
| 114 |
+
COUNT_DISCARD = 6
|
| 115 |
+
IS_CENTER = 7
|
| 116 |
+
LIFE_LEAD = 8
|
| 117 |
+
COUNT_GROUP = 9 # "3+ Aqours members"
|
| 118 |
+
GROUP_FILTER = 10 # Filter by group name
|
| 119 |
+
OPPONENT_HAS = 11 # Opponent has X
|
| 120 |
+
SELF_IS_GROUP = 12 # This card is from group X
|
| 121 |
+
MODAL_ANSWER = 13 # Choice/Answer branch (e.g. LL-PR-004-PR)
|
| 122 |
+
COUNT_ENERGY = 14 # エネルギーがX枚以上
|
| 123 |
+
HAS_LIVE_CARD = 15 # ライブカードがある場合
|
| 124 |
+
COST_CHECK = 16 # コストがX以下/以上
|
| 125 |
+
RARITY_CHECK = 17 # Rarity filter
|
| 126 |
+
HAND_HAS_NO_LIVE = 18 # Hand contains no live cards (usually paired with reveal cost)
|
| 127 |
+
COUNT_SUCCESS_LIVE = 19 # 成功ライブカード置き場にX枚以上
|
| 128 |
+
OPPONENT_HAND_DIFF = 20 # Opponent has more/less/diff cards in hand
|
| 129 |
+
SCORE_COMPARE = 21 # Compare scores (e.g. higher than opponent)
|
| 130 |
+
HAS_CHOICE = 22 # Ability involves a choice
|
| 131 |
+
OPPONENT_CHOICE = 23 # Opponent makes a choice (相手は~選ぶ)
|
| 132 |
+
COUNT_HEARTS = 24 # Heart count condition (ハートがX個以上)
|
| 133 |
+
COUNT_BLADES = 25 # Blade count condition (ブレードがX以上)
|
| 134 |
+
OPPONENT_ENERGY_DIFF = 26 # Opponent energy comparison
|
| 135 |
+
HAS_KEYWORD = 27 # Has specific keyword/property (e.g. Blade Heart)
|
| 136 |
+
DECK_REFRESHED = 28 # Deck was refreshed (reshuffled) this turn
|
| 137 |
+
HAS_MOVED = 29 # Member has moved this turn
|
| 138 |
+
HAND_INCREASED = 30 # Hand size increased by X cards this turn
|
| 139 |
+
COUNT_LIVE_ZONE = 31 # Number of cards in live zone
|
| 140 |
+
BATON = 32 # Baton Pass check
|
| 141 |
+
TYPE_CHECK = 33 # Card type check (member/live)
|
| 142 |
+
IS_IN_DISCARD = 34 # Check if card is in discard pile
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
# --- DESCRIPTIONS ---
|
| 146 |
+
|
| 147 |
+
EFFECT_DESCRIPTIONS = {
|
| 148 |
+
EffectType.DRAW: "Draw {value} card(s)",
|
| 149 |
+
EffectType.LOOK_DECK: "Look at top {value} card(s) of deck",
|
| 150 |
+
EffectType.ADD_BLADES: "Gain {value} Blade(s)",
|
| 151 |
+
EffectType.ADD_HEARTS: "Gain {value} Heart(s)",
|
| 152 |
+
EffectType.REDUCE_COST: "Reduce cost by {value}",
|
| 153 |
+
EffectType.BOOST_SCORE: "Boost live score by {value}",
|
| 154 |
+
EffectType.RECOVER_LIVE: "Recover {value} Live card(s) from discard",
|
| 155 |
+
EffectType.RECOVER_MEMBER: "Recover {value} Member card(s) from discard",
|
| 156 |
+
EffectType.BUFF_POWER: "Power Up {value} (Blade/Heart)",
|
| 157 |
+
EffectType.IMMUNITY: "Gain Immunity",
|
| 158 |
+
EffectType.MOVE_MEMBER: "Move Member to another zone",
|
| 159 |
+
EffectType.SWAP_CARDS: "Discard {value} card(s) then Draw {value}",
|
| 160 |
+
EffectType.SEARCH_DECK: "Search Deck",
|
| 161 |
+
EffectType.ENERGY_CHARGE: "Charge {value} Energy",
|
| 162 |
+
EffectType.SET_BLADES: "Set Blade(s) to {value}",
|
| 163 |
+
EffectType.SET_HEARTS: "Set Heart(s) to {value}",
|
| 164 |
+
EffectType.FORMATION_CHANGE: "Rearrange members on stage",
|
| 165 |
+
EffectType.NEGATE_EFFECT: "Negate effect",
|
| 166 |
+
EffectType.ORDER_DECK: "Reorder top {value} cards of deck",
|
| 167 |
+
EffectType.META_RULE: "[Rule modifier]",
|
| 168 |
+
EffectType.SELECT_MODE: "Choose One:",
|
| 169 |
+
EffectType.MOVE_TO_DECK: "Return {value} card(s) to Deck",
|
| 170 |
+
EffectType.TAP_OPPONENT: "Tap {value} Opponent Member(s)",
|
| 171 |
+
EffectType.PLACE_UNDER: "Place card under Member",
|
| 172 |
+
EffectType.RESTRICTION: "Apply Restriction",
|
| 173 |
+
EffectType.BATON_TOUCH_MOD: "Modify Baton Touch rules",
|
| 174 |
+
EffectType.SET_SCORE: "Set Live Score to {value}",
|
| 175 |
+
EffectType.REVEAL_CARDS: "Reveal {value} card(s)",
|
| 176 |
+
EffectType.LOOK_AND_CHOOSE: "Look at {value} card(s) from deck and choose",
|
| 177 |
+
EffectType.ACTIVATE_MEMBER: "Active {value} Member(s)/Energy",
|
| 178 |
+
EffectType.ADD_TO_HAND: "Add {value} card(s) to Hand",
|
| 179 |
+
EffectType.TRIGGER_REMOTE: "Trigger Remote Ability",
|
| 180 |
+
EffectType.CHEER_REVEAL: "Reveal via Cheer",
|
| 181 |
+
EffectType.REDUCE_HEART_REQ: "Modify Heart Requirement",
|
| 182 |
+
EffectType.SWAP_ZONE: "Swap card zones",
|
| 183 |
+
EffectType.FLAVOR_ACTION: "Flavor Action",
|
| 184 |
+
EffectType.MOVE_TO_DISCARD: "Move {value} card(s) to Discard",
|
| 185 |
+
EffectType.PLAY_MEMBER_FROM_HAND: "Play member from hand",
|
| 186 |
+
EffectType.TAP_MEMBER: "Tap {value} Member(s)",
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
EFFECT_DESCRIPTIONS_JP = {
|
| 190 |
+
EffectType.DRAW: "{value}枚ドロー",
|
| 191 |
+
EffectType.LOOK_DECK: "デッキの上から{value}枚見る",
|
| 192 |
+
EffectType.ADD_BLADES: "ブレード+{value}",
|
| 193 |
+
EffectType.ADD_HEARTS: "ハート+{value}",
|
| 194 |
+
EffectType.REDUCE_COST: "コスト-{value}",
|
| 195 |
+
EffectType.BOOST_SCORE: "スコア+{value}",
|
| 196 |
+
EffectType.RECOVER_LIVE: "控えライブ{value}枚回収",
|
| 197 |
+
EffectType.RECOVER_MEMBER: "控えメンバー{value}枚回収",
|
| 198 |
+
EffectType.BUFF_POWER: "パワー+{value}",
|
| 199 |
+
EffectType.IMMUNITY: "効果無効",
|
| 200 |
+
EffectType.MOVE_MEMBER: "メンバー移動",
|
| 201 |
+
EffectType.SWAP_CARDS: "手札交換({value}枚捨て{value}枚引く)",
|
| 202 |
+
EffectType.SEARCH_DECK: "デッキ検索",
|
| 203 |
+
EffectType.ENERGY_CHARGE: "エネルギーチャージ+{value}",
|
| 204 |
+
EffectType.SET_BLADES: "ブレードを{value}にセット",
|
| 205 |
+
EffectType.SET_HEARTS: "ハートを{value}にセット",
|
| 206 |
+
EffectType.FORMATION_CHANGE: "配置変更",
|
| 207 |
+
EffectType.NEGATE_EFFECT: "効果打ち消し",
|
| 208 |
+
EffectType.ORDER_DECK: "デッキトップ{value}枚並べ替��",
|
| 209 |
+
EffectType.META_RULE: "[ルール変更]",
|
| 210 |
+
EffectType.SELECT_MODE: "モード選択:",
|
| 211 |
+
EffectType.MOVE_TO_DECK: "{value}枚をデッキに戻す",
|
| 212 |
+
EffectType.TAP_OPPONENT: "相手メンバー{value}人をウェイトにする",
|
| 213 |
+
EffectType.PLACE_UNDER: "メンバーの下に置く",
|
| 214 |
+
EffectType.RESTRICTION: "プレイ制限適用",
|
| 215 |
+
EffectType.BATON_TOUCH_MOD: "バトンタッチルール変更",
|
| 216 |
+
EffectType.SET_SCORE: "ライブスコアを{value}にセット",
|
| 217 |
+
EffectType.REVEAL_CARDS: "{value}枚公開",
|
| 218 |
+
EffectType.LOOK_AND_CHOOSE: "デッキから{value}枚見て選ぶ",
|
| 219 |
+
EffectType.ACTIVATE_MEMBER: "{value}人/エネをアクティブにする",
|
| 220 |
+
EffectType.ADD_TO_HAND: "手札に{value}枚加える",
|
| 221 |
+
EffectType.TRIGGER_REMOTE: "リモート能力誘発",
|
| 222 |
+
EffectType.CHEER_REVEAL: "応援で公開",
|
| 223 |
+
EffectType.REDUCE_HEART_REQ: "ハート条件変更",
|
| 224 |
+
EffectType.SWAP_ZONE: "カード移動(ゾーン間)",
|
| 225 |
+
EffectType.FLAVOR_ACTION: "フレーバーアクション",
|
| 226 |
+
EffectType.MOVE_TO_DISCARD: "控え室に{value}枚置く",
|
| 227 |
+
EffectType.PLAY_MEMBER_FROM_HAND: "手札からメンバーを登場させる",
|
| 228 |
+
EffectType.TAP_MEMBER: "{value}人をウェイトにする",
|
| 229 |
+
EffectType.ACTIVATE_ENERGY: "エネルギーを{value}枚アクティブにする",
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
TRIGGER_DESCRIPTIONS = {
|
| 233 |
+
TriggerType.ON_PLAY: "[On Play]",
|
| 234 |
+
TriggerType.ON_LIVE_START: "[Live Start]",
|
| 235 |
+
TriggerType.ON_LIVE_SUCCESS: "[Live Success]",
|
| 236 |
+
TriggerType.TURN_START: "[Turn Start]",
|
| 237 |
+
TriggerType.TURN_END: "[Turn End - live]",
|
| 238 |
+
TriggerType.CONSTANT: "[Constant - live]",
|
| 239 |
+
TriggerType.ACTIVATED: "[Activated]",
|
| 240 |
+
TriggerType.ON_LEAVES: "[When Leaves]",
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
TRIGGER_DESCRIPTIONS_JP = {
|
| 244 |
+
TriggerType.ON_PLAY: "【登場時】",
|
| 245 |
+
TriggerType.ON_LIVE_START: "【ライブ開始時】",
|
| 246 |
+
TriggerType.ON_LIVE_SUCCESS: "【ライブ成功時】",
|
| 247 |
+
TriggerType.TURN_START: "【ターン開始時】",
|
| 248 |
+
TriggerType.TURN_END: "【ターン終了時】",
|
| 249 |
+
TriggerType.CONSTANT: "【常時】",
|
| 250 |
+
TriggerType.ACTIVATED: "【起動】",
|
| 251 |
+
TriggerType.ON_LEAVES: "【離脱時】",
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
@dataclass(slots=True)
|
| 256 |
+
class Condition:
|
| 257 |
+
type: ConditionType
|
| 258 |
+
params: Dict[str, Any] = field(default_factory=dict)
|
| 259 |
+
is_negated: bool = False # "If NOT X" / "Except X"
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
@dataclass(slots=True)
|
| 263 |
+
class Effect:
|
| 264 |
+
effect_type: EffectType
|
| 265 |
+
value: int = 0
|
| 266 |
+
value_cond: ConditionType = ConditionType.NONE
|
| 267 |
+
target: TargetType = TargetType.SELF
|
| 268 |
+
params: Dict[str, Any] = field(default_factory=dict)
|
| 269 |
+
is_optional: bool = False # ~てもよい
|
| 270 |
+
modal_options: List[List["Effect"]] = field(default_factory=list) # For SELECT_MODE
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
@dataclass(slots=True)
|
| 274 |
+
class ResolvingEffect:
|
| 275 |
+
"""Wrapper for an effect currently being resolved to track its source and progress."""
|
| 276 |
+
|
| 277 |
+
effect: Effect
|
| 278 |
+
source_card_id: int
|
| 279 |
+
step_index: int
|
| 280 |
+
total_steps: int
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
class AbilityCostType(IntEnum):
|
| 284 |
+
NONE = 0
|
| 285 |
+
ENERGY = 1
|
| 286 |
+
TAP_SELF = 2 # ウェイトにする
|
| 287 |
+
DISCARD_HAND = 3 # 手札を捨てる
|
| 288 |
+
RETURN_HAND = 4 # 手札に戻す (Self bounce)
|
| 289 |
+
SACRIFICE_SELF = 5 # このメンバーを控え室に置く
|
| 290 |
+
REVEAL_HAND_ALL = 6 # 手札をすべて公開する
|
| 291 |
+
SACRIFICE_UNDER = 7 # 下に置かれているカードを控え室に置く
|
| 292 |
+
DISCARD_ENERGY = 8 # エネルギーを控え室に置く
|
| 293 |
+
REVEAL_HAND = 9 # 手札を公開する
|
| 294 |
+
TAP_PLAYER = 2 # Alias for TAP_SELF (ウェイトにする)
|
| 295 |
+
|
| 296 |
+
# Missing aliases/members inferred from usage
|
| 297 |
+
TAP_MEMBER = 20
|
| 298 |
+
TAP_ENERGY = 21
|
| 299 |
+
PAY_ENERGY = 1 # Alias for ENERGY
|
| 300 |
+
REST_MEMBER = 22
|
| 301 |
+
RETURN_MEMBER_TO_HAND = 23
|
| 302 |
+
DISCARD_MEMBER = 24
|
| 303 |
+
DISCARD_LIVE = 25
|
| 304 |
+
REMOVE_LIVE = 26
|
| 305 |
+
REMOVE_MEMBER = 27
|
| 306 |
+
RETURN_LIVE_TO_HAND = 28
|
| 307 |
+
RETURN_LIVE_TO_DECK = 29
|
| 308 |
+
RETURN_MEMBER_TO_DECK = 30
|
| 309 |
+
PLACE_MEMBER_FROM_HAND = 31
|
| 310 |
+
PLACE_LIVE_FROM_HAND = 32
|
| 311 |
+
PLACE_ENERGY_FROM_HAND = 33
|
| 312 |
+
PLACE_MEMBER_FROM_DISCARD = 34
|
| 313 |
+
PLACE_LIVE_FROM_DISCARD = 35
|
| 314 |
+
PLACE_ENERGY_FROM_DISCARD = 36
|
| 315 |
+
PLACE_MEMBER_FROM_DECK = 37
|
| 316 |
+
PLACE_LIVE_FROM_DECK = 38
|
| 317 |
+
PLACE_ENERGY_FROM_DECK = 39
|
| 318 |
+
# REVEAL_HAND = 40 # Moved to 9
|
| 319 |
+
SHUFFLE_DECK = 41
|
| 320 |
+
DRAW_CARD = 42
|
| 321 |
+
DISCARD_TOP_DECK = 43
|
| 322 |
+
REMOVE_TOP_DECK = 44
|
| 323 |
+
RETURN_DISCARD_TO_DECK = 45
|
| 324 |
+
RETURN_REMOVED_TO_DECK = 46
|
| 325 |
+
RETURN_REMOVED_TO_HAND = 47
|
| 326 |
+
RETURN_REMOVED_TO_DISCARD = 48
|
| 327 |
+
PLACE_ENERGY_FROM_SUCCESS = 49
|
| 328 |
+
DISCARD_SUCCESS_LIVE = 50
|
| 329 |
+
REMOVE_SUCCESS_LIVE = 51
|
| 330 |
+
RETURN_SUCCESS_LIVE_TO_HAND = 52
|
| 331 |
+
RETURN_SUCCESS_LIVE_TO_DECK = 53
|
| 332 |
+
RETURN_SUCCESS_LIVE_TO_DISCARD = 54
|
| 333 |
+
PLACE_MEMBER_FROM_SUCCESS = 55
|
| 334 |
+
PLACE_LIVE_FROM_SUCCESS = 56
|
| 335 |
+
PLACE_ENERGY_FROM_REMOVED = 57
|
| 336 |
+
PLACE_MEMBER_FROM_REMOVED = 58
|
| 337 |
+
PLACE_LIVE_FROM_REMOVED = 59
|
| 338 |
+
RETURN_ENERGY_TO_DECK = 60
|
| 339 |
+
RETURN_ENERGY_TO_HAND = 61
|
| 340 |
+
REMOVE_ENERGY = 62
|
| 341 |
+
RETURN_STAGE_ENERGY_TO_HAND = 64
|
| 342 |
+
DISCARD_STAGE_ENERGY = 65
|
| 343 |
+
REMOVE_STAGE_ENERGY = 66
|
| 344 |
+
DISCARD_STAGE = 65 # Alias for DISCARD_STAGE_ENERGY (often used for members/energy)
|
| 345 |
+
MOVE_TO_DISCARD = 5 # Common alias for sacrifice/discard
|
| 346 |
+
PLACE_ENERGY_FROM_STAGE_ENERGY = 67
|
| 347 |
+
PLACE_MEMBER_FROM_STAGE_ENERGY = 68
|
| 348 |
+
PLACE_LIVE_FROM_STAGE_ENERGY = 69
|
| 349 |
+
PLACE_ENERGY_FROM_HAND_TO_STAGE_ENERGY = 70
|
| 350 |
+
PLACE_MEMBER_FROM_HAND_TO_STAGE_ENERGY = 71
|
| 351 |
+
PLACE_LIVE_FROM_HAND_TO_STAGE_ENERGY = 72
|
| 352 |
+
PLACE_ENERGY_FROM_DISCARD_TO_STAGE_ENERGY = 73
|
| 353 |
+
PLACE_MEMBER_FROM_DISCARD_TO_STAGE_ENERGY = 74
|
| 354 |
+
PLACE_LIVE_FROM_DISCARD_TO_STAGE_ENERGY = 75
|
| 355 |
+
PLACE_ENERGY_FROM_DECK_TO_STAGE_ENERGY = 76
|
| 356 |
+
PLACE_MEMBER_FROM_DECK_TO_STAGE_ENERGY = 77
|
| 357 |
+
PLACE_LIVE_FROM_DECK_TO_STAGE_ENERGY = 78
|
| 358 |
+
PLACE_ENERGY_FROM_SUCCESS_TO_STAGE_ENERGY = 79
|
| 359 |
+
PLACE_MEMBER_FROM_SUCCESS_TO_STAGE_ENERGY = 80
|
| 360 |
+
PLACE_LIVE_FROM_SUCCESS_TO_STAGE_ENERGY = 81
|
| 361 |
+
PLACE_ENERGY_FROM_REMOVED_TO_STAGE_ENERGY = 82
|
| 362 |
+
PLACE_MEMBER_FROM_REMOVED_TO_STAGE_ENERGY = 83
|
| 363 |
+
PLACE_LIVE_FROM_REMOVED_TO_STAGE_ENERGY = 84
|
| 364 |
+
RETURN_LIVE_TO_DISCARD = 85
|
| 365 |
+
RETURN_LIVE_TO_REMOVED = 86
|
| 366 |
+
RETURN_LIVE_TO_SUCCESS = 87
|
| 367 |
+
RETURN_MEMBER_TO_DISCARD = 88
|
| 368 |
+
RETURN_MEMBER_TO_REMOVED = 89
|
| 369 |
+
RETURN_MEMBER_TO_SUCCESS = 90
|
| 370 |
+
RETURN_ENERGY_TO_DISCARD = 91
|
| 371 |
+
RETURN_ENERGY_TO_REMOVED = 92
|
| 372 |
+
RETURN_ENERGY_TO_SUCCESS = 93
|
| 373 |
+
RETURN_SUCCESS_LIVE_TO_REMOVED = 94
|
| 374 |
+
RETURN_REMOVED_TO_SUCCESS = 95
|
| 375 |
+
RETURN_STAGE_ENERGY_TO_DISCARD = 96
|
| 376 |
+
RETURN_STAGE_ENERGY_TO_REMOVED = 97
|
| 377 |
+
RETURN_STAGE_ENERGY_TO_SUCCESS = 98
|
| 378 |
+
RETURN_DISCARD_TO_HAND = 99
|
| 379 |
+
RETURN_DISCARD_TO_REMOVED = 100
|
| 380 |
+
RETURN_DISCARD_TO_SUCCESS = 101
|
| 381 |
+
RETURN_DECK_TO_DISCARD = 102
|
| 382 |
+
RETURN_DECK_TO_HAND = 103
|
| 383 |
+
RETURN_DECK_TO_REMOVED = 104
|
| 384 |
+
RETURN_DECK_TO_SUCCESS = 105
|
| 385 |
+
RETURN_ENERGY_DECK_TO_DISCARD = 106
|
| 386 |
+
RETURN_ENERGY_DECK_TO_HAND = 107
|
| 387 |
+
RETURN_ENERGY_DECK_TO_REMOVED = 108
|
| 388 |
+
RETURN_ENERGY_DECK_TO_SUCCESS = 109
|
| 389 |
+
|
| 390 |
+
# Auto-generated missing members for effect_mixin.py compatibility
|
| 391 |
+
PLACE_ENERGY_FROM_DECK_TO_DISCARD = 110
|
| 392 |
+
PLACE_ENERGY_FROM_DECK_TO_HAND = 111
|
| 393 |
+
PLACE_ENERGY_FROM_DECK_TO_REMOVED = 112
|
| 394 |
+
PLACE_ENERGY_FROM_DECK_TO_SUCCESS = 113
|
| 395 |
+
PLACE_ENERGY_FROM_DISCARD_TO_HAND = 114
|
| 396 |
+
PLACE_ENERGY_FROM_DISCARD_TO_REMOVED = 115
|
| 397 |
+
PLACE_ENERGY_FROM_DISCARD_TO_SUCCESS = 116
|
| 398 |
+
PLACE_ENERGY_FROM_ENERGY_DECK = 117
|
| 399 |
+
PLACE_ENERGY_FROM_ENERGY_DECK_TO_DISCARD = 118
|
| 400 |
+
PLACE_ENERGY_FROM_ENERGY_DECK_TO_HAND = 119
|
| 401 |
+
PLACE_ENERGY_FROM_ENERGY_DECK_TO_REMOVED = 120
|
| 402 |
+
PLACE_ENERGY_FROM_ENERGY_DECK_TO_STAGE_ENERGY = 121
|
| 403 |
+
PLACE_ENERGY_FROM_ENERGY_DECK_TO_SUCCESS = 122
|
| 404 |
+
PLACE_ENERGY_FROM_ENERGY_ZONE_TO_DISCARD = 123
|
| 405 |
+
PLACE_ENERGY_FROM_ENERGY_ZONE_TO_HAND = 124
|
| 406 |
+
PLACE_ENERGY_FROM_ENERGY_ZONE_TO_REMOVED = 125
|
| 407 |
+
PLACE_ENERGY_FROM_ENERGY_ZONE_TO_SUCCESS = 126
|
| 408 |
+
PLACE_ENERGY_FROM_HAND_TO_DISCARD = 127
|
| 409 |
+
PLACE_ENERGY_FROM_HAND_TO_REMOVED = 128
|
| 410 |
+
PLACE_ENERGY_FROM_HAND_TO_SUCCESS = 129
|
| 411 |
+
PLACE_ENERGY_FROM_REMOVED_TO_DISCARD = 130
|
| 412 |
+
PLACE_ENERGY_FROM_REMOVED_TO_HAND = 131
|
| 413 |
+
PLACE_ENERGY_FROM_REMOVED_TO_SUCCESS = 132
|
| 414 |
+
PLACE_ENERGY_FROM_STAGE_ENERGY_TO_DISCARD = 133
|
| 415 |
+
PLACE_ENERGY_FROM_STAGE_ENERGY_TO_HAND = 134
|
| 416 |
+
PLACE_ENERGY_FROM_STAGE_ENERGY_TO_REMOVED = 135
|
| 417 |
+
PLACE_ENERGY_FROM_STAGE_ENERGY_TO_SUCCESS = 136
|
| 418 |
+
PLACE_ENERGY_FROM_SUCCESS_TO_DISCARD = 137
|
| 419 |
+
PLACE_ENERGY_FROM_SUCCESS_TO_HAND = 138
|
| 420 |
+
PLACE_ENERGY_FROM_SUCCESS_TO_REMOVED = 139
|
| 421 |
+
PLACE_LIVE_FROM_DECK_TO_DISCARD = 140
|
| 422 |
+
PLACE_LIVE_FROM_DECK_TO_HAND = 141
|
| 423 |
+
PLACE_LIVE_FROM_DECK_TO_REMOVED = 142
|
| 424 |
+
PLACE_LIVE_FROM_DECK_TO_SUCCESS = 143
|
| 425 |
+
PLACE_LIVE_FROM_DISCARD_TO_HAND = 144
|
| 426 |
+
PLACE_LIVE_FROM_DISCARD_TO_REMOVED = 145
|
| 427 |
+
PLACE_LIVE_FROM_DISCARD_TO_SUCCESS = 146
|
| 428 |
+
PLACE_LIVE_FROM_ENERGY_DECK = 147
|
| 429 |
+
PLACE_LIVE_FROM_ENERGY_DECK_TO_DISCARD = 148
|
| 430 |
+
PLACE_LIVE_FROM_ENERGY_DECK_TO_HAND = 149
|
| 431 |
+
PLACE_LIVE_FROM_ENERGY_DECK_TO_REMOVED = 150
|
| 432 |
+
PLACE_LIVE_FROM_ENERGY_DECK_TO_STAGE_ENERGY = 151
|
| 433 |
+
PLACE_LIVE_FROM_ENERGY_DECK_TO_SUCCESS = 152
|
| 434 |
+
PLACE_LIVE_FROM_ENERGY_ZONE_TO_DISCARD = 153
|
| 435 |
+
PLACE_LIVE_FROM_ENERGY_ZONE_TO_HAND = 154
|
| 436 |
+
PLACE_LIVE_FROM_ENERGY_ZONE_TO_REMOVED = 155
|
| 437 |
+
PLACE_LIVE_FROM_ENERGY_ZONE_TO_SUCCESS = 156
|
| 438 |
+
PLACE_LIVE_FROM_HAND_TO_DISCARD = 157
|
| 439 |
+
PLACE_LIVE_FROM_HAND_TO_REMOVED = 158
|
| 440 |
+
PLACE_LIVE_FROM_HAND_TO_SUCCESS = 159
|
| 441 |
+
PLACE_LIVE_FROM_REMOVED_TO_DISCARD = 160
|
| 442 |
+
PLACE_LIVE_FROM_REMOVED_TO_HAND = 161
|
| 443 |
+
PLACE_LIVE_FROM_REMOVED_TO_SUCCESS = 162
|
| 444 |
+
PLACE_LIVE_FROM_STAGE_ENERGY_TO_DISCARD = 163
|
| 445 |
+
PLACE_LIVE_FROM_STAGE_ENERGY_TO_HAND = 164
|
| 446 |
+
PLACE_LIVE_FROM_STAGE_ENERGY_TO_REMOVED = 165
|
| 447 |
+
PLACE_LIVE_FROM_STAGE_ENERGY_TO_SUCCESS = 166
|
| 448 |
+
PLACE_LIVE_FROM_SUCCESS_TO_DISCARD = 167
|
| 449 |
+
PLACE_LIVE_FROM_SUCCESS_TO_HAND = 168
|
| 450 |
+
PLACE_LIVE_FROM_SUCCESS_TO_REMOVED = 169
|
| 451 |
+
PLACE_MEMBER_FROM_DECK_TO_DISCARD = 170
|
| 452 |
+
PLACE_MEMBER_FROM_DECK_TO_HAND = 171
|
| 453 |
+
PLACE_MEMBER_FROM_DECK_TO_REMOVED = 172
|
| 454 |
+
PLACE_MEMBER_FROM_DECK_TO_SUCCESS = 173
|
| 455 |
+
PLACE_MEMBER_FROM_DISCARD_TO_HAND = 174
|
| 456 |
+
PLACE_MEMBER_FROM_DISCARD_TO_REMOVED = 175
|
| 457 |
+
PLACE_MEMBER_FROM_DISCARD_TO_SUCCESS = 176
|
| 458 |
+
PLACE_MEMBER_FROM_ENERGY_DECK = 177
|
| 459 |
+
PLACE_MEMBER_FROM_ENERGY_DECK_TO_DISCARD = 178
|
| 460 |
+
PLACE_MEMBER_FROM_ENERGY_DECK_TO_HAND = 179
|
| 461 |
+
PLACE_MEMBER_FROM_ENERGY_DECK_TO_REMOVED = 180
|
| 462 |
+
PLACE_MEMBER_FROM_ENERGY_DECK_TO_STAGE_ENERGY = 181
|
| 463 |
+
PLACE_MEMBER_FROM_ENERGY_DECK_TO_SUCCESS = 182
|
| 464 |
+
PLACE_MEMBER_FROM_ENERGY_ZONE_TO_DISCARD = 183
|
| 465 |
+
PLACE_MEMBER_FROM_ENERGY_ZONE_TO_HAND = 184
|
| 466 |
+
PLACE_MEMBER_FROM_ENERGY_ZONE_TO_REMOVED = 185
|
| 467 |
+
PLACE_MEMBER_FROM_ENERGY_ZONE_TO_SUCCESS = 186
|
| 468 |
+
PLACE_MEMBER_FROM_HAND_TO_DISCARD = 187
|
| 469 |
+
PLACE_MEMBER_FROM_HAND_TO_REMOVED = 188
|
| 470 |
+
PLACE_MEMBER_FROM_HAND_TO_SUCCESS = 189
|
| 471 |
+
PLACE_MEMBER_FROM_REMOVED_TO_DISCARD = 190
|
| 472 |
+
PLACE_MEMBER_FROM_REMOVED_TO_HAND = 191
|
| 473 |
+
PLACE_MEMBER_FROM_REMOVED_TO_SUCCESS = 192
|
| 474 |
+
PLACE_MEMBER_FROM_STAGE_ENERGY_TO_DISCARD = 193
|
| 475 |
+
PLACE_MEMBER_FROM_STAGE_ENERGY_TO_HAND = 194
|
| 476 |
+
PLACE_MEMBER_FROM_STAGE_ENERGY_TO_REMOVED = 195
|
| 477 |
+
PLACE_MEMBER_FROM_STAGE_ENERGY_TO_SUCCESS = 196
|
| 478 |
+
PLACE_MEMBER_FROM_SUCCESS_TO_DISCARD = 197
|
| 479 |
+
PLACE_MEMBER_FROM_SUCCESS_TO_HAND = 198
|
| 480 |
+
PLACE_MEMBER_FROM_SUCCESS_TO_REMOVED = 199
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
@dataclass
|
| 484 |
+
class Cost:
|
| 485 |
+
type: AbilityCostType
|
| 486 |
+
value: int = 0
|
| 487 |
+
params: Dict[str, Any] = field(default_factory=dict)
|
| 488 |
+
is_optional: bool = False
|
| 489 |
+
|
| 490 |
+
@property
|
| 491 |
+
def cost_type(self) -> AbilityCostType:
|
| 492 |
+
return self.type
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
@dataclass
|
| 496 |
+
class Ability:
|
| 497 |
+
raw_text: str
|
| 498 |
+
trigger: TriggerType
|
| 499 |
+
effects: List[Effect]
|
| 500 |
+
conditions: List[Condition] = field(default_factory=list)
|
| 501 |
+
costs: List[Cost] = field(default_factory=list)
|
| 502 |
+
modal_options: List[List[Effect]] = field(default_factory=list) # For SELECT_MODE
|
| 503 |
+
is_once_per_turn: bool = False
|
| 504 |
+
bytecode: List[int] = field(default_factory=list)
|
| 505 |
+
requires_selection: bool = False
|
| 506 |
+
# Ordered list of operations (Union[Effect, Condition]) for precise execution order
|
| 507 |
+
instructions: List[Any] = field(default_factory=list)
|
| 508 |
+
|
| 509 |
+
def compile(self) -> List[int]:
|
| 510 |
+
"""Compile ability into fixed-width bytecode sequence (groups of 4 ints)."""
|
| 511 |
+
bytecode = []
|
| 512 |
+
|
| 513 |
+
# 0. Compile Ordered Instructions (If present - New Parser V2.1)
|
| 514 |
+
if self.instructions:
|
| 515 |
+
for instr in self.instructions:
|
| 516 |
+
if isinstance(instr, Condition):
|
| 517 |
+
self._compile_single_condition(instr, bytecode)
|
| 518 |
+
elif isinstance(instr, Effect):
|
| 519 |
+
self._compile_effect_wrapper(instr, bytecode)
|
| 520 |
+
|
| 521 |
+
# Terminator
|
| 522 |
+
bytecode.extend([int(Opcode.RETURN), 0, 0, 0])
|
| 523 |
+
return bytecode
|
| 524 |
+
|
| 525 |
+
# 1. Compile Conditions (Legacy/Split Mode)
|
| 526 |
+
for cond in self.conditions:
|
| 527 |
+
self._compile_single_condition(cond, bytecode)
|
| 528 |
+
|
| 529 |
+
# 1.5. Compile Costs (Note: Modern engine handles costs via pay_cost shell)
|
| 530 |
+
# We don't compile costs into bytecode unless they are meant for mid-ability execution.
|
| 531 |
+
|
| 532 |
+
# 2. Compile Effects
|
| 533 |
+
for eff in self.effects:
|
| 534 |
+
self._compile_effect_wrapper(eff, bytecode)
|
| 535 |
+
|
| 536 |
+
# Terminator
|
| 537 |
+
bytecode.extend([int(Opcode.RETURN), 0, 0, 0])
|
| 538 |
+
return bytecode
|
| 539 |
+
|
| 540 |
+
def _compile_single_condition(self, cond: Condition, bytecode: List[int]):
|
| 541 |
+
op_name = f"CHECK_{cond.type.name}"
|
| 542 |
+
if hasattr(Opcode, op_name):
|
| 543 |
+
op = getattr(Opcode, op_name)
|
| 544 |
+
# Fixed width: [Opcode, Value, Attr, TargetSlot]
|
| 545 |
+
# Check multiple potential keys for the value (min, count, value, diff)
|
| 546 |
+
v_raw = cond.params.get("value", cond.params.get("min", cond.params.get("count", cond.params.get("diff", 0))))
|
| 547 |
+
try:
|
| 548 |
+
val = int(v_raw) if v_raw is not None else 0
|
| 549 |
+
except (ValueError, TypeError):
|
| 550 |
+
val = 0
|
| 551 |
+
|
| 552 |
+
# Resolve attr (color, group, or unit) to integer
|
| 553 |
+
attr_raw = cond.params.get("color", cond.params.get("group", cond.params.get("unit", 0)))
|
| 554 |
+
if isinstance(attr_raw, str):
|
| 555 |
+
# Resolve using enums
|
| 556 |
+
if "group" in cond.params:
|
| 557 |
+
from engine.models.enums import Group
|
| 558 |
+
|
| 559 |
+
attr = int(Group.from_japanese_name(attr_raw))
|
| 560 |
+
elif "unit" in cond.params:
|
| 561 |
+
from engine.models.enums import Unit
|
| 562 |
+
|
| 563 |
+
attr = int(Unit.from_japanese_name(attr_raw))
|
| 564 |
+
elif cond.type == ConditionType.SCORE_COMPARE:
|
| 565 |
+
# Map score/cost/heart types to int
|
| 566 |
+
stype = cond.params.get("type", "score")
|
| 567 |
+
type_map = {"score": 0, "cost": 1, "heart": 2, "heart_count": 2, "cheer_count": 3}
|
| 568 |
+
attr = type_map.get(stype, 0)
|
| 569 |
+
else:
|
| 570 |
+
attr = 0
|
| 571 |
+
else:
|
| 572 |
+
attr = int(attr_raw) if attr_raw is not None else 0
|
| 573 |
+
|
| 574 |
+
# Comparison mapping (GE=0, LE=1, GT=2, LT=3, EQ=4)
|
| 575 |
+
comp_str = cond.params.get("comparison", "GE")
|
| 576 |
+
comp_map = {"GE": 0, "LE": 1, "GT": 2, "LT": 3, "EQ": 4}
|
| 577 |
+
comp_val = comp_map.get(comp_str, 0)
|
| 578 |
+
|
| 579 |
+
# Zone mapping: STAGE=0, LIVE_ZONE=1, LIVE_RESULT/EXCESS=2
|
| 580 |
+
slot = 0
|
| 581 |
+
zone = cond.params.get("zone", "")
|
| 582 |
+
context = cond.params.get("context", "")
|
| 583 |
+
|
| 584 |
+
if zone == "LIVE_ZONE":
|
| 585 |
+
slot = 1
|
| 586 |
+
elif zone == "STAGE":
|
| 587 |
+
slot = 0
|
| 588 |
+
elif context == "excess":
|
| 589 |
+
slot = 2
|
| 590 |
+
else:
|
| 591 |
+
slot = cond.params.get("TargetSlot", 0)
|
| 592 |
+
|
| 593 |
+
# Pack comparison into higher bits of slot (bits 4-7)
|
| 594 |
+
# Slot is usually 0-3, so shift 4 is safe.
|
| 595 |
+
packed_slot = (slot & 0x0F) | ((comp_val & 0x0F) << 4)
|
| 596 |
+
|
| 597 |
+
op_val = int(op)
|
| 598 |
+
if cond.is_negated:
|
| 599 |
+
op_val += 1000
|
| 600 |
+
|
| 601 |
+
bytecode.extend(
|
| 602 |
+
[
|
| 603 |
+
op_val,
|
| 604 |
+
val,
|
| 605 |
+
attr,
|
| 606 |
+
packed_slot,
|
| 607 |
+
]
|
| 608 |
+
)
|
| 609 |
+
|
| 610 |
+
elif cond.type == ConditionType.BATON:
|
| 611 |
+
# Special handling for BATON condition
|
| 612 |
+
if hasattr(Opcode, "CHECK_BATON"):
|
| 613 |
+
unit_id = 0
|
| 614 |
+
if "unit" in cond.params:
|
| 615 |
+
from engine.models.enums import Unit
|
| 616 |
+
|
| 617 |
+
try:
|
| 618 |
+
# Handle string unit names
|
| 619 |
+
u_val = cond.params["unit"]
|
| 620 |
+
if isinstance(u_val, str):
|
| 621 |
+
unit_id = int(Unit.from_japanese_name(u_val))
|
| 622 |
+
else:
|
| 623 |
+
unit_id = int(u_val)
|
| 624 |
+
except:
|
| 625 |
+
unit_id = 0
|
| 626 |
+
|
| 627 |
+
filter_type = 0
|
| 628 |
+
if "filter" in cond.params:
|
| 629 |
+
f_str = cond.params["filter"]
|
| 630 |
+
if f_str == "COST_LT_SELF":
|
| 631 |
+
filter_type = 1 # 1 = Cost Check Less Than Self
|
| 632 |
+
|
| 633 |
+
bytecode.extend([int(Opcode.CHECK_BATON), unit_id, filter_type, 0])
|
| 634 |
+
|
| 635 |
+
elif cond.type == ConditionType.TYPE_CHECK:
|
| 636 |
+
if hasattr(Opcode, "CHECK_TYPE_CHECK"):
|
| 637 |
+
# card_type: "live" = 1, "member" = 0
|
| 638 |
+
ctype = 1 if cond.params.get("card_type") == "live" else 0
|
| 639 |
+
bytecode.extend([int(Opcode.CHECK_TYPE_CHECK), ctype, 0, 0])
|
| 640 |
+
|
| 641 |
+
def _compile_effect_wrapper(self, eff: Effect, bytecode: List[int]):
|
| 642 |
+
# Fix: Use name comparison to avoid Enum identity issues from reloading/imports
|
| 643 |
+
if eff.effect_type.name == "ORDER_DECK":
|
| 644 |
+
# O_ORDER_DECK requires looking at cards first.
|
| 645 |
+
# Emit: [O_LOOK_DECK, val, 0, 0] -> [O_ORDER_DECK, val, attr, 0]
|
| 646 |
+
# attr: 0=Discard, 1=DeckTop, 2=DeckBottom
|
| 647 |
+
rem = eff.params.get("remainder", "discard").lower()
|
| 648 |
+
attr = 0
|
| 649 |
+
if rem == "deck_top":
|
| 650 |
+
attr = 1
|
| 651 |
+
elif rem == "deck_bottom":
|
| 652 |
+
attr = 2
|
| 653 |
+
|
| 654 |
+
bytecode.extend([int(Opcode.LOOK_DECK), eff.value, 0, 0])
|
| 655 |
+
bytecode.extend([int(Opcode.ORDER_DECK), eff.value, attr, 0])
|
| 656 |
+
return
|
| 657 |
+
|
| 658 |
+
# Check for modal options on Effect OR Ability (fallback)
|
| 659 |
+
modal_opts = eff.modal_options if eff.modal_options else self.modal_options
|
| 660 |
+
|
| 661 |
+
if eff.effect_type == EffectType.SELECT_MODE and modal_opts:
|
| 662 |
+
# Handle SELECT_MODE with jump table
|
| 663 |
+
num_options = len(modal_opts)
|
| 664 |
+
# Emit header: [SELECT_MODE, NumOptions, 0, 0]
|
| 665 |
+
if hasattr(Opcode, "SELECT_MODE"):
|
| 666 |
+
bytecode.extend([int(Opcode.SELECT_MODE), num_options, 0, 0])
|
| 667 |
+
|
| 668 |
+
# Placeholders for Jump Table
|
| 669 |
+
jump_table_start_idx = len(bytecode)
|
| 670 |
+
for _ in range(num_options):
|
| 671 |
+
bytecode.extend([int(Opcode.JUMP), 0, 0, 0])
|
| 672 |
+
|
| 673 |
+
# Compile each option and track start/end
|
| 674 |
+
option_start_offsets = []
|
| 675 |
+
end_jumps_locations = []
|
| 676 |
+
|
| 677 |
+
for opt_effects in modal_opts:
|
| 678 |
+
# Record start offset (relative to current instruction pointer)
|
| 679 |
+
current_idx = len(bytecode) // 4
|
| 680 |
+
option_start_offsets.append(current_idx)
|
| 681 |
+
|
| 682 |
+
# Compile option effects
|
| 683 |
+
for opt_eff in opt_effects:
|
| 684 |
+
self._compile_single_effect(opt_eff, bytecode)
|
| 685 |
+
|
| 686 |
+
# Add Jump to End (placeholder)
|
| 687 |
+
end_jumps_locations.append(len(bytecode))
|
| 688 |
+
bytecode.extend([int(Opcode.JUMP), 0, 0, 0])
|
| 689 |
+
|
| 690 |
+
# Determine End Index
|
| 691 |
+
end_idx = len(bytecode) // 4
|
| 692 |
+
|
| 693 |
+
# Patch Jump Table (Start Jumps)
|
| 694 |
+
for i in range(num_options):
|
| 695 |
+
jump_instr_idx = (jump_table_start_idx // 4) + i
|
| 696 |
+
target_idx = option_start_offsets[i]
|
| 697 |
+
offset = target_idx - jump_instr_idx
|
| 698 |
+
bytecode[jump_instr_idx * 4 + 1] = offset
|
| 699 |
+
|
| 700 |
+
# Patch End Jumps
|
| 701 |
+
for loc in end_jumps_locations:
|
| 702 |
+
jump_instr_idx = loc // 4
|
| 703 |
+
offset = end_idx - jump_instr_idx
|
| 704 |
+
bytecode[loc + 1] = offset
|
| 705 |
+
|
| 706 |
+
else:
|
| 707 |
+
self._compile_single_effect(eff, bytecode)
|
| 708 |
+
|
| 709 |
+
def _compile_single_effect(self, eff: Effect, bytecode: List[int]):
|
| 710 |
+
print(f"DEBUG: Compiling Single Effect: {eff.effect_type.name} (Val={eff.value})")
|
| 711 |
+
if hasattr(Opcode, eff.effect_type.name):
|
| 712 |
+
op = getattr(Opcode, eff.effect_type.name)
|
| 713 |
+
|
| 714 |
+
try:
|
| 715 |
+
val = int(eff.value)
|
| 716 |
+
except (ValueError, TypeError):
|
| 717 |
+
val = 1
|
| 718 |
+
attr = eff.params.get("color", 0) if isinstance(eff.params.get("color"), int) else 0
|
| 719 |
+
slot = eff.target.value if hasattr(eff.target, "value") else int(eff.target)
|
| 720 |
+
|
| 721 |
+
# Check for interactive target selection requirement
|
| 722 |
+
# Use Bit 5 (0x20) in attr to flag "Requires Selection"
|
| 723 |
+
if eff.effect_type == EffectType.TAP_OPPONENT:
|
| 724 |
+
attr |= 1 << 5
|
| 725 |
+
|
| 726 |
+
# Special handling for PLACE_UNDER params
|
| 727 |
+
if eff.effect_type == EffectType.PLACE_UNDER:
|
| 728 |
+
if eff.params.get("from") == "energy":
|
| 729 |
+
attr = 1 # Source: Energy
|
| 730 |
+
elif eff.params.get("from") == "discard":
|
| 731 |
+
attr = 2 # Source: Discard (for future proofing)
|
| 732 |
+
# Keep existing type logic if any, but currently PLACE_UNDER uses attr for source
|
| 733 |
+
|
| 734 |
+
# Special handling for SEARCH_DECK params
|
| 735 |
+
if eff.effect_type == EffectType.SEARCH_DECK:
|
| 736 |
+
if "group" in eff.params:
|
| 737 |
+
slot = 1 # Filter Type: Group
|
| 738 |
+
try:
|
| 739 |
+
attr = int(eff.params["group"])
|
| 740 |
+
except:
|
| 741 |
+
pass
|
| 742 |
+
elif "unit" in eff.params:
|
| 743 |
+
slot = 2 # Filter Type: Unit
|
| 744 |
+
try:
|
| 745 |
+
attr = int(eff.params["unit"])
|
| 746 |
+
except:
|
| 747 |
+
pass
|
| 748 |
+
|
| 749 |
+
# Special handling for PLAY_MEMBER_FROM_HAND params
|
| 750 |
+
if eff.effect_type == EffectType.PLAY_MEMBER_FROM_HAND:
|
| 751 |
+
if "group" in eff.params:
|
| 752 |
+
from engine.models.enums import Group
|
| 753 |
+
|
| 754 |
+
try:
|
| 755 |
+
attr = int(Group.from_japanese_name(eff.params["group"]))
|
| 756 |
+
except:
|
| 757 |
+
pass
|
| 758 |
+
|
| 759 |
+
# Special handling for LOOK_AND_CHOOSE destination
|
| 760 |
+
if eff.effect_type == EffectType.LOOK_AND_CHOOSE:
|
| 761 |
+
if eff.params.get("source") == "HAND":
|
| 762 |
+
slot = int(TargetType.CARD_HAND)
|
| 763 |
+
|
| 764 |
+
attr = 0
|
| 765 |
+
if eff.params.get("destination") == "discard":
|
| 766 |
+
attr |= 0x01 # Bit 0: Destination Discard
|
| 767 |
+
if eff.is_optional or eff.params.get("is_optional"):
|
| 768 |
+
attr |= 0x02 # Bit 1: Optional (May)
|
| 769 |
+
|
| 770 |
+
# Parse 'filter' string if present (e.g. "GROUP_ID=0, TYPE_LIVE")
|
| 771 |
+
if "filter" in eff.params:
|
| 772 |
+
filter_str = str(eff.params["filter"])
|
| 773 |
+
parts = [p.strip() for p in filter_str.split(",")]
|
| 774 |
+
for part in parts:
|
| 775 |
+
if "=" in part:
|
| 776 |
+
k, v = part.split("=", 1)
|
| 777 |
+
k, v = k.strip().upper(), v.strip()
|
| 778 |
+
if k == "GROUP_ID":
|
| 779 |
+
eff.params["group"] = v
|
| 780 |
+
if k == "TYPE":
|
| 781 |
+
eff.params["type"] = v
|
| 782 |
+
else:
|
| 783 |
+
if part.upper() == "TYPE_LIVE":
|
| 784 |
+
eff.params["type"] = "live"
|
| 785 |
+
if part.upper() == "TYPE_MEMBER":
|
| 786 |
+
eff.params["type"] = "member"
|
| 787 |
+
|
| 788 |
+
# Source Zone: Bits 12-15
|
| 789 |
+
src_zone = eff.params.get("source", "DECK")
|
| 790 |
+
src_val = 8 # Default DECK
|
| 791 |
+
if src_zone == "HAND":
|
| 792 |
+
src_val = 6
|
| 793 |
+
elif src_zone == "DISCARD":
|
| 794 |
+
src_val = 7
|
| 795 |
+
attr |= src_val << 12
|
| 796 |
+
|
| 797 |
+
# Filter Mode (Type): Bit 2-3
|
| 798 |
+
ctype = str(eff.params.get("type", "")).lower()
|
| 799 |
+
if ctype == "live":
|
| 800 |
+
attr |= 0x02 << 2 # Value 2 in bits 2-3
|
| 801 |
+
elif ctype == "member":
|
| 802 |
+
attr |= 0x01 << 2 # Value 1 in bits 2-3
|
| 803 |
+
|
| 804 |
+
# Group Filter: Bit 4 (Enable) and Bits 5-11 (ID)
|
| 805 |
+
group_val = eff.params.get("group")
|
| 806 |
+
if group_val is not None:
|
| 807 |
+
try:
|
| 808 |
+
if str(group_val).isdigit():
|
| 809 |
+
g_id = int(str(group_val))
|
| 810 |
+
else:
|
| 811 |
+
from engine.models.enums import Group
|
| 812 |
+
|
| 813 |
+
g_id = int(Group.from_japanese_name(str(group_val)))
|
| 814 |
+
attr |= 0x10 # Bit 4: Has Group Filter
|
| 815 |
+
attr |= g_id << 5 # Bits 5-11: Group ID
|
| 816 |
+
except:
|
| 817 |
+
pass
|
| 818 |
+
|
| 819 |
+
# Special handling for TAP filters
|
| 820 |
+
if eff.effect_type in (EffectType.TAP_OPPONENT, EffectType.TAP_MEMBER):
|
| 821 |
+
# Use Value for cost_max filter (99 = dynamic/none)
|
| 822 |
+
# Use Attr bits 0-6 for blades_max filter (99 = none)
|
| 823 |
+
try:
|
| 824 |
+
val = int(eff.params.get("cost_max", 99))
|
| 825 |
+
except (ValueError, TypeError):
|
| 826 |
+
val = 99
|
| 827 |
+
try:
|
| 828 |
+
blades_max = int(eff.params.get("blades_max", 99))
|
| 829 |
+
except (ValueError, TypeError):
|
| 830 |
+
blades_max = 99
|
| 831 |
+
attr = (attr & 0x80) | (blades_max & 0x7F)
|
| 832 |
+
|
| 833 |
+
# Special handling for MOVE_TO_DISCARD params
|
| 834 |
+
if eff.effect_type == EffectType.MOVE_TO_DISCARD:
|
| 835 |
+
if eff.params.get("from") == "deck_top":
|
| 836 |
+
attr = 1 # Source: Deck Top
|
| 837 |
+
elif eff.params.get("from") == "hand":
|
| 838 |
+
attr = 2 # Source: Hand
|
| 839 |
+
elif eff.params.get("from") == "energy":
|
| 840 |
+
attr = 3 # Source: Energy
|
| 841 |
+
|
| 842 |
+
if eff.value_cond != ConditionType.NONE:
|
| 843 |
+
val = int(eff.value_cond)
|
| 844 |
+
attr |= 0x40 # Bit 6 for Dynamic
|
| 845 |
+
|
| 846 |
+
# Special encoding for REVEAL_UNTIL
|
| 847 |
+
if eff.effect_type == EffectType.REVEAL_UNTIL:
|
| 848 |
+
if eff.value_cond == ConditionType.TYPE_CHECK:
|
| 849 |
+
if eff.params.get("card_type") == "live":
|
| 850 |
+
attr |= 0x01
|
| 851 |
+
elif eff.value_cond == ConditionType.COST_CHECK:
|
| 852 |
+
cost = int(eff.params.get("min", 0))
|
| 853 |
+
attr |= (cost & 0x1F) << 1
|
| 854 |
+
|
| 855 |
+
# Default to Choice (slot 4) for PLAY opcodes if target is generic (SELF/PLAYER)
|
| 856 |
+
if eff.effect_type in (
|
| 857 |
+
EffectType.PLAY_MEMBER_FROM_HAND,
|
| 858 |
+
EffectType.PLAY_MEMBER_FROM_DISCARD,
|
| 859 |
+
EffectType.PLAY_LIVE_FROM_DISCARD,
|
| 860 |
+
):
|
| 861 |
+
if eff.target in (TargetType.SELF, TargetType.PLAYER):
|
| 862 |
+
slot = 4
|
| 863 |
+
|
| 864 |
+
bytecode.extend(
|
| 865 |
+
[
|
| 866 |
+
int(op),
|
| 867 |
+
int(val),
|
| 868 |
+
attr if not eff.params.get("all") else (attr | 0x80), # Bit 7 for ALL
|
| 869 |
+
slot,
|
| 870 |
+
]
|
| 871 |
+
)
|
| 872 |
+
|
| 873 |
+
def reconstruct_text(self, lang: str = "en") -> str:
|
| 874 |
+
"""Generate standardized text description."""
|
| 875 |
+
parts = []
|
| 876 |
+
is_jp = lang == "jp"
|
| 877 |
+
t_desc_map = TRIGGER_DESCRIPTIONS_JP if is_jp else TRIGGER_DESCRIPTIONS
|
| 878 |
+
e_desc_map = EFFECT_DESCRIPTIONS_JP if is_jp else EFFECT_DESCRIPTIONS
|
| 879 |
+
|
| 880 |
+
t_name = getattr(self.trigger, "name", str(self.trigger))
|
| 881 |
+
trigger_desc = t_desc_map.get(self.trigger, f"[{t_name}]")
|
| 882 |
+
if self.trigger == TriggerType.ON_LEAVES:
|
| 883 |
+
if "discard" not in trigger_desc.lower() and "控え室" not in trigger_desc:
|
| 884 |
+
suffix = " (to discard)" if not is_jp else "(控え室へ)"
|
| 885 |
+
trigger_desc += suffix
|
| 886 |
+
parts.append(trigger_desc)
|
| 887 |
+
|
| 888 |
+
for cost in self.costs:
|
| 889 |
+
if is_jp:
|
| 890 |
+
if cost.type == AbilityCostType.ENERGY:
|
| 891 |
+
parts.append(f"(コスト: エネ{cost.value}消費)")
|
| 892 |
+
elif cost.type == AbilityCostType.TAP_SELF:
|
| 893 |
+
parts.append("(コスト: 自身ウェイト)")
|
| 894 |
+
elif cost.type == AbilityCostType.DISCARD_HAND:
|
| 895 |
+
parts.append(f"(コスト: 手札{cost.value}枚捨て)")
|
| 896 |
+
elif cost.type == AbilityCostType.SACRIFICE_SELF:
|
| 897 |
+
parts.append("(コスト: 自身退場)")
|
| 898 |
+
else:
|
| 899 |
+
parts.append(f"(コスト: {cost.type.name} {cost.value})")
|
| 900 |
+
else:
|
| 901 |
+
if cost.type == AbilityCostType.ENERGY:
|
| 902 |
+
parts.append(f"(Cost: Pay {cost.value} Energy)")
|
| 903 |
+
elif cost.type == AbilityCostType.TAP_SELF:
|
| 904 |
+
parts.append("(Cost: Rest Self)")
|
| 905 |
+
elif cost.type == AbilityCostType.DISCARD_HAND:
|
| 906 |
+
parts.append(f"(Cost: Discard {cost.value} from hand)")
|
| 907 |
+
elif cost.type == AbilityCostType.SACRIFICE_SELF:
|
| 908 |
+
parts.append("(Cost: Sacrifice Self)")
|
| 909 |
+
else:
|
| 910 |
+
parts.append(f"(Cost: {cost.type.name} {cost.value})")
|
| 911 |
+
|
| 912 |
+
for cond in self.conditions:
|
| 913 |
+
if is_jp:
|
| 914 |
+
neg = "NOT " if cond.is_negated else "" # JP negation usually handles via suffix, but keeping simple
|
| 915 |
+
cond_desc = f"{neg}{cond.type.name}"
|
| 916 |
+
if cond.type == ConditionType.BATON:
|
| 917 |
+
cond_desc = "条件: バトンタッチ"
|
| 918 |
+
if "unit" in cond.params:
|
| 919 |
+
cond_desc += f" ({cond.params['unit']})"
|
| 920 |
+
# ... (add more JP specific cond descs if needed, but for now fallback)
|
| 921 |
+
else:
|
| 922 |
+
neg = "NOT " if cond.is_negated else ""
|
| 923 |
+
cond_desc = f"{neg}{cond.type.name}"
|
| 924 |
+
# Add basic params
|
| 925 |
+
if cond.params.get("type") == "score":
|
| 926 |
+
cond_desc += " (Score)"
|
| 927 |
+
if cond.type == ConditionType.SCORE_COMPARE:
|
| 928 |
+
target_str = " (Opponent)" if cond.params.get("target") == "opponent" else ""
|
| 929 |
+
cond_desc += f" (Score check{target_str})"
|
| 930 |
+
if cond.type == ConditionType.OPPONENT_HAS:
|
| 931 |
+
cond_desc += " (Opponent has)"
|
| 932 |
+
if cond.type == ConditionType.OPPONENT_CHOICE:
|
| 933 |
+
cond_desc += " (Opponent chooses)"
|
| 934 |
+
if cond.type == ConditionType.OPPONENT_HAND_DIFF:
|
| 935 |
+
cond_desc += " (Opponent hand check)"
|
| 936 |
+
if cond.params.get("group"):
|
| 937 |
+
cond_desc += f"({cond.params['group']})"
|
| 938 |
+
if cond.params.get("zone"):
|
| 939 |
+
cond_desc += f" (in {cond.params['zone']})"
|
| 940 |
+
if cond.params.get("zone") == "SUCCESS_LIVE":
|
| 941 |
+
cond_desc += " (in Live Area)"
|
| 942 |
+
if cond.type == ConditionType.HAS_CHOICE:
|
| 943 |
+
cond_desc = "Condition: Choose One"
|
| 944 |
+
if cond.type == ConditionType.HAS_KEYWORD:
|
| 945 |
+
cond_desc += f" (Has {cond.params.get('keyword', '?')})"
|
| 946 |
+
if cond.params.get("context") == "heart_inclusion":
|
| 947 |
+
cond_desc += " (Heart check)"
|
| 948 |
+
if cond.type == ConditionType.COUNT_BLADES:
|
| 949 |
+
cond_desc += " (Blade count)"
|
| 950 |
+
if cond.type == ConditionType.COUNT_HEARTS:
|
| 951 |
+
cond_desc += " (Heart count)"
|
| 952 |
+
if cond.params.get("context") == "excess":
|
| 953 |
+
cond_desc += " (Excess)"
|
| 954 |
+
if cond.type == ConditionType.COUNT_ENERGY:
|
| 955 |
+
cond_desc += " (Energy count)"
|
| 956 |
+
if cond.type == ConditionType.COUNT_SUCCESS_LIVE:
|
| 957 |
+
cond_desc += " (Success Live count)"
|
| 958 |
+
if cond.type == ConditionType.HAS_LIVE_CARD:
|
| 959 |
+
cond_desc += " (Live card check)"
|
| 960 |
+
type_str = (
|
| 961 |
+
"Heart comparison"
|
| 962 |
+
if cond.params.get("type") == "heart"
|
| 963 |
+
else "Cheer comparison"
|
| 964 |
+
if cond.params.get("type") == "cheer_count"
|
| 965 |
+
else "Score check"
|
| 966 |
+
)
|
| 967 |
+
cond_desc += f" ({type_str}{target_str})"
|
| 968 |
+
if cond.type == ConditionType.BATON:
|
| 969 |
+
cond_desc = "Condition: Baton Pass"
|
| 970 |
+
if "unit" in cond.params:
|
| 971 |
+
cond_desc += f" ({cond.params['unit']})"
|
| 972 |
+
parts.append(cond_desc)
|
| 973 |
+
|
| 974 |
+
for eff in self.effects:
|
| 975 |
+
# Special handling for META_RULE which relies heavily on params
|
| 976 |
+
desc = None
|
| 977 |
+
if eff.effect_type == EffectType.META_RULE:
|
| 978 |
+
if eff.params.get("type") == "opponent_trigger_allowed":
|
| 979 |
+
desc = "[Meta: Opponent effects trigger this]"
|
| 980 |
+
elif eff.params.get("type") == "shuffle":
|
| 981 |
+
desc = "Shuffle Deck"
|
| 982 |
+
elif eff.params.get("type") == "heart_rule":
|
| 983 |
+
src = eff.params.get("source", "")
|
| 984 |
+
src_text = "ALL Blades" if src == "all_blade" else "Blade" if src == "blade" else ""
|
| 985 |
+
desc = f"[Meta: Treat {src_text} as Heart]" if src_text else "[Meta: Treat as Heart]"
|
| 986 |
+
elif eff.params.get("type") == "live":
|
| 987 |
+
desc = "[Meta: Live Rule]"
|
| 988 |
+
elif eff.params.get("type") == "lose_blade_heart":
|
| 989 |
+
desc = "[Meta: Lose Blade Heart]"
|
| 990 |
+
elif eff.params.get("type") == "re_cheer":
|
| 991 |
+
desc = "[Meta: Cheer Again]"
|
| 992 |
+
elif eff.params.get("type") == "cheer_mod":
|
| 993 |
+
val = eff.value
|
| 994 |
+
desc = f"[Meta: Cheer Reveal Count {'+' if val > 0 else ''}{val}]"
|
| 995 |
+
elif eff.effect_type == getattr(EffectType, "TAP_OPPONENT", -1):
|
| 996 |
+
desc = "Tap Opponent Member(s)"
|
| 997 |
+
|
| 998 |
+
if desc is None:
|
| 999 |
+
# Custom overrides for standard effects with params
|
| 1000 |
+
if eff.effect_type == EffectType.DRAW and eff.params.get("multiplier") == "energy":
|
| 1001 |
+
req = eff.params.get("req_per_unit", 1)
|
| 1002 |
+
desc = f"Draw {eff.value} card(s) per {req} Energy"
|
| 1003 |
+
elif eff.effect_type == EffectType.REDUCE_HEART_REQ and eff.value < 0:
|
| 1004 |
+
# e.g. value -1 means reduce requirement. value +1 means increase requirement (opp).
|
| 1005 |
+
pass
|
| 1006 |
+
|
| 1007 |
+
if desc is None:
|
| 1008 |
+
template = e_desc_map.get(eff.effect_type, getattr(eff.effect_type, "name", str(eff.effect_type)))
|
| 1009 |
+
context = eff.params.copy()
|
| 1010 |
+
context["value"] = eff.value
|
| 1011 |
+
|
| 1012 |
+
# Refine REDUCE_HEART_REQ
|
| 1013 |
+
if eff.effect_type == EffectType.REDUCE_HEART_REQ:
|
| 1014 |
+
if eff.params.get("mode") == "select_requirement":
|
| 1015 |
+
desc = "Choose Heart Requirement (hearts) (choice)" if not is_jp else "ハート条件選択"
|
| 1016 |
+
elif eff.value < 0:
|
| 1017 |
+
desc = (
|
| 1018 |
+
f"Reduce Heart Requirement by {abs(eff.value)} (Live)"
|
| 1019 |
+
if not is_jp
|
| 1020 |
+
else f"ハート条件-{abs(eff.value)}"
|
| 1021 |
+
)
|
| 1022 |
+
else:
|
| 1023 |
+
desc = (
|
| 1024 |
+
f"Increase Heart Requirement by {eff.value} (Live)"
|
| 1025 |
+
if not is_jp
|
| 1026 |
+
else f"ハート条件+{eff.value}"
|
| 1027 |
+
)
|
| 1028 |
+
elif eff.effect_type == EffectType.TRANSFORM_COLOR:
|
| 1029 |
+
target_s = eff.params.get("target", "Color")
|
| 1030 |
+
if target_s == "heart":
|
| 1031 |
+
target_s = "Heart"
|
| 1032 |
+
desc = f"Transform {target_s} Color" if not is_jp else f"{target_s}の色を変換"
|
| 1033 |
+
elif eff.effect_type == EffectType.PLACE_UNDER:
|
| 1034 |
+
type_s = f" {eff.params.get('type', '')}" if "type" in eff.params else ""
|
| 1035 |
+
desc = f"Place{type_s} card under member" if not is_jp else f"メンバーの下に{type_s}置く"
|
| 1036 |
+
if eff.params.get("type") == "energy":
|
| 1037 |
+
desc = "Place Energy under member" if not is_jp else "メンバーの下にエネルギーを置く"
|
| 1038 |
+
else:
|
| 1039 |
+
try:
|
| 1040 |
+
desc = template.format(**context)
|
| 1041 |
+
except KeyError:
|
| 1042 |
+
desc = template
|
| 1043 |
+
|
| 1044 |
+
# Clean up descriptions
|
| 1045 |
+
if eff.params.get("live") and "live" not in desc.lower() and "meta" not in desc.lower():
|
| 1046 |
+
desc = f"{desc} (Live Rule)"
|
| 1047 |
+
|
| 1048 |
+
# Contextual refinements without spamming "Interaction" tags
|
| 1049 |
+
if eff.params.get("per_energy"):
|
| 1050 |
+
desc += " per Energy"
|
| 1051 |
+
if eff.params.get("per_member"):
|
| 1052 |
+
desc += " per Member"
|
| 1053 |
+
if eff.params.get("per_live"):
|
| 1054 |
+
desc += " per Live"
|
| 1055 |
+
|
| 1056 |
+
# Target Context
|
| 1057 |
+
if eff.target == TargetType.MEMBER_SELECT:
|
| 1058 |
+
desc += " (Choose member)"
|
| 1059 |
+
if eff.target == TargetType.OPPONENT or eff.target == TargetType.OPPONENT_HAND:
|
| 1060 |
+
if "opponent" not in desc.lower():
|
| 1061 |
+
desc += " (Opponent)"
|
| 1062 |
+
|
| 1063 |
+
# Trigger Remote Context
|
| 1064 |
+
if eff.effect_type == EffectType.TRIGGER_REMOTE:
|
| 1065 |
+
zone = eff.params.get("from", "unknown")
|
| 1066 |
+
desc += f" from {zone}"
|
| 1067 |
+
|
| 1068 |
+
# Reveal Context
|
| 1069 |
+
if eff.effect_type == EffectType.REVEAL_CARDS:
|
| 1070 |
+
if "from" in eff.params and eff.params["from"] == "deck":
|
| 1071 |
+
desc += " from Deck"
|
| 1072 |
+
if eff.effect_type == EffectType.MOVE_TO_DECK:
|
| 1073 |
+
if eff.params.get("to_energy_deck"):
|
| 1074 |
+
desc = "Return to Energy Deck"
|
| 1075 |
+
elif eff.params.get("from") == "discard":
|
| 1076 |
+
desc += " from Discard"
|
| 1077 |
+
|
| 1078 |
+
if eff.params.get("rest") == "discard" or eff.params.get("on_fail") == "discard":
|
| 1079 |
+
if "discard" not in desc.lower():
|
| 1080 |
+
desc += " (else Discard)"
|
| 1081 |
+
|
| 1082 |
+
if eff.params.get("both_players"):
|
| 1083 |
+
desc += " (Both Players)" if not is_jp else " (両プレイヤー)"
|
| 1084 |
+
|
| 1085 |
+
if eff.params.get("filter") == "live" and "live" not in desc.lower() and "ライブ" not in desc:
|
| 1086 |
+
desc += " (Live Card)" if not is_jp else " (ライブカード)"
|
| 1087 |
+
if eff.params.get("filter") == "energy" and "energy" not in desc.lower() and "エネ" not in desc:
|
| 1088 |
+
desc += " (Energy)" if not is_jp else " (エネルギー)"
|
| 1089 |
+
|
| 1090 |
+
parts.append(f"→ {desc}")
|
| 1091 |
+
|
| 1092 |
+
# Check for Effect-level modal options (e.g. from parser fix)
|
| 1093 |
+
if eff.modal_options:
|
| 1094 |
+
for i, option in enumerate(eff.modal_options):
|
| 1095 |
+
opt_descs = []
|
| 1096 |
+
for sub_eff in option:
|
| 1097 |
+
template = e_desc_map.get(sub_eff.effect_type, sub_eff.effect_type.name)
|
| 1098 |
+
context = sub_eff.params.copy()
|
| 1099 |
+
context["value"] = sub_eff.value
|
| 1100 |
+
try:
|
| 1101 |
+
opt_descs.append(template.format(**context))
|
| 1102 |
+
except KeyError:
|
| 1103 |
+
opt_descs.append(template)
|
| 1104 |
+
parts.append(
|
| 1105 |
+
f"[Option {i + 1}: {' + '.join(opt_descs)}]"
|
| 1106 |
+
if not is_jp
|
| 1107 |
+
else f"[選択肢 {i + 1}: {' + '.join(opt_descs)}]"
|
| 1108 |
+
)
|
| 1109 |
+
|
| 1110 |
+
# Include modal options (Ability level - legacy/bullet points)
|
| 1111 |
+
if self.modal_options:
|
| 1112 |
+
for i, option in enumerate(self.modal_options):
|
| 1113 |
+
opt_descs = []
|
| 1114 |
+
for eff in option:
|
| 1115 |
+
template = e_desc_map.get(eff.effect_type, eff.effect_type.name)
|
| 1116 |
+
context = eff.params.copy()
|
| 1117 |
+
context["value"] = eff.value
|
| 1118 |
+
try:
|
| 1119 |
+
opt_descs.append(template.format(**context))
|
| 1120 |
+
except KeyError:
|
| 1121 |
+
opt_descs.append(template)
|
| 1122 |
+
parts.append(
|
| 1123 |
+
f"[Option {i + 1}: {' + '.join(opt_descs)}]"
|
| 1124 |
+
if not is_jp
|
| 1125 |
+
else f"[選択肢 {i + 1}: {' + '.join(opt_descs)}]"
|
| 1126 |
+
)
|
| 1127 |
+
|
| 1128 |
+
return " ".join(parts)
|
engine/models/card.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import field
|
| 2 |
+
from typing import Annotated, Any, Dict, List
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
import pydantic
|
| 6 |
+
from pydantic import BeforeValidator, ConfigDict, field_serializer
|
| 7 |
+
|
| 8 |
+
from engine.models.ability import Ability
|
| 9 |
+
from engine.models.enums import Group, Unit, ensure_group_list, ensure_unit_list
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def ensure_ndarray(v: Any) -> Any:
|
| 13 |
+
"""Validator to convert list/dict to numpy array"""
|
| 14 |
+
if isinstance(v, list):
|
| 15 |
+
return np.array(v, dtype=np.int32)
|
| 16 |
+
return v
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@pydantic.dataclasses.dataclass(config=ConfigDict(arbitrary_types_allowed=True))
|
| 20 |
+
class MemberCard:
|
| 21 |
+
"""Represents a member card with all attributes"""
|
| 22 |
+
|
| 23 |
+
card_id: int
|
| 24 |
+
card_no: str
|
| 25 |
+
name: str
|
| 26 |
+
cost: int
|
| 27 |
+
hearts: Annotated[
|
| 28 |
+
np.ndarray, BeforeValidator(ensure_ndarray)
|
| 29 |
+
] # Shape (7,) for each color count (0-5: colors, 6: any/unassigned)
|
| 30 |
+
blade_hearts: Annotated[
|
| 31 |
+
np.ndarray, BeforeValidator(ensure_ndarray)
|
| 32 |
+
] # Shape (7,) blade hearts by color (Index 6 = ALL)
|
| 33 |
+
blades: int
|
| 34 |
+
groups: Annotated[List[Group], BeforeValidator(ensure_group_list)] = field(default_factory=list)
|
| 35 |
+
units: Annotated[List[Unit], BeforeValidator(ensure_unit_list)] = field(default_factory=list)
|
| 36 |
+
abilities: List[Ability] = field(default_factory=list)
|
| 37 |
+
img_path: str = ""
|
| 38 |
+
rare: str = "N" # Default to Normal (updated from rarity to match tests)
|
| 39 |
+
# Rule 2.12: カードテキスト (Card Text)
|
| 40 |
+
ability_text: str = ""
|
| 41 |
+
original_text: str = ""
|
| 42 |
+
# Rule 2.7: ブレードハート (Blade Heart Icons)
|
| 43 |
+
volume_icons: int = 0
|
| 44 |
+
draw_icons: int = 0
|
| 45 |
+
faq: List[Dict[str, Any]] = field(default_factory=list)
|
| 46 |
+
|
| 47 |
+
@field_serializer("hearts", "blade_hearts")
|
| 48 |
+
def serialize_array(self, v: np.ndarray, _info):
|
| 49 |
+
return v.tolist()
|
| 50 |
+
|
| 51 |
+
def total_hearts(self) -> int:
|
| 52 |
+
return int(np.sum(self.hearts))
|
| 53 |
+
|
| 54 |
+
def total_blade_hearts(self) -> int:
|
| 55 |
+
return int(np.sum(self.blade_hearts))
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@pydantic.dataclasses.dataclass(config=ConfigDict(arbitrary_types_allowed=True))
|
| 59 |
+
class LiveCard:
|
| 60 |
+
"""Represents a live/song card"""
|
| 61 |
+
|
| 62 |
+
card_id: int
|
| 63 |
+
card_no: str
|
| 64 |
+
name: str
|
| 65 |
+
score: int
|
| 66 |
+
required_hearts: Annotated[
|
| 67 |
+
np.ndarray, BeforeValidator(ensure_ndarray)
|
| 68 |
+
] # Shape (7,) required hearts by color (6 colors + any)
|
| 69 |
+
abilities: List[Ability] = field(default_factory=list)
|
| 70 |
+
groups: Annotated[List[Group], BeforeValidator(ensure_group_list)] = field(default_factory=list)
|
| 71 |
+
units: Annotated[List[Unit], BeforeValidator(ensure_unit_list)] = field(default_factory=list)
|
| 72 |
+
img_path: str = ""
|
| 73 |
+
rare: str = "N"
|
| 74 |
+
ability_text: str = ""
|
| 75 |
+
original_text: str = ""
|
| 76 |
+
volume_icons: int = 0
|
| 77 |
+
draw_icons: int = 0
|
| 78 |
+
blade_hearts: Annotated[np.ndarray, BeforeValidator(ensure_ndarray)] = field(
|
| 79 |
+
default_factory=lambda: np.zeros(7, dtype=np.int32)
|
| 80 |
+
)
|
| 81 |
+
faq: List[Dict[str, Any]] = field(default_factory=list)
|
| 82 |
+
|
| 83 |
+
@field_serializer("required_hearts", "blade_hearts")
|
| 84 |
+
def serialize_array(self, v: np.ndarray, _info):
|
| 85 |
+
return v.tolist()
|
| 86 |
+
|
| 87 |
+
def total_required(self) -> int:
|
| 88 |
+
return int(np.sum(self.required_hearts[:6])) # Exclude star/any
|
| 89 |
+
|
| 90 |
+
def total_blade_hearts(self) -> int:
|
| 91 |
+
return int(np.sum(self.blade_hearts))
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
@pydantic.dataclasses.dataclass(config=ConfigDict(arbitrary_types_allowed=True))
|
| 95 |
+
class EnergyCard:
|
| 96 |
+
"""Represents an energy card with metadata"""
|
| 97 |
+
|
| 98 |
+
card_id: int
|
| 99 |
+
card_no: str = ""
|
| 100 |
+
name: str = "Energy"
|
| 101 |
+
img_path: str = ""
|
| 102 |
+
ability_text: str = ""
|
| 103 |
+
original_text: str = ""
|
| 104 |
+
rare: str = "N"
|
engine/models/choice_types.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from enum import IntEnum
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class ChoiceType(IntEnum):
|
| 5 |
+
NONE = 0
|
| 6 |
+
TARGET_HAND = 1
|
| 7 |
+
TARGET_MEMBER = 2
|
| 8 |
+
TARGET_MEMBER_SLOT = 3
|
| 9 |
+
DISCARD_SELECT = 4
|
| 10 |
+
MODAL = 5
|
| 11 |
+
SELECT_MODE = 6
|
| 12 |
+
CHOOSE_FORMATION = 7
|
| 13 |
+
COLOR_SELECT = 8
|
| 14 |
+
SELECT_FROM_LIST = 9
|
| 15 |
+
TARGET_OPPONENT_MEMBER = 10
|
| 16 |
+
SELECT_SWAP_SOURCE = 11
|
| 17 |
+
TARGET_SUCCESS_LIVES = 12
|
| 18 |
+
TARGET_DISCARD = 13
|
| 19 |
+
TARGET_REMOVED = 14
|
| 20 |
+
TARGET_DECK = 15
|
| 21 |
+
TARGET_LIVE = 16
|
engine/models/context_indices.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from enum import IntEnum
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class ContextIndex(IntEnum):
|
| 5 |
+
"""
|
| 6 |
+
Indices for the fixed-size NumPy context array (int32).
|
| 7 |
+
Size should be enough to hold all standard context variables.
|
| 8 |
+
Standard Size: 64 integers.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
# Header / Meta
|
| 12 |
+
TYPE = 0 # Context Type (Trigger, Effect, etc)
|
| 13 |
+
PLAYER_ID = 1 # Owner/Active Player
|
| 14 |
+
OPPONENT_ID = 2
|
| 15 |
+
PHASE = 3
|
| 16 |
+
TURN = 4
|
| 17 |
+
|
| 18 |
+
# Source Info
|
| 19 |
+
SOURCE_CARD_ID = 5
|
| 20 |
+
SOURCE_ZONE = 6 # Zone Enum
|
| 21 |
+
SOURCE_ZONE_IDX = 7
|
| 22 |
+
SOURCE_TYPE = 8 # Member/Live
|
| 23 |
+
|
| 24 |
+
# Target Info
|
| 25 |
+
TARGET_CARD_ID = 10
|
| 26 |
+
TARGET_ZONE = 11
|
| 27 |
+
TARGET_ZONE_IDX = 12
|
| 28 |
+
TARGET_PLAYER_ID = 13
|
| 29 |
+
TARGET_COUNT = 14 # How many targets
|
| 30 |
+
|
| 31 |
+
# Payload / Parameters
|
| 32 |
+
VALUE = 20 # Generic Value (Amount, Score, etc)
|
| 33 |
+
COST_PAID = 21 # Boolean/Chek
|
| 34 |
+
ATTRIBUTE = 22 # Color/Attribute
|
| 35 |
+
GROUP = 23 # Group Enum
|
| 36 |
+
SUB_TYPE = 24 # Trigger Subtype / Effect Subtype
|
| 37 |
+
|
| 38 |
+
# Mapped Params (Generic registers)
|
| 39 |
+
PARAM_1 = 30
|
| 40 |
+
PARAM_2 = 31
|
| 41 |
+
PARAM_3 = 32
|
| 42 |
+
PARAM_4 = 33
|
| 43 |
+
|
| 44 |
+
# Flags (Bitmask)
|
| 45 |
+
FLAGS = 50 # Optional, Negated, etc.
|
| 46 |
+
|
| 47 |
+
# Execution State
|
| 48 |
+
STEP_INDEX = 60
|
| 49 |
+
TOTAL_STEPS = 61
|
| 50 |
+
|
| 51 |
+
SIZE = 64
|
engine/models/enums.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from enum import IntEnum
|
| 2 |
+
from typing import Any, List
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class CardType(IntEnum):
|
| 6 |
+
"""Card types in the game"""
|
| 7 |
+
|
| 8 |
+
MEMBER = 0
|
| 9 |
+
LIVE = 1
|
| 10 |
+
ENERGY = 2
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class HeartColor(IntEnum):
|
| 14 |
+
"""Heart/color types (6 colors + any + rainbow)"""
|
| 15 |
+
|
| 16 |
+
PINK = 0
|
| 17 |
+
RED = 1
|
| 18 |
+
YELLOW = 2
|
| 19 |
+
GREEN = 3
|
| 20 |
+
BLUE = 4
|
| 21 |
+
PURPLE = 5
|
| 22 |
+
ANY = 6 # Colorless requirement
|
| 23 |
+
RAINBOW = 7 # Can be any color
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class Area(IntEnum):
|
| 27 |
+
"""Member areas on stage"""
|
| 28 |
+
|
| 29 |
+
LEFT = 0
|
| 30 |
+
CENTER = 1
|
| 31 |
+
RIGHT = 2
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class Group(IntEnum):
|
| 35 |
+
"""Card Groups (Series/Schools)"""
|
| 36 |
+
|
| 37 |
+
MUSE = 0
|
| 38 |
+
AQOURS = 1
|
| 39 |
+
NIJIGASAKI = 2
|
| 40 |
+
LIELLA = 3
|
| 41 |
+
HASUNOSORA = 4
|
| 42 |
+
LIVE = 98
|
| 43 |
+
OTHER = 99
|
| 44 |
+
|
| 45 |
+
@classmethod
|
| 46 |
+
def from_japanese_name(cls, name: str) -> "Group":
|
| 47 |
+
name = name.strip()
|
| 48 |
+
name_lower = name.lower()
|
| 49 |
+
if "ラブライブ!" == name or "μ's" in name or "muse" in name_lower:
|
| 50 |
+
return cls.MUSE
|
| 51 |
+
if "サンシャイン" in name or "aqours" in name_lower:
|
| 52 |
+
return cls.AQOURS
|
| 53 |
+
if "虹ヶ咲" in name or "nijigasaki" in name_lower:
|
| 54 |
+
return cls.NIJIGASAKI
|
| 55 |
+
if "スーパースター" in name or "liella" in name_lower:
|
| 56 |
+
return cls.LIELLA
|
| 57 |
+
if "蓮ノ空" in name or "hasunosora" in name_lower:
|
| 58 |
+
return cls.HASUNOSORA
|
| 59 |
+
return cls.OTHER
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class Unit(IntEnum):
|
| 63 |
+
"""Card Units"""
|
| 64 |
+
|
| 65 |
+
PRINTEMPS = 0
|
| 66 |
+
LILY_WHITE = 1
|
| 67 |
+
BIBI = 2
|
| 68 |
+
CYARON = 3
|
| 69 |
+
AZALEA = 4
|
| 70 |
+
GUILTY_KISS = 5
|
| 71 |
+
DIVER_DIVA = 6
|
| 72 |
+
A_ZU_NA = 7
|
| 73 |
+
QU4RTZ = 8
|
| 74 |
+
R3BIRTH = 9
|
| 75 |
+
CATCHU = 10
|
| 76 |
+
KALEIDOSCORE = 11
|
| 77 |
+
SYNCRISE = 12
|
| 78 |
+
CERISE_BOUQUET = 13
|
| 79 |
+
DOLLCHESTRA = 14
|
| 80 |
+
MIRA_CRA_PARK = 15
|
| 81 |
+
EDEL_NOTE = 16
|
| 82 |
+
OTHER = 99
|
| 83 |
+
|
| 84 |
+
@classmethod
|
| 85 |
+
def from_japanese_name(cls, name: str) -> "Unit":
|
| 86 |
+
name = name.strip()
|
| 87 |
+
name_lower = name.lower()
|
| 88 |
+
if "printemps" in name_lower:
|
| 89 |
+
return cls.PRINTEMPS
|
| 90 |
+
if "lily white" in name_lower or "lilywhite" in name_lower:
|
| 91 |
+
return cls.LILY_WHITE
|
| 92 |
+
if "bibi" in name_lower:
|
| 93 |
+
return cls.BIBI
|
| 94 |
+
if "cyaron" in name_lower or "cyaron!" in name_lower:
|
| 95 |
+
return cls.CYARON
|
| 96 |
+
if "azalea" in name_lower:
|
| 97 |
+
return cls.AZALEA
|
| 98 |
+
if "guilty kiss" in name_lower or "guiltykiss" in name_lower:
|
| 99 |
+
return cls.GUILTY_KISS
|
| 100 |
+
if "diverdiva" in name_lower:
|
| 101 |
+
return cls.DIVER_DIVA
|
| 102 |
+
if "azuna" in name_lower or "a・zu・na" in name_lower:
|
| 103 |
+
return cls.A_ZU_NA
|
| 104 |
+
if "qu4rtz" in name_lower:
|
| 105 |
+
return cls.QU4RTZ
|
| 106 |
+
if "r3birth" in name_lower:
|
| 107 |
+
return cls.R3BIRTH
|
| 108 |
+
if "catchu" in name_lower:
|
| 109 |
+
return cls.CATCHU
|
| 110 |
+
if "kaleidoscore" in name_lower:
|
| 111 |
+
return cls.KALEIDOSCORE
|
| 112 |
+
if "5yncri5e" in name_lower:
|
| 113 |
+
return cls.SYNCRISE
|
| 114 |
+
if "スリーズブーケ" in name or "cerise" in name_lower:
|
| 115 |
+
return cls.CERISE_BOUQUET
|
| 116 |
+
if "dollchestra" in name_lower:
|
| 117 |
+
return cls.DOLLCHESTRA
|
| 118 |
+
if "みらくらぱーく" in name or "mira-cra" in name_lower or "mirakura" in name_lower:
|
| 119 |
+
return cls.MIRA_CRA_PARK
|
| 120 |
+
if "edelnote" in name_lower:
|
| 121 |
+
return cls.EDEL_NOTE
|
| 122 |
+
if not name:
|
| 123 |
+
return cls.OTHER
|
| 124 |
+
return cls.OTHER
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def ensure_group_list(v: Any) -> List[Group]:
|
| 128 |
+
"""Validator to convert string/single Group to List[Group]"""
|
| 129 |
+
if isinstance(v, list):
|
| 130 |
+
return [
|
| 131 |
+
g if isinstance(g, Group) else Group(g) if isinstance(g, int) else Group.from_japanese_name(str(g))
|
| 132 |
+
for g in v
|
| 133 |
+
]
|
| 134 |
+
if isinstance(v, Group):
|
| 135 |
+
return [v]
|
| 136 |
+
if isinstance(v, int):
|
| 137 |
+
return [Group(v)]
|
| 138 |
+
if isinstance(v, str):
|
| 139 |
+
if not v:
|
| 140 |
+
return []
|
| 141 |
+
parts = [p.strip() for p in v.split("\n") if p.strip()]
|
| 142 |
+
return [Group.from_japanese_name(p) for p in parts]
|
| 143 |
+
return []
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def ensure_unit_list(v: Any) -> List[Unit]:
|
| 147 |
+
"""Validator to convert string/single Unit to List[Unit]"""
|
| 148 |
+
if isinstance(v, list):
|
| 149 |
+
return [
|
| 150 |
+
u if isinstance(u, Unit) else Unit(u) if isinstance(u, int) else Unit.from_japanese_name(str(u)) for u in v
|
| 151 |
+
]
|
| 152 |
+
if isinstance(v, Unit):
|
| 153 |
+
return [v]
|
| 154 |
+
if isinstance(v, int):
|
| 155 |
+
return [Unit(v)]
|
| 156 |
+
if isinstance(v, str):
|
| 157 |
+
if not v:
|
| 158 |
+
return []
|
| 159 |
+
parts = [p.strip() for p in v.split("\n") if p.strip()]
|
| 160 |
+
return [Unit.from_japanese_name(p) for p in parts]
|
| 161 |
+
return []
|
engine/models/opcodes.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from enum import IntEnum
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class Opcode(IntEnum):
|
| 5 |
+
# Core Flow
|
| 6 |
+
NOP = 0
|
| 7 |
+
RETURN = 1
|
| 8 |
+
JUMP = 2
|
| 9 |
+
JUMP_IF_FALSE = 3
|
| 10 |
+
|
| 11 |
+
# State Modification (10-99)
|
| 12 |
+
DRAW = 10
|
| 13 |
+
ADD_BLADES = 11
|
| 14 |
+
ADD_HEARTS = 12
|
| 15 |
+
REDUCE_COST = 13
|
| 16 |
+
LOOK_DECK = 14
|
| 17 |
+
RECOVER_LIVE = 15
|
| 18 |
+
BOOST_SCORE = 16
|
| 19 |
+
RECOVER_MEMBER = 17
|
| 20 |
+
BUFF_POWER = 18
|
| 21 |
+
IMMUNITY = 19
|
| 22 |
+
MOVE_MEMBER = 20
|
| 23 |
+
SWAP_CARDS = 21
|
| 24 |
+
SEARCH_DECK = 22
|
| 25 |
+
ENERGY_CHARGE = 23
|
| 26 |
+
SET_BLADES = 24
|
| 27 |
+
SET_HEARTS = 25
|
| 28 |
+
FORMATION_CHANGE = 26
|
| 29 |
+
NEGATE_EFFECT = 27
|
| 30 |
+
ORDER_DECK = 28 # Reorder cards in deck. Attr: 0=Discard, 1=DeckTop, 2=DeckBottom
|
| 31 |
+
META_RULE = 29
|
| 32 |
+
SELECT_MODE = 30
|
| 33 |
+
MOVE_TO_DECK = 31
|
| 34 |
+
TAP_OPPONENT = 32
|
| 35 |
+
PLACE_UNDER = 33
|
| 36 |
+
FLAVOR_ACTION = 34
|
| 37 |
+
RESTRICTION = 35
|
| 38 |
+
BATON_TOUCH_MOD = 36
|
| 39 |
+
SET_SCORE = 37
|
| 40 |
+
SWAP_ZONE = 38
|
| 41 |
+
TRANSFORM_COLOR = 39
|
| 42 |
+
REVEAL_CARDS = 40
|
| 43 |
+
LOOK_AND_CHOOSE = 41
|
| 44 |
+
CHEER_REVEAL = 42
|
| 45 |
+
ACTIVATE_MEMBER = 43
|
| 46 |
+
ADD_TO_HAND = 44
|
| 47 |
+
COLOR_SELECT = 45
|
| 48 |
+
REPLACE_EFFECT = 46
|
| 49 |
+
TRIGGER_REMOTE = 47
|
| 50 |
+
REDUCE_HEART_REQ = 48
|
| 51 |
+
MODIFY_SCORE_RULE = 49
|
| 52 |
+
ADD_STAGE_ENERGY = 50 # Internal
|
| 53 |
+
SET_TAPPED = 51 # Internal
|
| 54 |
+
ADD_CONTINUOUS = 52 # Internal
|
| 55 |
+
TAP_MEMBER = 53
|
| 56 |
+
PLAY_MEMBER_FROM_HAND = 57
|
| 57 |
+
MOVE_TO_DISCARD = 58
|
| 58 |
+
|
| 59 |
+
# --- Added to fix systemic parsing issues ---
|
| 60 |
+
GRANT_ABILITY = 60
|
| 61 |
+
INCREASE_HEART_COST = 61
|
| 62 |
+
REDUCE_YELL_COUNT = 62
|
| 63 |
+
PLAY_MEMBER_FROM_DISCARD = 63
|
| 64 |
+
PAY_ENERGY = 64
|
| 65 |
+
SELECT_MEMBER = 65
|
| 66 |
+
DRAW_UNTIL = 66
|
| 67 |
+
SELECT_PLAYER = 67
|
| 68 |
+
SELECT_LIVE = 68
|
| 69 |
+
REVEAL_UNTIL = 69
|
| 70 |
+
INCREASE_COST = 70
|
| 71 |
+
PREVENT_PLAY_TO_SLOT = 71
|
| 72 |
+
SWAP_AREA = 72
|
| 73 |
+
TRANSFORM_HEART = 73
|
| 74 |
+
SELECT_CARDS = 74
|
| 75 |
+
OPPONENT_CHOOSE = 75
|
| 76 |
+
PLAY_LIVE_FROM_DISCARD = 76
|
| 77 |
+
REDUCE_LIVE_SET_LIMIT = 77
|
| 78 |
+
PREVENT_ACTIVATE = 82
|
| 79 |
+
ACTIVATE_ENERGY = 81
|
| 80 |
+
|
| 81 |
+
# Target Specification (100-199)
|
| 82 |
+
SET_TARGET_SELF = 100
|
| 83 |
+
SET_TARGET_PLAYER = 101
|
| 84 |
+
SET_TARGET_OPPONENT = 102
|
| 85 |
+
SET_TARGET_ALL_PLAYERS = 103
|
| 86 |
+
SET_TARGET_MEMBER_SELF = 104
|
| 87 |
+
SET_TARGET_MEMBER_OTHER = 105
|
| 88 |
+
SET_TARGET_CARD_HAND = 106
|
| 89 |
+
SET_TARGET_CARD_DISCARD = 107
|
| 90 |
+
SET_TARGET_CARD_DECK_TOP = 108
|
| 91 |
+
SET_TARGET_OPPONENT_HAND = 109
|
| 92 |
+
SET_TARGET_MEMBER_SELECT = 110
|
| 93 |
+
SET_TARGET_MEMBER_NAMED = 111
|
| 94 |
+
|
| 95 |
+
# Condition Checks (200-299)
|
| 96 |
+
CHECK_TURN_1 = 200
|
| 97 |
+
CHECK_HAS_MEMBER = 201
|
| 98 |
+
CHECK_HAS_COLOR = 202
|
| 99 |
+
CHECK_COUNT_STAGE = 203
|
| 100 |
+
CHECK_COUNT_HAND = 204
|
| 101 |
+
CHECK_COUNT_DISCARD = 205
|
| 102 |
+
CHECK_IS_CENTER = 206
|
| 103 |
+
CHECK_LIFE_LEAD = 207
|
| 104 |
+
CHECK_COUNT_GROUP = 208
|
| 105 |
+
CHECK_GROUP_FILTER = 209
|
| 106 |
+
CHECK_OPPONENT_HAS = 210
|
| 107 |
+
CHECK_SELF_IS_GROUP = 211
|
| 108 |
+
CHECK_MODAL_ANSWER = 212
|
| 109 |
+
CHECK_COUNT_ENERGY = 213
|
| 110 |
+
CHECK_HAS_LIVE_CARD = 214
|
| 111 |
+
CHECK_COST_CHECK = 215
|
| 112 |
+
CHECK_RARITY_CHECK = 216
|
| 113 |
+
CHECK_HAND_HAS_NO_LIVE = 217
|
| 114 |
+
CHECK_COUNT_SUCCESS_LIVE = 218
|
| 115 |
+
CHECK_OPPONENT_HAND_DIFF = 219
|
| 116 |
+
CHECK_SCORE_COMPARE = 220
|
| 117 |
+
CHECK_HAS_CHOICE = 221
|
| 118 |
+
CHECK_OPPONENT_CHOICE = 222
|
| 119 |
+
CHECK_COUNT_HEARTS = 223
|
| 120 |
+
CHECK_COUNT_BLADES = 224
|
| 121 |
+
CHECK_OPPONENT_ENERGY_DIFF = 225
|
| 122 |
+
CHECK_HAS_KEYWORD = 226
|
| 123 |
+
CHECK_DECK_REFRESHED = 227
|
| 124 |
+
CHECK_HAS_MOVED = 228
|
| 125 |
+
CHECK_HAND_INCREASED = 229
|
| 126 |
+
CHECK_COUNT_LIVE_ZONE = 230
|
| 127 |
+
CHECK_BATON = 231
|
| 128 |
+
CHECK_TYPE_CHECK = 232
|
| 129 |
+
CHECK_IS_IN_DISCARD = 233
|