Spaces:
Running
Running
File size: 11,800 Bytes
bb3fbf9 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 |
import random
from typing import Any, Dict, List, Optional
from engine.game.game_state import GameState
from engine.game.serializer import serialize_state
try:
from engine.game.state_utils import create_uid
except ImportError:
# Fallback if state_utils was deleted (it shouldn't have been, but just in case)
# Reimplement create_uid if needed, or fix the file location
BASE_ID_MASK = 0xFFFFF
INSTANCE_SHIFT = 20
def create_uid(base_id: int, instance_index: int) -> int:
return (base_id & BASE_ID_MASK) | (instance_index << INSTANCE_SHIFT)
def optimize_history(
history: List[Dict[str, Any]],
member_db: Dict[int, Any],
live_db: Dict[int, Any],
energy_db: Dict[int, Any],
exclude_db_cards: bool = True,
seed: Optional[int] = None,
action_log: Optional[List[int]] = None,
deck_info: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Optimize replay history.
Args:
history: List of states
member_db: Database of member cards
live_db: Database of live cards
energy_db: Database of energy cards
exclude_db_cards: Use DB-backed optimization (Level 2)
seed: Random seed (Level 3)
action_log: List of action IDs (Level 3)
deck_info: Dict with 'p0_deck', 'p1_deck', etc. (Level 3)
"""
# Level 3: Action-Based Replay (Max Compression)
if seed is not None and action_log is not None and deck_info is not None:
return {
"level": 3,
"seed": seed,
"decks": deck_info,
"action_log": action_log,
# We don't save 'states' or 'registry' at all!
}
# Level 2: State-Based DB-Backed
registry = {}
def extract_static_data(card_data):
"""Extract static fields that don't change during gameplay."""
if not isinstance(card_data, dict):
return {}
# known static fields
static_fields = [
"name",
"card_no",
"type",
"cost",
"blade",
"img",
"hearts",
"blade_hearts",
"text",
"score",
"required_hearts",
]
return {k: card_data[k] for k in static_fields if k in card_data}
def optimize_object(obj):
"""recursively traverse and optimize payload."""
if isinstance(obj, list):
return [optimize_object(x) for x in obj]
elif isinstance(obj, dict):
# Check if this object looks like a serialized card
if "id" in obj and ("name" in obj or "type" in obj):
cid = obj["id"]
# If it's a known card (positive ID), register it
if isinstance(cid, int) and cid >= 0:
is_in_db = cid in member_db or cid in live_db or cid in energy_db
# Decide whether to add to registry
should_register = False
if not is_in_db:
should_register = True
elif not exclude_db_cards:
should_register = True
if should_register:
if cid not in registry:
registry[cid] = extract_static_data(obj)
# Return ONLY dynamic data + ID reference
dynamic_data = {"id": cid}
static_keys = registry[cid].keys()
for k, v in obj.items():
if k not in static_keys and k != "id":
dynamic_data[k] = optimize_object(v)
return dynamic_data
elif is_in_db:
# IT IS IN DB and we exclude it from registry
# We still strip static data, but we don't save it to file
# effectively assuming "registry[cid]" exists implicitly in DB
# We need to know which keys are static to strip them
# We can use a representative static extraction
static_keys = extract_static_data(obj).keys()
dynamic_data = {"id": cid}
for k, v in obj.items():
if k not in static_keys and k != "id":
dynamic_data[k] = optimize_object(v)
return dynamic_data
# Regular dict recursion
return {k: optimize_object(v) for k, v in obj.items()}
else:
return obj
optimized_states = optimize_object(history)
return {"registry": registry, "states": optimized_states}
def inflate_history(
optimized_data: Dict[str, Any],
member_db: Dict[int, Any],
live_db: Dict[int, Any],
energy_db: Dict[int, Any],
) -> List[Dict[str, Any]]:
"""
Reconstruct full history state from optimized data using server DB.
"""
# Level 3 Inflation (Action Log -> State History)
if optimized_data.get("level") == 3 or "action_log" in optimized_data:
print("Inflating Level 3 Action Log replay...")
action_log = optimized_data.get("action_log", [])
seed = optimized_data.get("seed", 0)
deck_info = optimized_data.get("decks", {})
# 1. Reset Game with Seed
# Use local random instance to avoid messing with global random state if possible,
# but GameState uses random module globally.
# We must save and restore random state if we want to be clean, but python random is global.
# Ideally GameState should use a random instance.
# For now, we assume the caller handles global state implications or we just reset seed.
# NOTE: This modifies global random state!
random.seed(seed)
# 2. Init Game State (Headless)
GameState.member_db = member_db
GameState.live_db = live_db
# Energy DB is not static on GameState?
GameState.energy_db = energy_db # server.py sets this on instance or class?
# server.py says: GameState.energy_db = energy_db
# Create fresh state
temp_gs = GameState()
temp_gs.initialize_game()
# Set decks if available
if deck_info:
p0_deck = deck_info.get("p0_deck")
p1_deck = deck_info.get("p1_deck")
if p0_deck and len(p0_deck) > 0:
print(f"Loading custom deck for P0: {len(p0_deck)} cards")
p0 = temp_gs.players[0]
# Reset Deck & Hand
p0.main_deck = [int(x) for x in p0_deck]
p0.hand = []
p0.discard = []
# Draw initial hand (5 cards)
draw_count = min(5, len(p0.main_deck))
p0.hand = p0.main_deck[:draw_count]
p0.hand_added_turn = [1] * draw_count
p0.main_deck = p0.main_deck[draw_count:]
if p1_deck and len(p1_deck) > 0:
print(f"Loading custom deck for P1: {len(p1_deck)} cards")
p1 = temp_gs.players[1]
p1.main_deck = [int(x) for x in p1_deck]
p1.hand = []
p1.discard = []
draw_count = min(5, len(p1.main_deck))
p1.hand = p1.main_deck[:draw_count]
p1.hand_added_turn = [1] * draw_count
p1.main_deck = p1.main_deck[draw_count:]
reconstructed_history = []
# 3. Serialize Initial State
reconstructed_history.append(serialize_state(temp_gs))
# 4. Replay Actions
for action_id in action_log:
temp_gs.step(action_id)
reconstructed_history.append(serialize_state(temp_gs))
print(f"Reconstructed {len(reconstructed_history)} frames from {len(action_log)} actions.")
return reconstructed_history
# Level 2 Logic (State Inflation)
registry = optimized_data.get("registry", {})
states = optimized_data.get("states", [])
def get_static_data(cid):
"""Get static data from Registry OR Database"""
# 1. Registry (Custom cards / Legacy format)
if str(cid) in registry:
return registry[str(cid)]
if cid in registry:
return registry[cid]
# 2. Database
if cid in member_db:
m = member_db[cid]
# Reconstruct dictionary from object
ability_text = getattr(m, "ability_text", "")
if hasattr(m, "abilities") and m.abilities:
# Use raw Japanese text
# Clean wiki markup: {{icon.png|Text}} -> Text, [[Link|Text]] -> Text
import re
def clean_text(text):
text = re.sub(r"\{\{.*?\|(.*?)\}\}", r"\1", text) # {{icon|Text}} -> Text
text = re.sub(r"\[\[.*?\|(.*?)\]\]", r"\1", text) # [[Link|Text]] -> Text
return text
ability_lines = [clean_text(ab.raw_text) for ab in m.abilities]
ability_text = "\n".join(ability_lines)
return {
"name": m.name,
"card_no": m.card_no,
"type": "member",
"cost": m.cost,
"blade": m.blades,
"img": m.img_path,
"hearts": m.hearts.tolist(),
"blade_hearts": m.blade_hearts.tolist(),
"text": ability_text,
"color": "Unknown",
}
elif cid in live_db:
l = live_db[cid]
ability_text = getattr(l, "ability_text", "")
if hasattr(l, "abilities") and l.abilities:
import re
def clean_text(text):
text = re.sub(r"\{\{.*?\|(.*?)\}\}", r"\1", text)
text = re.sub(r"\[\[.*?\|(.*?)\]\]", r"\1", text)
return text
ability_lines = [clean_text(ab.raw_text) for ab in l.abilities]
ability_text = "\n".join(ability_lines)
return {
"name": l.name,
"card_no": l.card_no,
"type": "live",
"score": l.score,
"img": l.img_path,
"required_hearts": l.required_hearts.tolist(),
"text": ability_text,
}
elif cid in energy_db:
# EnergyCard is simple (just ID), so we hardcode display info
return {"name": "Energy", "type": "energy", "img": "assets/energy_card.png"}
return None
def inflate_object(obj):
if isinstance(obj, list):
return [inflate_object(x) for x in obj]
elif isinstance(obj, dict):
# Check for ID reference to inflate
if "id" in obj:
cid = obj["id"]
static_data = get_static_data(cid)
if static_data:
# Merge static data into this object (dynamic overrides static if conflict, though shouldn't happen)
# We create a new dict to Avoid mutating the source if it's reused
new_obj = static_data.copy()
for k, v in obj.items():
new_obj[k] = inflate_object(v)
return new_obj
return {k: inflate_object(v) for k, v in obj.items()}
else:
return obj
return inflate_object(states)
|