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)