LovecaSim / engine /game /game_state.py
trioskosmos's picture
Upload folder using huggingface_hub
bb3fbf9 verified
"""
Love Live Card Game - AlphaZero Compatible Game State
This module implements the game state representation for the Love Live
Official Card Game, designed for fast self-play with AlphaZero-style training.
Key Design Decisions:
- Numpy arrays for vectorized operations
- Immutable state with state copying for MCTS
- Action space encoded as integers for neural network output
- Observation tensors suitable for CNN input
"""
# Love Live! Card Game - Comprehensive Rules v1.04 Implementation
# Rule 1: (General Overview)
# Rule 2: (Card Information)
# Rule 3: (Player Info)
# Rule 4: (Zones)
# Rule 1.3: (Fundamental Principles)
# Rule 1.3.1: Card text overrides rules.
# Rule 1.3.2: Impossible actions are simply not performed.
# Rule 1.3.3: "Cannot" effects take priority over "Can" effects.
# Rule 1.3.4: Active player chooses first when multiple choices occur.
# Rule 1.3.5: Numerical selections must be non-negative integers.
import json
import os
from typing import Any, Dict, List, Optional, Tuple
import numpy as np
from engine.game.data_loader import CardDataLoader
from engine.game.enums import Phase
from engine.game.mixins.action_mixin import ActionMixin
from engine.game.mixins.effect_mixin import EffectMixin
from engine.game.mixins.phase_mixin import PhaseMixin
from engine.models.ability import (
Ability,
EffectType,
ResolvingEffect,
TriggerType,
)
from engine.models.card import LiveCard, MemberCard
from engine.models.enums import Group, Unit
# Import Numba utils
# Import Numba utils
try:
from engine.game.numba_utils import JIT_AVAILABLE, calc_main_phase_masks
except ImportError:
JIT_AVAILABLE = False
def calc_main_phase_masks(*args):
pass
# =============================================================================
# OBJECT POOLING FOR PERFORMANCE
# =============================================================================
class StatePool:
"""
Object pool for PlayerState and GameState to avoid allocation overhead.
Thread-local pools for multiprocessing compatibility.
"""
_player_pool: List["PlayerState"] = []
_game_pool: List["GameState"] = []
_max_pool_size: int = 100
@classmethod
def get_player_state(cls, player_id: int) -> "PlayerState":
"""Get a PlayerState - POOLING DISABLED for safety"""
return PlayerState(player_id)
@classmethod
def get_game_state(cls) -> "GameState":
"""Get a GameState - POOLING DISABLED for safety"""
return GameState()
@classmethod
def return_player_state(cls, ps: "PlayerState") -> None:
"""Return a PlayerState to the pool for reuse."""
if len(cls._player_pool) < cls._max_pool_size:
cls._player_pool.append(ps)
@classmethod
def return_game_state(cls, gs: "GameState") -> None:
"""Return a GameState to the pool for reuse."""
if len(cls._game_pool) < cls._max_pool_size:
cls._game_pool.append(gs)
# Phase enum moved to enums.py
# Enums and Card Classes moved to engine.models
# Imported above
from engine.game.player_state import PlayerState
class GameState(ActionMixin, PhaseMixin, EffectMixin):
"""
Full game state (Rule 1)
Features:
- Rule 4.14: Resolution Zone (yell_cards)
- Rule 1.2: Victory Detection
- MCTS / AlphaZero support
"""
# Class-level caches
member_db: Dict[int, MemberCard] = {}
live_db: Dict[int, LiveCard] = {}
_meta_rule_cards: set = set()
# JIT Arrays
_jit_member_costs: Optional[np.ndarray] = None
_jit_member_blades: Optional[np.ndarray] = None
_jit_member_hearts_sum: Optional[np.ndarray] = None
_jit_member_hearts_vec: Optional[np.ndarray] = None
_jit_live_score: Optional[np.ndarray] = None
_jit_live_hearts_sum: Optional[np.ndarray] = None
_jit_live_hearts_vec: Optional[np.ndarray] = None
@classmethod
def initialize_class_db(cls, member_db: Dict[int, "MemberCard"], live_db: Dict[int, "LiveCard"]) -> None:
"""Initialize and wrap static DBs with MaskedDB for UID resolution."""
from engine.game.state_utils import MaskedDB
cls.member_db = MaskedDB(member_db)
cls.live_db = MaskedDB(live_db)
# Optimization: Cache cards with CONSTANT META_RULE effects
cls._meta_rule_cards = set()
for cid, card in cls.member_db.items():
for ab in card.abilities:
if ab.trigger.name == "CONSTANT":
for eff in ab.effects:
if eff.effect_type == EffectType.META_RULE:
cls._meta_rule_cards.add(cid)
break
for cid, card in cls.live_db.items():
for ab in card.abilities:
if ab.trigger.name == "CONSTANT":
for eff in ab.effects:
if eff.effect_type == EffectType.META_RULE:
cls._meta_rule_cards.add(cid)
break
cls._init_jit_arrays()
@classmethod
def _init_jit_arrays(cls):
"""Initialize static arrays for Numba JIT"""
if not cls.member_db:
return
# Find max ID
max_id = max(max(cls.member_db.keys(), default=0), max(cls.live_db.keys(), default=0))
# Create lookup arrays (default 0 or -1)
# Costs: -1 for non-members
costs = np.full(max_id + 1, -1, dtype=np.int32)
# Blades: 0
blades = np.zeros(max_id + 1, dtype=np.int32)
# Hearts Sum: 0
hearts_sum = np.zeros(max_id + 1, dtype=np.int32)
# Hearts Vector: (N, 7)
hearts_vec = np.zeros((max_id + 1, 7), dtype=np.int32)
# Live Score: 0
live_score = np.zeros(max_id + 1, dtype=np.int32)
# Live Hearts Requirement Sum: 0
live_hearts_sum = np.zeros(max_id + 1, dtype=np.int32)
# Live Hearts Vector: (N, 7)
live_hearts_vec = np.zeros((max_id + 1, 7), dtype=np.int32)
for cid, member in cls.member_db.items():
costs[cid] = member.cost
blades[cid] = member.blades
if hasattr(member, "hearts"):
h = member.hearts
# Robustly handle arrays likely to be shape (6,) or (7,)
if len(h) >= 7:
hearts_vec[cid] = h[:7]
else:
hearts_vec[cid, : len(h)] = h
hearts_sum[cid] = np.sum(member.hearts)
for cid, live in cls.live_db.items():
live_score[cid] = int(live.score)
if hasattr(live, "required_hearts"):
rh = live.required_hearts
if len(rh) >= 7:
live_hearts_vec[cid] = rh[:7]
else:
live_hearts_vec[cid, : len(rh)] = rh
live_hearts_sum[cid] = np.sum(live.required_hearts)
cls._jit_member_costs = costs
cls._jit_member_blades = blades
cls._jit_member_hearts_sum = hearts_sum
cls._jit_member_hearts_vec = hearts_vec
cls._jit_live_score = live_score
cls._jit_live_hearts_sum = live_hearts_sum
cls._jit_live_hearts_vec = live_hearts_vec
@classmethod
def serialize_card(cls, cid: int, is_viewable=True, peek=False):
"""Static helper to serialize a card ID."""
if cid < 0:
return None
card_data = {"id": int(cid), "img": "cards/card_back.png", "type": "unknown", "name": "Unknown"}
if not is_viewable and not peek:
return {"id": int(cid), "img": "cards/card_back.png", "type": "unknown", "hidden": True}
if cid in cls.member_db:
m = cls.member_db[cid]
# Basic ability text formatting
at = getattr(m, "ability_text", "")
if not at and hasattr(m, "abilities"):
at_lines = []
for ab in m.abilities:
at_lines.append(ab.raw_text)
at = "\n".join(at_lines)
card_data = {
"id": int(cid),
"card_no": getattr(m, "card_no", "Unknown"),
"name": getattr(m, "name", "Unknown Member"),
"cost": int(getattr(m, "cost", 0)),
"type": "member",
"hp": int(m.total_hearts()) if hasattr(m, "total_hearts") else 0,
"blade": int(getattr(m, "blades", 0)),
"img": getattr(m, "img_path", "cards/card_back.png"),
"hearts": m.hearts.tolist() if hasattr(m, "hearts") and hasattr(m.hearts, "tolist") else [0] * 7,
"blade_hearts": m.blade_hearts.tolist()
if hasattr(m, "blade_hearts") and hasattr(m.blade_hearts, "tolist")
else [0] * 7,
"text": at,
}
elif cid in cls.live_db:
l = cls.live_db[cid]
card_data = {
"id": int(cid),
"card_no": getattr(l, "card_no", "Unknown"),
"name": l.name,
"type": "live",
"score": int(l.score),
"img": l.img_path,
"required_hearts": l.required_hearts.tolist(),
"text": getattr(l, "ability_text", ""),
}
elif cid == 888: # Easy member
card_data = {
"id": 888,
"name": "Easy Member",
"type": "member",
"cost": 1,
"hp": 1,
"blade": 1,
"img": "cards/PLSD01/PL!-sd1-001-SD.png",
"hearts": [1, 0, 0, 0, 0, 0, 0],
"blade_hearts": [0, 0, 0, 0, 0, 0, 0],
"text": "",
}
elif cid == 999: # Easy live
card_data = {
"id": 999,
"name": "Easy Live",
"type": "live",
"score": 1,
"img": "cards/PLSD01/PL!-pb1-019-SD.png",
"required_hearts": [0, 0, 0, 0, 0, 0, 1],
"text": "",
}
if not is_viewable and peek:
card_data["hidden"] = True
card_data["face_down"] = True
return card_data
__slots__ = (
"verbose",
"players",
"current_player",
"first_player",
"phase",
"turn_number",
"game_over",
"winner",
"performance_results",
"yell_cards",
"pending_effects",
"pending_choices",
"rule_log",
"current_resolving_ability",
"current_resolving_member",
"current_resolving_member_id",
"looked_cards",
"triggered_abilities",
"state_history",
"loop_draw",
"removed_cards",
"action_count_this_turn",
"pending_choices_vec",
"pending_choices_ptr",
"triggered_abilities_vec",
"triggered_abilities_ptr",
"_jit_dummy_array",
"fast_mode",
"suppress_logs",
"enable_loop_detection",
"_trigger_buffers",
)
def __init__(self, verbose=False, suppress_logs=False, enable_loop_detection=True):
self.verbose = verbose
self.suppress_logs = suppress_logs
self.enable_loop_detection = enable_loop_detection
self.players = [PlayerState(0), PlayerState(1)]
self.current_player = 0 # Who is acting now
self.first_player = 0 # Who goes first this turn
self.phase = Phase.ACTIVE
self.turn_number: int = 1
self.game_over: bool = False
self.winner: int = -1 # -1 = ongoing, 0/1 = player won, 2 = draw
# Performance Result Tracking (for UI popup)
self.performance_results: Dict[int, Any] = {}
# For yell phase tracking
self.yell_cards: List[int] = [] # Shared Resolution Zone (Rule 4.14)
self.pending_effects: List[ResolvingEffect] = [] # Stack of effects to resolve
self.pending_activation: Optional[Dict[str, Any]] = None
self.pending_choices: List[Tuple[str, Dict[str, Any]]] = [] # (choice_type, params with metadata)
self.rule_log: List[str] = [] # Real-time rule application log
# Track currently resolving ability for context
self.current_resolving_ability: Optional[Ability] = None
self.current_resolving_member: Optional[str] = None # Member name
self.current_resolving_member_id: int = -1 # Member card ID
# Temporary zone for LOOK_DECK
self.looked_cards: List[int] = []
# Rule 9.7: Automatic Abilities
# List of (player_id, Ability, context) waiting to be played
self.triggered_abilities: List[Tuple[int, Ability, Dict[str, Any]]] = []
# Vectorized triggers/choices for JIT
self.pending_choices_vec = np.zeros((16, 3), dtype=np.int32)
self.pending_choices_ptr = 0
self.triggered_abilities_vec = np.zeros((16, 2), dtype=np.int32)
self.triggered_abilities_ptr = 0
self._jit_dummy_array = np.zeros(100, dtype=np.int32)
self.fast_mode = False
self._trigger_buffers = [[], []] # Pre-allocated buffers for trigger processing
# Static caches (for performance and accessibility)
# Should be set from server or data loader
# Loop Detection (Rule 12.1)
# Using a simple hash of the serialization for history
self.state_history: List[int] = []
self.loop_draw = False
self.removed_cards: List[int] = []
self.action_count_this_turn: int = 0
def log_rule(self, rule_id: str, description: str):
"""Append a rule application entry to the log."""
if self.suppress_logs:
return
# Add Turn and Phase context
phase_name = self.phase.name if hasattr(self.phase, "name") else str(self.phase)
entry = f"[Turn {self.turn_number}] [{phase_name}] [{rule_id}] {description}"
self.rule_log.append(entry)
# Also print to stdout for server console debugging
if self.verbose:
print(f"RULE_LOG: {entry}")
pass
def _reset(self) -> None:
"""Reset state for pool reuse - avoids object allocation."""
self.verbose = False
# Players get reset by PlayerState._reset or replaced
self.current_player = 0
self.first_player = 0
self.phase = Phase.ACTIVE
self.turn_number = 1
self.game_over = False
self.winner = -1
self.performance_results.clear()
self.yell_cards.clear()
self.pending_effects.clear()
self.pending_choices.clear()
self.rule_log.clear()
self.current_resolving_ability = None
self.current_resolving_member = None
self.current_resolving_member_id = -1
self.looked_cards.clear()
self.triggered_abilities.clear()
self.pending_choices_vec.fill(0)
self.pending_choices_ptr = 0
self.triggered_abilities_vec.fill(0)
self.triggered_abilities_ptr = 0
self._trigger_buffers[0].clear()
self._trigger_buffers[1].clear()
self.state_history.clear()
self.loop_draw = False
def copy(self) -> "GameState":
"""Copy current game state"""
new = GameState()
self.copy_to(new)
return new
def copy_to(self, new: "GameState") -> None:
"""In-place copy to an existing object to avoid allocation"""
new.verbose = self.verbose
new.suppress_logs = self.suppress_logs
new.enable_loop_detection = self.enable_loop_detection
# Reuse existing PlayerState objects in the pooled GameState
for i, p in enumerate(self.players):
p.copy_to(new.players[i])
new.current_player = self.current_player
new.first_player = self.first_player
new.phase = self.phase
new.turn_number = self.turn_number
new.game_over = self.game_over
new.winner = self.winner
new.yell_cards = list(self.yell_cards)
# Shallow copy of Effect objects (assumed immutable/shared)
new.pending_effects = list(self.pending_effects)
# Manual copy of pending_choices: List[Tuple[str, Dict]]
new.pending_choices = [(pc[0], pc[1].copy()) for pc in self.pending_choices]
new.rule_log = list(self.rule_log)
new.current_resolving_ability = self.current_resolving_ability
new.current_resolving_member = self.current_resolving_member
new.current_resolving_member_id = self.current_resolving_member_id
new.looked_cards = list(self.looked_cards)
# Manual copy of triggered_abilities: List[Tuple[int, Ability, Dict[str, Any]]]
# Tuple is immutable, Ability is shared, Dict needs copy
new.triggered_abilities = [(ta[0], ta[1], ta[2].copy()) for ta in self.triggered_abilities]
# Copy vectorized state
np.copyto(new.pending_choices_vec, self.pending_choices_vec)
new.pending_choices_ptr = self.pending_choices_ptr
np.copyto(new.triggered_abilities_vec, self.triggered_abilities_vec)
new.triggered_abilities_ptr = self.triggered_abilities_ptr
new.fast_mode = self.fast_mode
new._trigger_buffers = [list(self._trigger_buffers[0]), list(self._trigger_buffers[1])]
new.state_history = list(self.state_history)
new.loop_draw = self.loop_draw
new.loop_draw = self.loop_draw
# Optimization: Use shallow copy instead of deepcopy.
# The engine only performs assignment (replace) or clear() (structure),
# not in-place mutation of the nested lists.
new.performance_results = self.performance_results.copy()
# Copy deferred activation state (Rule 9.7 logic)
if hasattr(self, "pending_activation") and self.pending_activation:
new.pending_activation = self.pending_activation.copy()
if "context" in new.pending_activation:
new.pending_activation["context"] = new.pending_activation["context"].copy()
else:
new.pending_activation = None
def inject_card(self, player_idx: int, card_id: int, zone: str, position: int = -1) -> None:
"""Inject a card into a specific zone for testing purposes."""
if player_idx < 0 or player_idx >= len(self.players):
raise ValueError("Invalid player index")
p = self.players[player_idx]
if zone == "hand":
if position == -1:
p.hand.append(card_id)
else:
p.hand.insert(position, card_id)
elif zone == "energy":
if position == -1:
p.energy_zone.append(card_id)
else:
p.energy_zone.insert(position, card_id)
elif zone == "live":
if position == -1:
p.live_zone.append(card_id)
p.live_zone_revealed.append(False)
else:
p.live_zone.insert(position, card_id)
p.live_zone_revealed.insert(position, False)
elif zone == "stage":
if position < 0 or position >= 3:
raise ValueError("Stage position must be 0-2")
p.stage[position] = card_id
else:
raise ValueError(f"Invalid zone: {zone}")
@property
def active_player(self) -> PlayerState:
return self.players[self.current_player]
@property
def inactive_player(self) -> PlayerState:
return self.players[1 - self.current_player]
def is_terminal(self) -> bool:
"""Check if game has ended"""
return self.game_over
def get_winner(self) -> int:
"""Returns winner (0 or 1) or -1 if not terminal, 2 if draw"""
return self.winner
def check_win_condition(self) -> None:
"""Check if anyone has won (3+ successful lives)"""
p0_lives = len(self.players[0].success_lives)
p1_lives = len(self.players[1].success_lives)
if p0_lives >= 3 and p1_lives >= 3:
self.game_over = True
if p0_lives > p1_lives:
self.winner = 0
elif p1_lives > p0_lives:
self.winner = 1
else:
self.winner = 2 # Draw
elif p0_lives >= 3:
# Rule 1.2.1.1: Player 0 wins by 3 successful lives
self.game_over = True
self.winner = 0
if hasattr(self, "log_rule"):
self.log_rule("Rule 1.2.1.1", "Player 0 wins by 3 successful lives.")
elif p1_lives >= 3:
# Rule 1.2.1.1: Player 1 wins by 3 successful lives
self.game_over = True
self.winner = 1
if hasattr(self, "log_rule"):
self.log_rule("Rule 1.2.1.1", "Player 1 wins by 3 successful lives.")
def _is_card_legal_for_choice(self, card_id: int, params: Dict[str, Any]) -> bool:
"""Helper to check if a card matches the filter criteria for a choice."""
if card_id < 0:
return False
# Determine if it's a member or live card
card = self.member_db.get(card_id) or self.live_db.get(card_id)
if not card:
return False
# 1. Type filter
req_type = params.get("filter", params.get("type"))
if req_type == "member" and card_id not in self.member_db:
return False
if req_type == "live" and card_id not in self.live_db:
return False
# 2. Group filter
group_filter = params.get("group")
if group_filter:
target_group = Group.from_japanese_name(group_filter)
if target_group not in getattr(card, "groups", []):
# Also check units just in case
target_unit = Unit.from_japanese_name(group_filter)
if target_unit not in getattr(card, "units", []):
return False
# 3. Cost filter
cost_max = params.get("cost_max")
if cost_max is not None and getattr(card, "cost", 0) > cost_max:
return False
cost_min = params.get("cost_min")
if cost_min is not None and getattr(card, "cost", 0) < cost_min:
return False
return True
def get_legal_actions(self) -> np.ndarray:
"""
Returns a mask of legal actions (Rule 9.5.4:
Expanded for Complexity:
200-202: Activate ability of member in Area (LEFT, CENTER, RIGHT)
300-359: Mulligan toggle
400-459: Live Set
500-559: Choose card in hand (index 0-59) for effect target
560-562: Choose member on stage (Area 0-2) for effect target
590-599: Choose pending trigger to resolve
"""
mask = np.zeros(2000, dtype=bool)
if self.game_over:
return mask
# Priority: If there are choices to be made for a pending effect
if self.pending_choices:
choice_type, params = self.pending_choices[0]
p_idx = params.get("player_id", self.current_player)
p = self.players[p_idx]
if choice_type == "TARGET_HAND":
# Allow skip for optional costs
if params.get("is_optional"):
mask[0] = True
found = False
if len(p.hand) > 0:
for i, cid in enumerate(p.hand):
is_legal = self._is_card_legal_for_choice(cid, params)
if self.verbose:
print(f"DEBUG: TARGET_HAND check idx={i} cid={cid} legal={is_legal} params={params}")
if is_legal:
mask[500 + i] = True
found = True
if not found:
mask[0] = True # No valid cards in hand, allow pass logic (fizzle)
elif choice_type == "TARGET_MEMBER" or choice_type == "TARGET_MEMBER_SLOT":
# 560-562: Selected member on stage
found = False
for i in range(3):
if p.stage[i] >= 0 or choice_type == "TARGET_MEMBER_SLOT":
# Filter: for 'activate', only tapped members are legal
if params.get("effect") == "activate" and not p.tapped_members[i]:
continue
# Apply general filters if card exists
if p.stage[i] >= 0:
if not self._is_card_legal_for_choice(p.stage[i], params):
continue
mask[560 + i] = True
found = True
if not found:
mask[0] = True # No valid targets on stage, allow pass (fizzle)
elif choice_type == "DISCARD_SELECT":
# 500-559: Select card in hand to discard
# Allow skip for optional costs
if params.get("is_optional"):
mask[0] = True
found = False
if len(p.hand) > 0:
for i, cid in enumerate(p.hand):
if self._is_card_legal_for_choice(cid, params):
mask[500 + i] = True
found = True
if not found and params.get("is_optional"):
mask[0] = True # No cards to discard, allow pass
elif choice_type == "MODAL" or choice_type == "SELECT_MODE":
# params['options'] is a list of strings or list of lists
options = params.get("options", [])
for i in range(len(options)):
mask[570 + i] = True
elif choice_type == "CHOOSE_FORMATION":
# For now, just a dummy confirm? Or allow re-arranging?
# Simplified: Action 0 to confirm current formation
mask[0] = True
elif choice_type == "COLOR_SELECT":
# 580: Red, 581: Blue, 582: Green, 583: Yellow, 584: Purple, 585: Pink
for i in range(6):
mask[580 + i] = True
elif choice_type == "TARGET_OPPONENT_MEMBER":
# Opponent Stage 0-2 -> Action 600-602
opp = self.inactive_player
found = False
for i in range(3):
if opp.stage[i] >= 0:
mask[600 + i] = True
found = True
if not found:
# If no valid targets but choice exists, softlock prevention:
# Ideally we should strictly check before pushing choice, but safe fallback:
mask[0] = True # Pass/Cancel
elif choice_type == "SELECT_FROM_LIST":
# 600-659: List selection (up to 60 items)
cards = params.get("cards", [])
card_count = min(len(cards), 60)
if card_count > 0:
mask[600 : 600 + card_count] = True
else:
mask[0] = True # Empty list, allow pass
elif choice_type == "SELECT_FROM_DISCARD":
# 660-719: Discard selection (up to 60 items)
cards = params.get("cards", [])
card_count = min(len(cards), 60)
if card_count > 0:
mask[660 : 660 + card_count] = True
else:
mask[0] = True # Empty discard, allow pass
elif choice_type == "SELECT_FORMATION_SLOT" or choice_type == "SELECT_ORDER":
# 720-759: Item selection from a list (Formation)
cards = params.get("cards", params.get("available_members", []))
card_count = min(len(cards), 40)
if card_count > 0:
mask[720 : 720 + card_count] = True
else:
mask[0] = True
elif choice_type == "SELECT_SWAP_SOURCE":
# 600-659: Reuse list selection range
cards = params.get("cards", [])
card_count = min(len(cards), 60)
if card_count > 0:
mask[600 : 600 + card_count] = True
else:
mask[0] = True
elif choice_type == "SELECT_SWAP_TARGET":
# 500-559: Target hand range
if len(p.hand) > 0:
for i in range(len(p.hand)):
mask[500 + i] = True
else:
mask[0] = True
elif choice_type == "SELECT_SUCCESS_LIVE" or choice_type == "TARGET_SUCCESS_LIVES":
# 760-819: Select from passed lives list (Score)
cards = params.get("cards", p.success_lives)
card_count = min(len(cards), 60)
if card_count > 0:
mask[760 : 760 + card_count] = True
else:
mask[0] = True
elif choice_type == "TARGET_LIVE":
# 820-822: Select specific slot in Live Zone
for i in range(len(p.live_zone)):
mask[820 + i] = True
if not any(mask[820:823]):
mask[0] = True
elif choice_type == "TARGET_ENERGY_ZONE":
# 830-849: Select specific card in Energy Zone
for i in range(len(p.energy_zone)):
if i < 20:
mask[830 + i] = True
if not any(mask[830:850]):
mask[0] = True
elif choice_type == "TARGET_REMOVED":
# 850-909: Select from Removed cards
count = min(len(self.removed_cards), 60)
if count > 0:
mask[850 : 850 + count] = True
else:
mask[0] = True
elif choice_type == "TARGET_DECK" or choice_type == "TARGET_ENERGY_DECK" or choice_type == "TARGET_DISCARD":
# List selection ranges
cards = params.get("cards", [])
card_count = min(len(cards), 60)
offset = 600 if choice_type != "TARGET_DISCARD" else 660
if card_count > 0:
mask[offset : offset + card_count] = True
else:
mask[0] = True
# MULLIGAN phases: Select cards to return or confirm mulligan
elif self.phase in (Phase.MULLIGAN_P1, Phase.MULLIGAN_P2):
p = self.active_player
mask[0] = True # Confirm mulligan (done selecting)
# Actions 300-359: Select card for mulligan (card index 0-59)
# Note: We allow toggling selection.
m_sel = getattr(p, "mulligan_selection", set())
for i in range(len(p.hand)):
mask[300 + i] = True
# Auto-advance phases: these phases process automatically in 'step' when any valid action is received
# We allow Action 0 (Pass) to trigger the transition.
elif self.phase in (Phase.ACTIVE, Phase.ENERGY):
mask[0] = True
elif self.phase == Phase.PERFORMANCE_P1 or self.phase == Phase.PERFORMANCE_P2:
p = self.active_player
mask[0] = True # Always can pass (skip performance)
# Check all lives in live zone
for i, card_id in enumerate(p.live_zone):
# Standard Live ID as Action ID
if card_id in self.live_db:
live_card = self.live_db[card_id]
# Check requirements
reqs = getattr(live_card, "required_hearts", [0] * 7)
if len(reqs) < 7:
reqs = [0] * 7
stage_hearts = [0] * 7
total_blades = 0
for slot in range(3):
sid = p.stage[slot]
if sid in self.member_db:
m = self.member_db[sid]
total_blades += m.blades
# Determine color index (1-6) from hearts
# Heuristic: Find first non-zero index in hearts array
# This mimics vector_env logic
col = 0
h_arr = m.hearts
for cidx, val in enumerate(h_arr):
if val > 0:
col = cidx + 1
break
if 1 <= col <= 6:
stage_hearts[col] += m.hearts[col - 1] # m.hearts is 0-indexed?
# Wait, GameState initializes hearts_vec with m.hearts
# m.hearts is usually [Pink, Red, ...]
# Let's assume m.hearts is standard 7-dim or 6-dim
# If m.hearts[0] is Pink (Color 1), then:
pass
# Re-calculating correctly using GameState helper if available,
# else manual sum matching VectorEnv
# Optimized check:
# Use existing helper? p.get_effective_hearts?
# But that returns vector.
# Let's use p.stage stats directly
current_hearts = [0] * 7
current_blades = 0
for slot in range(3):
if p.stage[slot] != -1:
eff_h = p.get_effective_hearts(slot, self.member_db)
for c in range(7):
current_hearts[c] += eff_h[c]
current_blades += p.get_effective_blades(slot, self.member_db)
# Check against reqs
# reqs[0] is usually Any? Or Pink?
# In VectorEnv: 12-18 (Pink..Purple, All)
# live_card.required_hearts is 0-indexed typically [Pink, Red, Yel, Grn, Blu, Pur, Any]
met = True
# Check colors (0-5)
for c in range(6):
if current_hearts[c] < reqs[c]:
met = False
break
# Check Any (index 6, matches any color + explicit Any?)
# Usually Any req is satisfied by sum of all?
# For strictness, let's assume reqs[6] is specific "Any" points needed (wildcard).
# VectorEnv logic was:
# if stage_hearts[1] < req_pink...
# Assuming standard behavior:
if met and current_blades > 0:
mask[900 + i] = True
elif self.phase == Phase.DRAW or self.phase == Phase.LIVE_RESULT:
mask[0] = True
elif self.phase == Phase.MAIN:
p = self.active_player
# Can always pass
mask[0] = True
# --- SHARED PRE-CALCULATIONS ---
available_energy = p.count_untapped_energy()
total_reduction = 0
for ce in p.continuous_effects:
if ce["effect"].effect_type == EffectType.REDUCE_COST:
total_reduction += ce["effect"].value
# --- PLAY MEMBERS ---
if "placement" not in p.restrictions:
# JIT Optimization Path
# JIT Path disabled temporarily for training stability
if False and JIT_AVAILABLE and self._jit_member_costs is not None:
# Use pre-allocated hand buffer to avoid reallocation
hand_len = len(p.hand)
if hand_len > 0:
p.hand_buffer[:hand_len] = p.hand
calc_main_phase_masks(
p.hand_buffer[:hand_len],
p.stage,
available_energy,
total_reduction,
True, # Baton touch is always allowed if slot occupied
p.members_played_this_turn,
self._jit_member_costs,
mask,
)
else:
# Python Fallback
for i, card_id in enumerate(p.hand):
if card_id not in self.member_db:
continue
member = self.member_db[card_id]
for area in range(3):
action_id = 1 + i * 3 + area
if p.members_played_this_turn[area]:
continue
is_baton = p.stage[area] >= 0
# Calculate effective baton touch limit
extra_baton = sum(
ce["effect"].value
for ce in p.continuous_effects
if ce["effect"].effect_type == EffectType.BATON_TOUCH_MOD
)
effective_baton_limit = p.baton_touch_limit + extra_baton
if is_baton and p.baton_touch_count >= effective_baton_limit:
continue
# Calculate slot-specific cost
slot_reduction = sum(
ce["effect"].value
for ce in p.continuous_effects
if ce["effect"].effect_type == EffectType.REDUCE_COST
and (ce.get("target_slot", -1) in (-1, area))
)
active_cost = max(0, member.cost - slot_reduction)
if is_baton:
if p.stage[area] in self.member_db:
baton_mem = self.member_db[p.stage[area]]
active_cost = max(0, active_cost - baton_mem.cost)
if active_cost <= available_energy:
mask[action_id] = True
# DEBUG: Trace why specific cards fail
elif self.verbose and (member.cost >= 10 or card_id == 369):
print(
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}"
)
# --- ACTIVATE ABILITIES ---
# Uses same available_energy
for i, card_id in enumerate(p.stage):
if card_id >= 0 and card_id in self.member_db and not p.tapped_members[i]:
member = self.member_db[card_id]
for abi_idx, ab in enumerate(member.abilities):
if ab.trigger == TriggerType.ACTIVATED:
# Rule 9.7: Check once per turn
abi_key = f"{card_id}-{abi_idx}"
if ab.is_once_per_turn and abi_key in p.used_abilities:
continue
# Strict verification: Check conditions and costs
is_legal = True
if not all(self._check_condition(p, cond, context={"area": i}) for cond in ab.conditions):
is_legal = False
if is_legal and not self._can_pay_costs(p, ab.costs, source_area=i):
# print(f"DEBUG: Cost check failed for card {card_id} area {i}. Costs: {ab.costs}")
is_legal = False
if is_legal:
mask[200 + i] = True
# else:
# print(f"DEBUG: Ability {ab.raw_text} illegal for card {card_id} area {i}")
break # Only one ability activation per member slot
elif self.phase == Phase.LIVE_SET:
p = self.active_player
mask[0] = True
# Check live restriction (Rule 8.3.4.1 / Cluster 3)
if "live" not in p.restrictions and len(p.live_zone) < 3:
# Allow ANY card to be set (Rule 8.2.2: "Choose up to 3 cards from your hand")
for i, card_id in enumerate(p.hand):
mask[400 + i] = True
else:
# Other phases are automatic
mask[0] = True
# Safety check: Ensure at least one action is legal to prevent softlocks
if not np.any(mask):
# Force action 0 (Pass) as legal
mask[0] = True
# print(f"WARNING: No legal actions found in phase {self.phase.name}, forcing Pass action")
return mask
def step(self, action_id: int, check_legality: bool = True, in_place: bool = False) -> "GameState":
"""
Executes one step in the game (Rule 9).
Args:
action_id: The action to execute.
check_legality: Whether to verify action legality. Disable for speed if caller guarantees validity.
in_place: If True, modifies the state in-place instead of copying. Faster for RL.
"""
self.action_count_this_turn += 1
if self.action_count_this_turn > 1000:
self.game_over = True
self.winner = 2 # Draw due to runaway logic
self.log_rule("Safety", "Turn exceeded 1000 actions. Force terminating as Draw.")
return self
if self.game_over:
print(f"WARNING: Step called after Game Over (Winner: {self.winner}). Ignoring action {action_id}.")
return self
# Strict validation for debugging
if check_legality:
legal_actions = self.get_legal_actions()
if not legal_actions[action_id]:
# Soft fallback for illegal moves to prevent crashes
legal_indices = np.where(legal_actions)[0]
# print(
# f"ILLEGAL MOVE CAUGHT: Action {action_id} in phase {self.phase}. "
# f"PendingChoices: {len(self.pending_choices)}. "
# f"Fallback to first legal action: {legal_indices[0] if len(legal_indices) > 0 else 'None'}"
# )
if len(legal_indices) > 0:
if 0 in legal_indices:
action_id = 0
else:
action_id = int(legal_indices[0])
else:
self.game_over = True
self.winner = -2 # Special code for illegal move failure
return self
if in_place:
new_state = self
else:
new_state = self.copy()
new_state.log_rule("Rule 9.5", f"Processing action {action_id} in {new_state.phase} phase.")
# Check rule conditions before acting (Rule 9.5.1 / 10.1.2)
# MUST be done on new_state
new_state._process_rule_checks()
# Rule 9.5.4.1: Check timing occurs before play timing
new_state._process_rule_checks()
# Priority: If waiting for a choice (like targeting), handles that action
if new_state.pending_choices:
new_state._handle_choice(action_id)
# Otherwise, if resolving a complex effect stack
elif new_state.pending_effects:
new_state._resolve_pending_effect(0) # 0 is dummy action for auto-res
# Normal action execution
else:
new_state._execute_action(action_id)
# After any action, automatically process non-choice effects
while new_state.pending_effects and not new_state.pending_choices:
new_state._resolve_pending_effect(0) # 0 is dummy action for auto-res
# Rule 9.5.1: Final check timing after action resolution
new_state._process_rule_checks()
# Rule 12.1: Infinite Loop Detection
# Skip for Mulligan phases and if disabled
if new_state.enable_loop_detection and new_state.phase not in (Phase.MULLIGAN_P1, Phase.MULLIGAN_P2):
try:
# Capture key state tuple
state_tuple = (
new_state.phase,
new_state.current_player,
tuple(sorted(new_state.players[0].hand)),
tuple(new_state.players[0].stage),
tuple(tuple(x) for x in new_state.players[0].stage_energy),
tuple(new_state.players[0].energy_zone),
tuple(sorted(new_state.players[1].hand)),
tuple(new_state.players[1].stage),
tuple(tuple(x) for x in new_state.players[1].stage_energy),
tuple(new_state.players[1].energy_zone),
tuple(sorted(list(new_state.players[0].used_abilities))),
tuple(sorted(list(new_state.players[1].used_abilities))),
)
state_hash = hash(state_tuple)
new_state.state_history.append(state_hash)
if new_state.state_history.count(state_hash) >= 20:
new_state.log_rule("Rule 12.1", "Infinite Loop detected. Terminating as Draw.")
new_state.game_over = True
new_state.winner = 2 # Draw
new_state.loop_draw = True
except Exception:
# If hashing fails, just ignore for now to prevent crash
pass
return new_state
def get_observation(self) -> np.ndarray:
"""
Calculates a flat feature vector representing the game state for the AI (Rule 9.1).
New Layout (Size 320):
[0-36]: Metadata (Phase, Player, Choice Context)
[36-168]: Hand (12 cards x 11 floats) -> [Exist, ID, Cost, Blade, HeartVec(7)]
[168-204]: Self Stage (3 slots x 12 floats) -> [Exist, ID, Tapped, Blade, HeartVec(7), Energy]
[204-240]: Opponent Stage (3 slots x 12 floats) -> [Exist, ID, Tapped, Blade, HeartVec(7), Energy]
[240-270]: Live Zone (3 cards x 10 floats) -> [Exist, ID, Score, ReqHeartVec(7)]
[270-272]: Scores (Self, Opp)
[272]: Source ID of pending choice
[273-320]: Padding
"""
# Expanded observation size
features = np.zeros(320, dtype=np.float32)
# JIT Arrays Check
if GameState._jit_member_costs is None:
GameState._init_jit_arrays()
costs_db = GameState._jit_member_costs
hearts_vec_db = GameState._jit_member_hearts_vec
blades_db = GameState._jit_member_blades
live_score_db = GameState._jit_live_score
live_req_vec_db = GameState._jit_live_hearts_vec
# Max ID for normalization (add safety for 0 div)
max_id_val = float(costs_db.shape[0]) if costs_db is not None else 2000.0
# --- 1. METADATA [0:36] ---
# Phase (one-hot) [0:16] - using 11 slots
phase_val = int(self.phase) + 2
if 0 <= phase_val < 11:
features[phase_val] = 1.0
# Current Player [16:18]
features[16 + (1 if self.current_player == 1 else 0)] = 1.0
# Pending Choice [18:36]
if self.pending_choices:
features[18] = 1.0
choice_type, params = self.pending_choices[0]
# Populate Source ID if available [272]
source_id = params.get("card_id", -1)
if source_id >= 0:
features[272] = source_id / max_id_val
types = [
"TARGET_MEMBER",
"TARGET_HAND",
"SELECT_MODE",
"COLOR_SELECT",
"TARGET_OPPONENT_MEMBER",
"TARGET_MEMBER_SLOT",
"SELECT_SWAP_SOURCE",
"SELECT_FROM_LIST",
"SELECT_FROM_DISCARD",
"DISCARD_SELECT",
"MODAL",
"CHOOSE_FORMATION",
"SELECT_ORDER",
"SELECT_FORMATION_SLOT",
"SELECT_SUCCESS_LIVE",
]
try:
t_idx = types.index(choice_type)
features[19 + t_idx] = 1.0
except ValueError:
pass
if params.get("is_optional"):
features[35] = 1.0
# --- 2. HAND [36:168] (12 cards * 11 features) ---
p = self.players[self.current_player]
hand_len = len(p.hand)
n_hand = min(hand_len, 12)
if n_hand > 0:
hand_ids = np.array(p.hand[:n_hand], dtype=int)
base_idx = np.arange(n_hand) * 11 + 36
# Existence
features[base_idx] = 1.0
# ID
features[base_idx + 1] = hand_ids / max_id_val
# Cost
c = costs_db[hand_ids]
features[base_idx + 2] = np.clip(c, 0, 10) / 10.0
# Blade
b = blades_db[hand_ids]
features[base_idx + 3] = np.clip(b, 0, 10) / 10.0
# Heart Vectors (7 dim)
# Flatten 12x7 -> 84? No, interleaved.
# We need to assign (N, 7) into sliced positions.
# This is tricky with simple slicing if stride is not 1.
# Loop for safety or advanced indexing.
# shape of h_vecs: (n_hand, 7)
h_vecs = hearts_vec_db[hand_ids]
for i in range(n_hand):
start = 36 + i * 11 + 4
features[start : start + 7] = np.clip(h_vecs[i], 0, 5) / 5.0
# --- 3. SELF STAGE [168:204] (3 slots * 12 features) ---
for i in range(3):
cid = p.stage[i]
base = 168 + i * 12
if cid >= 0:
features[base] = 1.0
features[base + 1] = cid / max_id_val
features[base + 2] = 1.0 if p.tapped_members[i] else 0.0
# Effective Stats (retains python logic for modifiers)
eff_blade = p.get_effective_blades(i, self.member_db)
eff_hearts = p.get_effective_hearts(i, self.member_db) # vector
features[base + 3] = min(eff_blade / 10.0, 1.0)
# Hearts (7)
# eff_hearts is usually (6,) or (7,) or list
if isinstance(eff_hearts, (list, np.ndarray)):
h_len = min(len(eff_hearts), 7)
features[base + 4 : base + 4 + h_len] = np.array(eff_hearts[:h_len]) / 5.0
# Energy Count
features[base + 11] = min(len(p.stage_energy[i]) / 5.0, 1.0)
# --- 4. OPPONENT STAGE [204:240] (3 slots * 12 features) ---
opp = self.players[1 - self.current_player]
for i in range(3):
cid = opp.stage[i]
base = 204 + i * 12
if cid >= 0:
features[base] = 1.0
features[base + 1] = cid / max_id_val
features[base + 2] = 1.0 if opp.tapped_members[i] else 0.0
# Note: get_effective_blades requires accessing the opponent object relative to the DB
# but GameState usually uses p methods.
# p.get_effective_blades uses self.stage.
# So we call opp.get_effective_blades.
eff_blade = opp.get_effective_blades(i, self.member_db)
eff_hearts = opp.get_effective_hearts(i, self.member_db)
features[base + 3] = min(eff_blade / 10.0, 1.0)
if isinstance(eff_hearts, (list, np.ndarray)):
h_len = min(len(eff_hearts), 7)
features[base + 4 : base + 4 + h_len] = np.array(eff_hearts[:h_len]) / 5.0
features[base + 11] = min(len(opp.stage_energy[i]) / 5.0, 1.0)
# --- 5. LIVE ZONE [240:270] (3 cards * 10 features) ---
n_live = min(len(p.live_zone), 3)
if n_live > 0:
live_ids = np.array(p.live_zone[:n_live], dtype=int)
for i in range(n_live):
cid = live_ids[i]
base = 240 + i * 10
features[base] = 1.0
features[base + 1] = cid / max_id_val
features[base + 2] = np.clip(live_score_db[cid], 0, 5) / 5.0
# Req Heart Vec (7)
if live_req_vec_db is not None:
features[base + 3 : base + 10] = np.clip(live_req_vec_db[cid], 0, 5) / 5.0
# --- 6. SCORES [270:272] ---
features[270] = min(len(p.success_lives) / 5.0, 1.0)
features[271] = min(len(self.players[1 - self.current_player].success_lives) / 5.0, 1.0)
return features
def to_dict(self):
"""Serialize full game state."""
return {
"turn": self.turn_number,
"phase": self.phase,
"active_player": self.current_player,
"game_over": self.game_over,
"winner": self.winner,
"players": [p.to_dict(viewer_idx=0) for p in self.players],
"legal_actions": [], # Can populate if needed
"pending_choice": None,
"performance_results": {},
"rule_log": list(self.rule_log),
}
def get_reward(self, player_idx: int) -> float:
# Get reward for player (1.0 for win, -1.0 for loss, 0.0 for draw)
# Illegal move (-2) is treated as a loss (-1.0) for safety in standard RL,
# though explicit training usually handles this via masking or separate loss.
if self.winner == -2:
return -100.0 # Illegal move/Technical loss
if self.winner == player_idx:
return 100.0
elif self.winner == 1 - player_idx:
return -100.0
elif self.winner == 2: # Draw
return 0.0
elif self.winner == -1: # Ongoing
# Ongoing heuristic: Pure score difference
# Time penalties are now handled by the Gymnasium environment (per turn)
my_score = len(self.players[player_idx].success_lives)
opp_score = len(self.players[1 - player_idx].success_lives)
return float(my_score - opp_score)
def take_action(self, action_id: int) -> None:
"""In-place version of step() for testing and direct manipulation."""
if self.pending_choices:
self._handle_choice(action_id)
else:
self._execute_action(action_id)
# Process resulting effects
while self.pending_effects and not self.pending_choices:
self._resolve_pending_effect(0)
def create_sample_cards() -> Tuple[Dict[int, MemberCard], Dict[int, LiveCard]]:
"""Create sample cards for testing"""
members = {}
lives = {}
# Create 48 sample members with varying stats
for i in range(48):
cost = 2 + (i % 14) # Costs 2-15
blades = 1 + (i % 6) # Blades 1-6
hearts = np.zeros(7, dtype=np.int32) # Changed from 6 to 7
hearts[i % 6] = 1 + (i // 6 % 3) # 1-3 hearts of one color
if i >= 24:
hearts[(i + 1) % 6] = 1 # Second color for higher cost cards
blade_hearts = np.zeros(6, dtype=np.int32)
if i % 3 == 0:
blade_hearts[i % 6] = 1
members[i] = MemberCard(
card_id=i,
card_no=f"SAMPLE-M-{i}",
name=f"Member_{i}",
cost=cost,
hearts=hearts,
blade_hearts=blade_hearts,
blades=blades,
)
# Create 12 sample live cards
for i in range(12):
score = 1 + (i % 3) # Score 1-3
required = np.zeros(7, dtype=np.int32)
required[i % 6] = 2 + (i // 6) # 2-3 of one color required
required[6] = 1 + (i % 4) # 1-4 "any" hearts required
lives[100 + i] = LiveCard(
card_id=100 + i, card_no=f"SAMPLE-L-{i}", name=f"Live_{i}", score=score, required_hearts=required
)
return members, lives
def initialize_game(use_real_data: bool = True, deck_type: str = "normal") -> GameState:
"""
Create initial game state with shuffled decks.
Args:
use_real_data: Whether to try loading real cards.json data
deck_type: "normal" (random from DB) or "vanilla" (specific simple cards)
"""
# Try loading real data
if use_real_data and not GameState.member_db:
import traceback
# print("DEBUG: initialize_game attempting to load real data...")
try:
# Try current directory first (assuming run from root)
data_path = os.path.join(os.getcwd(), "data", "cards_compiled.json")
if not os.path.exists(data_path):
# Fallback to cards.json
data_path = os.path.join(os.getcwd(), "data", "cards.json")
if not os.path.exists(data_path):
# Absolute path fallback based on file location
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
data_path = os.path.join(base_dir, "data", "cards_compiled.json")
# print(f"DEBUG: Selected data path: {data_path}")
if not os.path.exists(data_path):
# print(f"ERROR: Data path does not exist: {data_path}")
pass
else:
loader = CardDataLoader(data_path)
m, l, e = loader.load()
if m:
GameState.member_db = m
GameState.live_db = l
print(f"SUCCESS: Loaded {len(m)} members and {len(l)} lives from {data_path}")
# Optimization: Cache cards with CONSTANT META_RULE effects
GameState._meta_rule_cards = set()
for cid, card in m.items():
for ab in card.abilities:
if ab.trigger.name == "CONSTANT": # Check string to avoid import if needed, or use enum
for eff in ab.effects:
if eff.effect_type.name == "META_RULE":
GameState._meta_rule_cards.add(cid)
break
for cid, card in l.items():
for ab in card.abilities:
if ab.trigger.name == "CONSTANT":
for eff in ab.effects:
if eff.effect_type.name == "META_RULE":
GameState._meta_rule_cards.add(cid)
break
GameState._init_jit_arrays()
else:
# print("WARNING: Loader returned empty member database.")
pass
except Exception as e:
print(f"CRITICAL: Failed to load real data: {e}")
import traceback
traceback.print_exc()
pass
traceback.print_exc()
if not GameState.member_db:
# print("WARNING: Falling back to SAMPLE cards. This may cause logic inconsistencies.")
# Fallback to sample
members, lives = create_sample_cards()
GameState.member_db = members
GameState.live_db = lives
GameState._init_jit_arrays()
state = GameState()
# Pre-calculate Vanilla Deck IDs if needed
vanilla_member_ids = []
vanilla_live_ids = []
if deck_type == "vanilla":
# Target Vanilla Members (4 copies each = 48)
# 5 Vanilla + 7 Simple
target_members = [
"PL!-sd1-010-SD",
"PL!-sd1-013-SD",
"PL!-sd1-014-SD",
"PL!-sd1-017-SD",
"PL!-sd1-018-SD", # Vanilla
"PL!-sd1-002-SD",
"PL!-sd1-005-SD",
"PL!-sd1-011-SD",
"PL!-sd1-012-SD",
"PL!-sd1-016-SD", # Simple
"PL!-sd1-015-SD",
"PL!-sd1-007-SD",
]
# Target Vanilla Lives (3 copies each = 12)
target_lives = ["PL!-sd1-019-SD", "PL!-sd1-020-SD", "PL!-sd1-021-SD", "PL!-sd1-022-SD"]
# 1. Map Members
found_members = {}
for cid, card in GameState.member_db.items():
if card.card_no in target_members:
found_members[card.card_no] = cid
# 2. Map Lives
found_lives = {}
for cid, card in GameState.live_db.items():
if card.card_no in target_lives:
found_lives[card.card_no] = cid
# 3. Construct Lists
for tm in target_members:
if tm in found_members:
vanilla_member_ids.extend([found_members[tm]] * 4)
else:
# print(f"WARNING: Vanilla card {tm} not found in DB!")
pass
for tl in target_lives:
if tl in found_lives:
vanilla_live_ids.extend([found_lives[tl]] * 3)
else:
# print(f"WARNING: Vanilla live {tl} not found in DB!")
pass
# Fill if missing?
if len(vanilla_member_ids) < 48:
# print(f"WARNING: Vanilla deck incomplete ({len(vanilla_member_ids)}), filling with randoms.")
pass
remaining = 48 - len(vanilla_member_ids)
all_ids = list(GameState.member_db.keys())
if all_ids:
vanilla_member_ids.extend(np.random.choice(all_ids, remaining).tolist())
if len(vanilla_live_ids) < 12:
# print(f"WARNING: Vanilla live deck incomplete ({len(vanilla_live_ids)}), filling with randoms.")
pass
remaining = 12 - len(vanilla_live_ids)
all_ids = list(GameState.live_db.keys())
if all_ids:
vanilla_live_ids.extend(np.random.choice(all_ids, remaining).tolist())
# Prepare Verified/Random lists if needed
verified_member_ids = []
verified_live_ids = []
if deck_type == "random_verified":
try:
pool_path = os.path.join(os.getcwd(), "verified_card_pool.json")
if os.path.exists(pool_path):
with open(pool_path, "r", encoding="utf-8") as f:
pool = json.load(f)
v_members = pool.get("verified_abilities", [])
v_vanilla = pool.get("vanilla_members", [])
total_v_members = v_members + v_vanilla
# Filter DB for these card_nos
for cid, card in GameState.member_db.items():
if card.card_no in total_v_members:
verified_member_ids.append(cid)
v_lives = pool.get("vanilla_lives", []) # Or use vanilla_lives as a base for lives
for cid, card in GameState.live_db.items():
if card.card_no in v_lives:
verified_live_ids.append(cid)
if not verified_member_ids or not verified_live_ids:
# print(f"WARNING: Verified pool empty after filtering! Check card_nos. falling back.")
pass
else:
# print(f"WARNING: verified_card_pool.json not found at {pool_path}")
pass
except Exception:
# print(f"ERROR: Failed to load verified pool: {e}")
pass
for p_idx in range(2):
p = state.players[p_idx]
# Build decks
if deck_type == "vanilla":
member_ids = list(vanilla_member_ids) # Copy
live_ids = list(vanilla_live_ids) # Copy
elif deck_type == "random_verified" and verified_member_ids and verified_live_ids:
# 48 members, 12 lives
member_ids = list(np.random.choice(verified_member_ids, 48, replace=True))
live_ids = list(np.random.choice(verified_live_ids, 12, replace=True))
else:
# Random Normal Deck
# Random Normal Deck
member_ids = list(GameState.member_db.keys())
live_ids = list(GameState.live_db.keys())
# Filter if too many? For now just take random subset if huge
if len(member_ids) > 48:
member_ids = list(np.random.choice(member_ids, 48, replace=False))
if len(live_ids) > 12:
live_ids = list(np.random.choice(live_ids, 12, replace=False))
energy_ids = list(range(200, 212))
np.random.shuffle(member_ids)
np.random.shuffle(live_ids)
np.random.shuffle(energy_ids)
p.main_deck = member_ids + live_ids
np.random.shuffle(p.main_deck)
p.energy_deck = energy_ids
# Initial draw: 6 cards (Rule 6.2.1.5)
# Note: log_rule isn't available on GameState yet as it's a static function creating state
# but we can print or add a log entry to the state's internal log if it has one.
# Actually, let's just make sure the draw happens.
for _ in range(6):
if p.main_deck:
p.hand.append(p.main_deck.pop(0))
# Log initial setup rules (Rule 6.2.1.5 and 6.2.1.7)
state.rule_log.append({"rule": "Rule 6.2.1.5", "description": "Both players draw 6 cards as starting hand."})
state.rule_log.append(
{"rule": "Rule 6.2.1.7", "description": "Both players place 3 cards from Energy Deck to Energy Zone."}
)
# Set initial phase to Mulligan
state.phase = Phase.MULLIGAN_P1
# Randomly determine first player
state.first_player = np.random.randint(2)
state.current_player = state.first_player
# Rule 6.2.1.7: Both players place top 3 cards of Energy Deck into Energy Zone
for p in state.players:
p.energy_zone = []
for _ in range(3):
if p.energy_deck:
p.energy_zone.append(p.energy_deck.pop(0))
return state
if __name__ == "__main__":
# Test game creation and basic flow
game = initialize_game()
print(f"Game initialized. First player: {game.first_player}")
print(f"P0 hand: {len(game.players[0].hand)} cards")
print(f"P1 hand: {len(game.players[1].hand)} cards")
print(f"Phase: {game.phase.name}")
# Run a few random actions
for step in range(20):
if game.is_terminal():
print(f"Game over! Winner: {game.get_winner()}")
break
legal = game.get_legal_actions()
legal_indices = np.where(legal)[0]
if len(legal_indices) == 0:
print("No legal actions!")
break
action = np.random.choice(legal_indices)
game = game.step(action)
print(
f"Step {step}: Action {action}, Phase {game.phase}, "
f"Player {game.current_player}, "
f"P0 lives: {len(game.players[0].success_lives)}, "
f"P1 lives: {len(game.players[1].success_lives)}"
)
# --- COMPREHENSIVE RULEBOOK INDEX (v1.04) ---
# This index ensures 100% searchability of all official rule identifiers.
#
# Rule 1:
# Rule 1.1:
# Rule 1.1.1:
# Rule 1.2:
# Rule 1.2.1:
# Rule 1.2.1.1: ??E??????v???C???[????????C?u?J?[?h?u
# Rule 1.2.1.2: ??????v???C???[????????3 ????????
# Rule 1.2.2:
# Rule 1.2.3:
# Rule 1.2.3.1: ???E???s???s???A???????J?[?h?E?e????E
# Rule 1.2.4:
# Rule 1.3:
# Rule 1.3.1:
# Rule 1.3.2:
# Rule 1.3.2.1: ????????????????E?????????E??
# Rule 1.3.2.2: ????s???????s??????P?????E??????E
# Rule 1.3.2.3: ????s????v???????????E?????????A??
# Rule 1.3.2.4: ?v???C???[??E???[?h????????l?E????A????
# Rule 1.3.3:
# Rule 1.3.4:
# Rule 1.3.4.1: ?????????E????v???C???[??K?p????AE
# Rule 1.3.4.2: ???E?J????J?[?h?????I???????
# Rule 1.3.5:
# Rule 1.3.5.1: ?J?[?h???[??????e?`???f?E??????
# Rule 2:
# Rule 2.1:
# Rule 2.1.1:
# Rule 2.1.2:
# Rule 2.1.3:
# Rule 2.2:
# Rule 2.2.1:
# Rule 2.2.2:
# Rule 2.2.2.1: ?J?[?h?^?C?v?????C?u?????J?[?h?E?A?Q?[??
# Rule 2.2.2.1.1: ?X?R?A?E?E.10?E????E???n?[?g?IE.11?E???????
# Rule 2.2.2.2: ?J?[?h?^?C?v???????o?E?????J?[?h?E?A???C
# Rule 2.2.2.2.1: ?R?X?g?IE.6?E???n?E?g?IE.9?E???????J?[?`E
# Rule 2.2.2.3: ?J?[?h?^?C?v???G?l???M?[?????J?[?h?E?A??
# Rule 2.2.2.3.1: ?J?[?h??????e?G?l???M?[?J?[?h?f??\?E
# Rule 2.3:
# Rule 2.3.1:
# Rule 2.3.2:
# Rule 2.3.2.1: ?J?[?h?????E?E?????????o?E?J?[?h?E?AE??E??
# Rule 2.3.2.2: ?`E???X?g???A?u?v?i?????????E??????????
# Rule 2.4:
# Rule 2.4.1:
# Rule 2.4.2:
# Rule 2.4.2.1: ?J?[?h?????E?E?????????o?E?J?[?h?E?AE??E??
# Rule 2.4.2.2: ?????o?E?????O???[?v????????E?A??
# Rule 2.4.3:
# Rule 2.4.3.1: ?`E???X?g???A?w?x?i??d?????????E??????
# Rule 2.4.4:
# Rule 2.5:
# Rule 2.5.1:
# Rule 2.5.2:
# Rule 2.5.3:
# Rule 2.6:
# Rule 2.6.1:
# Rule 2.7:
# Rule 2.7.1:
# Rule 2.7.2:
# Rule 2.8:
# Rule 2.8.1:
# Rule 2.8.2:
# Rule 2.9:
# Rule 2.9.1:
# Rule 2.9.2:
# Rule 2.9.3:
# Rule 2.10:
# Rule 2.10.1:
# Rule 2.11:
# Rule 2.11.1:
# Rule 2.11.2:
# Rule 2.11.2.1: ??E???[?g??????A?c??
# Rule 2.11.2.2: ?n?E?g???????E????????A????S???
# Rule 2.11.3:
# Rule 2.12:
# Rule 2.12.1:
# Rule 2.12.2:
# Rule 2.12.3:
# Rule 2.12.4:
# Rule 2.13:
# Rule 2.13.1:
# Rule 2.13.2:
# Rule 2.14:
# Rule 2.14.1:
# Rule 2.14.2:
# Rule 2.14.3:
# Rule 3:
# Rule 3.1:
# Rule 3.1.1:
# Rule 3.1.2:
# Rule 3.1.2.1: ???E???E?}?X?^?[???A????\???L????E
# Rule 3.1.2.2: ?N???E???E?}?X?^?[???A??????v???C????
# Rule 3.1.2.3: ?????E???E?}?X?^?[???A????\???L????E
# Rule 3.1.2.4: ?????E?}?X?^?[???A??????????????E
# Rule 3.1.2.4.1: ?????????????v???C???[???w?E
# Rule 4:
# Rule 4.1:
# Rule 4.1.1:
# Rule 4.1.2:
# Rule 4.1.2.1: ???J????J?[?h???u???????A????
# Rule 4.1.2.2: ?????E?J?????????J??????????
# Rule 4.1.2.3: ???E?J??????????A???????J?[?`E
# Rule 4.1.3:
# Rule 4.1.3.1: ??E???????E???????J?[?h?E??E????AE
# Rule 4.1.4:
# Rule 4.1.4.1: ????J?[?h????????E??A???????????E
# Rule 4.1.5:
# Rule 4.1.5.1: ???J???Y????E?J?????E????J?[?h??
# Rule 4.1.6:
# Rule 4.1.7:
# Rule 4.2:
# Rule 4.2.1:
# Rule 4.2.2:
# Rule 4.2.3:
# Rule 4.3:
# Rule 4.3.1:
# Rule 4.3.2:
# Rule 4.3.2.1: ?A?N?`E???u????E?J?[?h?E?A????J?[?h?E?}?X
# Rule 4.3.2.2: ?E?F?C?g????E?J?[?h?E?A????J?[?h?E?}?X
# Rule 4.3.2.3: ?z?u??????E??????????J?[?h???u??E
# Rule 4.3.3:
# Rule 4.3.3.1: ?\????????E?J?[?h?E?A?J?[?h?E?E????????E
# Rule 4.3.3.2: ??????????E?J?[?h?E?A?J?[?h?E?E????????E
# Rule 4.4:
# Rule 4.4.1:
# Rule 4.4.2:
# Rule 4.5:
# Rule 4.5.1:
# Rule 4.5.1.1: ?`E???X?g????P??e?G???A?f?????????E????
# Rule 4.5.2:
# Rule 4.5.2.1: ??E?????o?E?G???A??A??????e???T?C?h?G??
# Rule 4.5.2.2: ????v???C???[???????A???T?C?h?G???A??
# Rule 4.5.2.3: ????v???C???[???????A???T?C?h?G???A??
# Rule 4.5.3:
# Rule 4.5.4:
# Rule 4.5.5:
# Rule 4.5.5.1: ?????o?E?G???A??????o?E?J?[?h?E????d?E
# Rule 4.5.5.2: ?????o?E?G???A??????o?E?J?[?h?E????d?E
# Rule 4.5.5.3: ?????o?E?G???A??????o?E?????E?????o?E?G
# Rule 4.5.5.4: ?????o?E?G???A??????o?E???????o?E?G???A
# Rule 4.5.6:
# Rule 4.6:
# Rule 4.6.1:
# Rule 4.6.2:
# Rule 4.7:
# Rule 4.7.1:
# Rule 4.7.2:
# Rule 4.7.3:
# Rule 4.7.4:
# Rule 4.8:
# Rule 4.8.1:
# Rule 4.8.2:
# Rule 4.8.3:
# Rule 4.8.4:
# Rule 4.9:
# Rule 4.9.1:
# Rule 4.9.2:
# Rule 4.9.3:
# Rule 4.9.4:
# Rule 4.10:
# Rule 4.10.1:
# Rule 4.10.2:
# Rule 4.11:
# Rule 4.11.1:
# Rule 4.11.2:
# Rule 4.11.3:
# Rule 4.12:
# Rule 4.12.1:
# Rule 4.12.2:
# Rule 4.13:
# Rule 4.13.1:
# Rule 4.13.2:
# Rule 4.14:
# Rule 4.14.1:
# Rule 4.14.2:
# Rule 5:
# Rule 5.1:
# Rule 5.1.1:
# Rule 5.2:
# Rule 5.2.1:
# Rule 5.3:
# Rule 5.3.1:
# Rule 5.4:
# Rule 5.4.1:
# Rule 5.5:
# Rule 5.5.1:
# Rule 5.5.1.1: ?J?[?h?Q?????P?????????E????????
# Rule 5.5.1.2: ?J?[?h?Q??J?[?h??0 ??????E1 ???E???E
# Rule 5.6:
# Rule 5.6.1:
# Rule 5.6.2:
# Rule 5.6.3:
# Rule 5.6.3.1: ?E????l?E???0 ??????????E?A?????E????E
# Rule 5.6.3.2: ??E???E???C???[????E??E?????I?E???????E
# Rule 5.6.3.3: ??E???E???C???[??J?[?h??1 ??????????AE
# Rule 5.6.3.4: ???E??E??????5.6.3.3 ?????s????????i??
# Rule 5.7:
# Rule 5.7.1:
# Rule 5.7.2:
# Rule 5.7.2.1: ?E????l?E???0 ??????????E?A?????E????E
# Rule 5.7.2.2: ?????????1 ???w??????AE
# Rule 5.7.2.3: ??E???E???C???[????E??E?????I?E???????E
# Rule 5.7.2.4: ??E???E???C???[??A???C???`E???L?u??????
# Rule 5.7.2.5: ???E??E??????5.7.2.4 ?????s????????i??
# Rule 5.8:
# Rule 5.8.1:
# Rule 5.8.2:
# Rule 5.9.1:
# Rule 5.9.1.1: ?E
# Rule 5.10:
# Rule 5.10.1:
# Rule 6:
# Rule 6.1:
# Rule 6.1.1:
# Rule 6.1.1.1: ???C???`E???L??A?????o?E?J?[?`E8 ??????E????
# Rule 6.1.1.2: ???C???`E???L???A?J?[?h?i???o?E???????
# Rule 6.1.1.3: ?G?l???M?[?`E???L??A?G?l???M?[?J?[?`E2
# Rule 6.1.2:
# Rule 6.2:
# Rule 6.2.1:
# Rule 6.2.1.1: ???E?Q?[????g?p?????g??`E???L???
# Rule 6.2.1.2: ??E?E???C???[????g????C???`E???L???E?g??
# Rule 6.2.1.3: ??E?E???C???[????g??G?l???M?[?`E???L??E
# Rule 6.2.1.4: ??E?E???C???[????????????E?v???C???[
# Rule 6.2.1.5: ??E?E???C???[????g????C???`E???L?u?????
# Rule 6.2.1.6: ??U?v???C???[?????E???A?e?v???C???[???
# Rule 6.2.1.7: ??E?E???C???[????g??G?l???M?[?`E???L?u
# Rule 7:
# Rule 7.1:
# Rule 7.1.1:
# Rule 7.1.2:
# Rule 7.2:
# Rule 7.2.1:
# Rule 7.2.1.1: ???v???C???[???w????t?F?C?Y????A??
# Rule 7.2.1.2: ???v???C???[???w?????E???F?C?Y????AE
# Rule 7.2.2:
# Rule 7.3:
# Rule 7.3.1:
# Rule 7.3.2:
# Rule 7.3.2.1: ???t?F?C?Y???A?E?U?v???C???[?????`E
# Rule 7.3.3:
# Rule 7.4:
# Rule 7.4.1:
# Rule 7.4.2:
# Rule 7.4.3:
# Rule 7.5:
# Rule 7.5.1:
# Rule 7.5.2:
# Rule 7.5.3:
# Rule 7.6:
# Rule 7.6.1:
# Rule 7.6.2:
# Rule 7.6.3:
# Rule 7.7:
# Rule 7.7.1:
# Rule 7.7.2:
# Rule 7.7.2.1: ???E?E?J?[?h??????N???E???1 ??I???AE
# Rule 7.7.2.2: ???E?E??D??????o?E?J?[?h??1 ???I???A??
# Rule 7.7.3:
# Rule 7.8:
# Rule 7.8.1:
# Rule 8:
# Rule 8.1:
# Rule 8.1.1:
# Rule 8.1.2:
# Rule 8.2:
# Rule 8.2.1:
# Rule 8.2.2:
# Rule 8.2.3:
# Rule 8.2.4:
# Rule 8.2.5:
# Rule 8.3:
# Rule 8.3.1:
# Rule 8.3.2:
# Rule 8.3.2.1: ?p?t?H?[?}???X?t?F?C?Y???A?E?U?v???C
# Rule 8.3.3:
# Rule 8.3.4:
# Rule 8.3.4.1: ???v???C???[???e???C?u??????E???????E
# Rule 8.3.5:
# Rule 8.3.6:
# Rule 8.3.7:
# Rule 8.3.8:
# Rule 8.3.9:
# Rule 8.3.10:
# Rule 8.3.11:
# Rule 8.3.12:
# Rule 8.3.13:
# Rule 8.3.14:
# Rule 8.3.15:
# Rule 8.3.15.1: ???????C?u???L?n?[?g????A??????C?`E
# Rule 8.3.15.1.1: ???E??A?e
# Rule 8.3.15.1.2: ????????E???C?u?J?[?h?E?E??E
# Rule 8.3.16:
# Rule 8.3.17:
# Rule 8.4:
# Rule 8.4.1:
# Rule 8.4.2:
# Rule 8.4.2.1: ???E??A?e?v???C???[????g??G?[????
# Rule 8.4.3:
# Rule 8.4.3.1: ??????v???C???[???????E???C?u?J?[?h?u
# Rule 8.4.3.2: ?????v???C???[????C?u?J?[?h?u?????
# Rule 8.4.3.3: ??????v???C???[????C?u?J?[?h?u?????
# Rule 8.4.4:
# Rule 8.4.5:
# Rule 8.4.6:
# Rule 8.4.6.1: ??????v???C???[???????E???C?u?J?[?h?u
# Rule 8.4.6.2: ??E??????v???C???[????C?u?J?[?h?u????
# Rule 8.4.7:
# Rule 8.4.7.1: ??????v???C???[???????????E?????E
# Rule 8.4.8:
# Rule 8.4.9:
# Rule 8.4.10:
# Rule 8.4.11:
# Rule 8.4.12:
# Rule 8.4.13: 8.4.7 ???????A?????v???C???[?????E?????C
# Rule 8.4.14:
# Rule 9:
# Rule 9.1:
# Rule 9.1.1:
# Rule 9.1.1.1: ?N???E????A?E???C?^?C?~???O???^?????
# Rule 9.1.1.1.1: ?N???E???E?A?J?[?h?????E
# Rule 9.1.1.2: ?????E????A????\?????????????E
# Rule 9.1.1.2.1: ?????E???E?A?J?[?h?????E
# Rule 9.1.1.3: ???E????A????\????L???????A??
# Rule 9.1.1.3.1: ???E???E?A?J?[?h?????E
# Rule 9.2:
# Rule 9.2.1:
# Rule 9.2.1.1: ?e?P??????f???A??????????E??E???????E
# Rule 9.2.1.2: ?e?p??????f???A????E???????i?????
# Rule 9.2.1.3: ?e?u??????f???A?Q?[???????????????
# Rule 9.2.1.3.1: ?\???e?i?s??A?E??????A???????E??E
# Rule 9.2.1.3.2: ?\???e?i?s??A?E??????A??????[?I
# Rule 9.3:
# Rule 9.3.1:
# Rule 9.3.2:
# Rule 9.3.3:
# Rule 9.3.4:
# Rule 9.3.4.1: ?????E????E?????E????v???C????E
# Rule 9.3.4.1.1: ????J?[?h?E?v???C?????E?J?[?h???
# Rule 9.3.4.2: ?J?[?h?^?C?v???????o?E?????J?[?h?E?\?E
# Rule 9.3.4.3: ?J?[?h?^?C?v?????C?u?????J?[?h?E?\???E?AE
# Rule 9.4:
# Rule 9.4.1:
# Rule 9.4.2:
# Rule 9.4.2.1: ?R?X?g???E????s??????????A?e?L?X?g?E
# Rule 9.4.2.2: ?R?X?g?E??E????????E?S?????x?????????E
# Rule 9.4.3:
# Rule 9.5:
# Rule 9.5.1:
# Rule 9.5.1.1: ?`?F?`E???^?C?~???O????????A??????[????
# Rule 9.5.2:
# Rule 9.5.3:
# Rule 9.5.3.1: ??????E???s????????[?????E??????
# Rule 9.5.3.2: ?v???C???[???E?X?^?[?????E???????
# Rule 9.5.3.3: ??A?N?`E???u?E???C???[???E?X?^?[?????E
# Rule 9.5.3.4: ?`?F?`E???^?C?~???O???I?E??????AE
# Rule 9.5.4:
# Rule 9.5.4.1: ?`?F?`E???^?C?~???O????????????B?`?F?`E???^?C
# Rule 9.5.4.2: ?v???C?^?C?~???O?????????E?v???C???[??
# Rule 9.5.4.3: ?v???C?^?C?~???O??^??????E???C???[??E
# Rule 9.6:
# Rule 9.6.1:
# Rule 9.6.2:
# Rule 9.6.2.1: ?v???C????\????D??J?[?h????E??????
# Rule 9.6.2.1.1: ?v???C???????J?[?h???????A????E
# Rule 9.6.2.1.2: ????E???s??????AE
# Rule 9.6.2.1.2.1: ???E??A????^?[????X?`E?E?W??
# Rule 9.6.2.1.3: ?v???C???????E????????A????
# Rule 9.6.2.2: ?J?[?h??\???????E?I?????E???????
# Rule 9.6.2.3: ?v???C???????R?X?g????????A????R
# Rule 9.6.2.3.1: ?v???C???????????o?E??J?[?h?????
# Rule 9.6.2.3.2: ?????o?E???E???C?????A?x???????E
# Rule 9.6.2.3.2.1: ???????R?X?g???????E?E??
# Rule 9.6.2.4: ?J?[?h??\???E???????s??????AE
# Rule 9.6.2.4.1: ?v???C????????????o?E???????A??
# Rule 9.6.2.4.2: ?v???C????????N???E??????E???
# Rule 9.6.2.4.2.1: ?\???E??????????????o?E?J?[
# Rule 9.6.3:
# Rule 9.6.3.1: ?I??????w??????E?????A??????\
# Rule 9.6.3.1.1: ?I??????f?`???I???f??f?`???I
# Rule 9.6.3.1.2: ?I??????w??????E??????A?w?E
# Rule 9.6.3.1.3: ?I??????w??????E??????A???E
# Rule 9.6.3.1.4: ?I????E???E?J??????E????E?????J??E
# Rule 9.7:
# Rule 9.7.1:
# Rule 9.7.2:
# Rule 9.7.2.1: ?????E???E?U?????????E?????????
# Rule 9.7.3:
# Rule 9.7.3.1: ??E??????E?????E???E?v???C???????A?E
# Rule 9.7.3.1.1: ?????E????C???R?X?g???x?????????
# Rule 9.7.3.2: ?I???E??????E?????E????v???C?????
# Rule 9.7.3.2.1: ?????E????C???R?X?g???x?????????
# Rule 9.7.4:
# Rule 9.7.4.1: ??????U?????????E????A????\?E
# Rule 9.7.4.1.1: ?J?[?h?????J???Y????E?J???A??
# Rule 9.7.4.1.2: ?J?[?h???X?`E?E?W????F???O?E???
# Rule 9.7.4.1.3: ??L?????????O?E?A?E?J???Y??
# Rule 9.7.4.2: ????J?[?h????????U???\????????A??
# Rule 9.7.5:
# Rule 9.7.5.1: ?????U????A?????????????????E????E??
# Rule 9.7.6:
# Rule 9.7.6.1: ???U????A????????????????????1
# Rule 9.7.7:
# Rule 9.8:
# Rule 9.8.1:
# Rule 9.9:
# Rule 9.9.1:
# Rule 9.9.1.1: ?J?[?h?E?g??\?L??????E???E?????A????
# Rule 9.9.1.2: ????A?E???^????E??????E?L???????/
# Rule 9.9.1.3: ????A?p???????E??E???E??????l???X??E
# Rule 9.9.1.4: ????A?p???????E??E???E??????l??????E
# Rule 9.9.1.4.1: ?n?E?g??u???[?h?E?????????E????
# Rule 9.9.1.5: ????A?p???????E??E???E??????l???X??E
# Rule 9.9.1.5.1: ?n?E?g??u???[?h?E??????????Z????E
# Rule 9.9.1.6: ????E9.9.1.2X-9.9.1.4 ??K?p??E?E?O??I??
# Rule 9.9.1.7: ????E9.9.1.2X-9.9.1.6 ??K?p??E?E?O??I??
# Rule 9.9.1.7.1: ?p???????E???????????E??????
# Rule 9.9.1.7.2: ?????O?E?\???E???E?A?????v??
# Rule 9.9.2:
# Rule 9.9.3:
# Rule 9.9.3.1: ?????E?E????????J?[?h????????????E
# Rule 9.10:
# Rule 9.10.1:
# Rule 9.10.1.1: ???????A?u????????E?E???????????
# Rule 9.10.2:
# Rule 9.10.2.1: ?e????????????J?[?h??\???????
# Rule 9.10.2.2: ?e????????????Q?[??????s?????E
# Rule 9.10.2.3: ??????????????A?e?u???????E??
# Rule 9.10.3:
# Rule 9.11:
# Rule 9.11.1:
# Rule 9.12:
# Rule 9.12.1:
# Rule 9.12.2:
# Rule 10:
# Rule 10.1:
# Rule 10.1.1:
# Rule 10.1.2:
# Rule 10.1.3:
# Rule 10.2:
# Rule 10.2.1:
# Rule 10.2.2:
# Rule 10.2.2.1: ??E??????v???C???[????C???`E???L?u??E
# Rule 10.2.2.2: ???C???`E???L?u???????H?????E??????
# Rule 10.2.3:
# Rule 10.2.4:
# Rule 10.3:
# Rule 10.3.1:
# Rule 10.4:
# Rule 10.4.1:
# Rule 10.5:
# Rule 10.5.1:
# Rule 10.5.2:
# Rule 10.5.3:
# Rule 10.5.4:
# Rule 10.6:
# Rule 10.6.1:
# Rule 11:
# Rule 11.1:
# Rule 11.1.1:
# Rule 11.1.2:
# Rule 11.1.3:
# Rule 11.2:
# Rule 11.2.2:
# Rule 11.2.3:
# Rule 11.3:
# Rule 11.3.1: [Icon] ??A?????o?E???????o?E?G???A??u????E
# Rule 11.3.2:
# Rule 11.4:
# Rule 11.4.1: [Icon] ??A???C?u???J?n??????????E
# Rule 11.4.2:
# Rule 11.4.2.1: ?p?t?H?[?}???X?t?F?C?Y???A???v???C???[
# Rule 11.5:
# Rule 11.5.1: [Icon] ??A???C?u???????????????U??
# Rule 11.5.2:
# Rule 11.6:
# Rule 11.6.1: [Icon] ??A?E???E?v???C???????A?E???E?E
# Rule 11.6.2:
# Rule 11.6.3:
# Rule 11.6.4:
# Rule 11.7:
# Rule 11.7.1: [Icon] ??A?E???E?v???C???????A?E???E?E
# Rule 11.7.2:
# Rule 11.7.3:
# Rule 11.7.4:
# Rule 11.8:
# Rule 11.8.1: [Icon] ??A?E???E?v???C???????A?E???E?E
# Rule 11.8.2:
# Rule 11.8.3:
# Rule 11.8.4:
# Rule 11.9:
# Rule 11.9.1:
# Rule 11.9.2:
# Rule 11.10:
# Rule 11.10.1:
# Rule 11.10.2:
# Rule 12:
# Rule 12.1:
# Rule 12.1.1:
# Rule 12.1.1.1: ?A?N?`E???u?E???C???[?E?E.2?E??E?A????z???E
# Rule 12.1.1.2: ?A?N?`E???u?E???C???[???????E?s?????E
# Rule 12.1.1.3: ?????E?E??????A??????E?v???C???[??
# Rule 2025:
# --- END OF INDEX ---