Spaces:
Running
Running
| import json | |
| from engine.game.desc_utils import get_action_desc | |
| from engine.game.state_utils import get_base_id | |
| from engine.models.ability import EFFECT_DESCRIPTIONS, EffectType, TriggerType | |
| def format_effect_description(effect): | |
| # Logic copied/adapted from effect_mixin.py | |
| template = EFFECT_DESCRIPTIONS.get(effect.effect_type, getattr(effect.effect_type, "name", str(effect.effect_type))) | |
| context = effect.params.copy() | |
| context["value"] = effect.value | |
| # Custom overrides if needed (e.g. REDUCE_HEART_REQ) | |
| if effect.effect_type == EffectType.REDUCE_HEART_REQ: | |
| if effect.value < 0: | |
| return f"Reduce Heart Requirement by {abs(effect.value)}" | |
| else: | |
| return f"Increase Heart Requirement by {effect.value}" | |
| try: | |
| desc = template.format(**context) | |
| except KeyError: | |
| desc = template | |
| # Add contextual suffixes | |
| if effect.params.get("per_energy"): | |
| desc += " per Energy" | |
| if effect.params.get("per_member"): | |
| desc += " per Member" | |
| if effect.params.get("per_live"): | |
| desc += " per Live" | |
| return desc | |
| def get_card_modifiers(player, slot_idx, card_id, member_db, live_db): | |
| modifiers = [] | |
| # 0. Player Level Restrictions (Virtual Modifiers) | |
| if player.cannot_live: | |
| modifiers.append({"description": "Cannot perform Live this turn", "source": "Game Rule", "expiry": "LIVE_END"}) | |
| # 1. Continuous Effects | |
| for ce in player.continuous_effects: | |
| target_slot = ce.get("target_slot", -1) | |
| eff = ce["effect"] | |
| # Determine relevance: | |
| # - member on stage (slot_idx >= 0): matches target_slot or target_slot is -1 | |
| # - live card (slot_idx == -1): matches global effects or live-specific effects | |
| is_relevant = False | |
| if slot_idx >= 0: | |
| if target_slot == -1 or target_slot == slot_idx: | |
| is_relevant = True | |
| else: | |
| # For live cards/others, only show global or relevant types | |
| if target_slot == -1 or eff.effect_type in (EffectType.REDUCE_HEART_REQ, EffectType.MODIFY_SCORE_RULE): | |
| is_relevant = True | |
| if is_relevant: | |
| desc = format_effect_description(eff) | |
| # Resolve Source | |
| source_name = "Effect" | |
| sid = ce.get("source_card_id") | |
| if sid is not None: | |
| base_sid = get_base_id(int(sid)) | |
| if base_sid in member_db: | |
| source_name = member_db[base_sid].name | |
| elif base_sid in live_db: | |
| source_name = live_db[base_sid].name | |
| modifiers.append({"description": desc, "source": source_name, "expiry": ce.get("expiry", "TURN_END")}) | |
| # 2. Intrinsic Constant Abilities | |
| base_cid = get_base_id(int(card_id)) | |
| db = member_db if base_cid in member_db else live_db if base_cid in live_db else None | |
| if db and base_cid in db: | |
| card = db[base_cid] | |
| if hasattr(card, "abilities"): | |
| for ab in card.abilities: | |
| if ab.trigger == TriggerType.CONSTANT: | |
| # Check conditions | |
| # Note: _check_condition_for_constant might not be available on card/db, | |
| # but we assume the environment has it or fallback to True for display | |
| # Serializer is often called without full GameState context for individual cards | |
| # but here we have 'player'. | |
| try: | |
| if all(player._check_condition_for_constant(cond, slot_idx) for cond in ab.conditions): | |
| for eff in ab.effects: | |
| desc = format_effect_description(eff) | |
| modifiers.append({"description": desc, "source": "Self", "expiry": "CONSTANT"}) | |
| except (AttributeError, TypeError): | |
| # Fallback: display constant if it exists but condition check is impossible | |
| for eff in ab.effects: | |
| desc = format_effect_description(eff) | |
| modifiers.append({"description": desc, "source": "Self", "expiry": "CONSTANT"}) | |
| return modifiers | |
| def serialize_card(cid, member_db, live_db, energy_db, is_viewable=True, peek=False): | |
| if not is_viewable and not peek: | |
| return {"id": int(cid), "name": "???", "type": "unknown", "img": "cards/back.png", "hidden": True} | |
| card_data = {} | |
| # Try direct lookup, then fallback to base ID for unique IDs | |
| base_cid = get_base_id(int(cid)) | |
| if base_cid in member_db: | |
| m = member_db[base_cid] | |
| ability_text = getattr(m, "ability_text", "") | |
| # Only reconstruct if no rich text is available | |
| if not ability_text and hasattr(m, "abilities") and m.abilities: | |
| ability_lines = [] | |
| for ab in m.abilities: | |
| trigger_icon = { | |
| TriggerType.ACTIVATED: "γθ΅·εγ", | |
| TriggerType.ON_PLAY: "γη»ε ΄γ", | |
| TriggerType.CONSTANT: "γεΈΈζγ", | |
| TriggerType.ON_LIVE_START: "γγ©γ€γιε§γ", | |
| TriggerType.ON_LIVE_SUCCESS: "γγ©γ€γζεζγ", | |
| }.get(ab.trigger, "γθͺεγ") | |
| ability_lines.append(f"{trigger_icon} {ab.raw_text}") | |
| ability_text = "\n".join(ability_lines) | |
| card_data = { | |
| "id": int(cid), # Keep original ID (UID) for frontend tracking | |
| "card_no": m.card_no, | |
| "name": m.name, | |
| "type": "member", | |
| "cost": m.cost, | |
| "blade": m.blades, | |
| "img": m.img_path, | |
| "hearts": m.hearts.tolist() if hasattr(m.hearts, "tolist") else list(m.hearts), | |
| "blade_hearts": m.blade_hearts.tolist() if hasattr(m.blade_hearts, "tolist") else list(m.blade_hearts), | |
| "text": ability_text, | |
| "original_text": getattr(m, "original_text", ""), | |
| } | |
| elif base_cid in live_db: | |
| l = live_db[base_cid] | |
| ability_text = getattr(l, "ability_text", "") | |
| # Only reconstruct if no rich text is available | |
| if not ability_text and hasattr(l, "abilities") and l.abilities: | |
| ability_lines = [] | |
| for ab in l.abilities: | |
| trigger_icon = {TriggerType.ON_LIVE_START: "γγ©γ€γιε§γ"}.get(ab.trigger, "γθͺεγ") | |
| ability_lines.append(f"{trigger_icon} {ab.raw_text}") | |
| ability_text = "\n".join(ability_lines) | |
| card_data = { | |
| "id": int(cid), | |
| "card_no": l.card_no, | |
| "name": l.name, | |
| "type": "live", | |
| "score": l.score, | |
| "img": l.img_path, | |
| "required_hearts": l.required_hearts.tolist() | |
| if hasattr(l.required_hearts, "tolist") | |
| else list(l.required_hearts), | |
| "text": ability_text, | |
| "original_text": getattr(l, "original_text", ""), | |
| } | |
| elif base_cid in energy_db: | |
| e = energy_db[base_cid] | |
| card_data = { | |
| "id": int(cid), | |
| "card_no": getattr(e, "card_no", "ENERGY"), | |
| "name": getattr(e, "name", "Energy"), | |
| "type": "energy", | |
| "img": getattr(e, "img_path", "assets/energy_card.png"), | |
| } | |
| else: | |
| return {"id": int(cid), "name": f"Card {cid}", "type": "unknown", "img": None} | |
| if not is_viewable and peek: | |
| card_data["hidden"] = True | |
| card_data["face_down"] = True | |
| return card_data | |
| def serialize_player(p, game_state, player_idx, viewer_idx=0, is_viewable=True): | |
| member_db = game_state.member_db | |
| live_db = game_state.live_db | |
| energy_db = getattr(game_state, "energy_db", {}) | |
| legal_mask = game_state.get_legal_actions() | |
| expected_yells = 0 | |
| for i, card_id in enumerate(p.stage): | |
| if card_id >= 0 and not p.tapped_members[i]: | |
| base_cid = get_base_id(int(card_id)) | |
| if base_cid in member_db: | |
| expected_yells += member_db[base_cid].blades | |
| hand = [] | |
| for i, cid in enumerate(p.hand): | |
| if is_viewable: | |
| c = serialize_card(cid, member_db, live_db, energy_db) | |
| is_new = False | |
| if hasattr(p, "hand_added_turn") and i < len(p.hand_added_turn): | |
| is_new = p.hand_added_turn[i] == game_state.turn_number | |
| c["is_new"] = is_new | |
| valid_actions = [] | |
| for area in range(3): | |
| aid = 1 + i * 3 + area | |
| if aid < len(legal_mask) and legal_mask[aid]: | |
| valid_actions.append(aid) | |
| for aid in [400 + i, 300 + i, 500 + i]: | |
| if aid < len(legal_mask) and legal_mask[aid]: | |
| valid_actions.append(aid) | |
| c["valid_actions"] = valid_actions | |
| hand.append(c) | |
| else: | |
| hand.append(serialize_card(cid, member_db, live_db, energy_db, is_viewable=False)) | |
| stage = [] | |
| for i, _ in enumerate(range(3)): | |
| # Correctly access stage by index | |
| cid = int(p.stage[i]) | |
| if cid >= 0: | |
| c = serialize_card(cid, member_db, live_db, energy_db) | |
| c["tapped"] = bool(p.tapped_members[i]) | |
| c["energy"] = int(p.stage_energy_count[i]) | |
| c["locked"] = bool(p.members_played_this_turn[i]) | |
| # Add modifiers | |
| c["modifiers"] = get_card_modifiers(p, i, cid, member_db, live_db) | |
| stage.append(c) | |
| else: | |
| stage.append(None) | |
| discard = [serialize_card(cid, member_db, live_db, energy_db, is_viewable=True) for cid in p.discard] | |
| energy = [] | |
| for i, cid in enumerate(p.energy_zone): | |
| energy.append( | |
| { | |
| "id": i, | |
| "tapped": bool(p.tapped_energy[i]), | |
| "card": serialize_card(cid, member_db, live_db, energy_db, is_viewable=False), | |
| } | |
| ) | |
| # Calculate total hearts and blades for the player | |
| total_blades = p.get_total_blades(member_db) | |
| total_hearts = p.get_total_hearts(member_db) # Returns np.array(7) | |
| # Track remaining hearts for live fulfillment calculation (Greedy allocation) | |
| temp_hearts = total_hearts.copy() | |
| live_zone = [] | |
| for i, cid in enumerate(p.live_zone): | |
| is_revealed = bool(p.live_zone_revealed[i]) if i < len(p.live_zone_revealed) else False | |
| card_obj = serialize_card( | |
| cid, member_db, live_db, energy_db, is_viewable=is_revealed, peek=(player_idx == viewer_idx) | |
| ) | |
| # Calculate heart progress for this live card | |
| if cid in live_db: | |
| l = live_db[cid] | |
| req = l.required_hearts # np.array | |
| filled = [0] * 7 | |
| if is_revealed or (player_idx == viewer_idx): | |
| # Greedy fill logic matching PlayerState.get_performance_guide | |
| # Colors 0-5 | |
| for c_idx in range(6): | |
| have = temp_hearts[c_idx] | |
| need = req[c_idx] | |
| take = min(have, need) | |
| filled[c_idx] = int(take) | |
| temp_hearts[c_idx] -= take | |
| # Any Color (Index 6) | |
| req_any = req[6] if len(req) > 6 else 0 | |
| remaining_total = sum(temp_hearts[:6]) + temp_hearts[6] | |
| take_any = min(remaining_total, req_any) | |
| filled[6] = int(take_any) | |
| # Note: We don't subtract from temp_hearts for 'any' because strict color matching is done, | |
| # and 'any' sucks from the pool of remaining. | |
| # But to be strictly correct for *subsequent* cards (if we supported multiple approvals at once), | |
| # we should decrement. But the game usually checks one at a time or order matters. | |
| # Use the logic from get_performance_guide: | |
| # It doesn't actually decrement 'any' from specific colors in temp_hearts | |
| # because 'any' is a wildcard check on the *sum*. | |
| # Wait, get_performance_guide does: | |
| # remaining_total = np.sum(temp_hearts[:6]) + temp_hearts[6] | |
| card_obj["required_hearts"] = req.tolist() | |
| card_obj["filled_hearts"] = filled | |
| # Determine passed status | |
| is_passed = True | |
| for c_idx in range(6): | |
| if filled[c_idx] < req[c_idx]: | |
| is_passed = False | |
| if filled[6] < req[6]: | |
| is_passed = False | |
| card_obj["is_cleared"] = is_passed | |
| # Add modifiers for live card (e.g. requirement reduction) | |
| card_obj["modifiers"] = get_card_modifiers(p, -1, cid, member_db, live_db) | |
| live_zone.append(card_obj) | |
| score = sum(live_db[cid].score for cid in p.success_lives if cid in live_db) | |
| return { | |
| "player_id": p.player_id, | |
| "score": score, | |
| "is_active": (game_state.current_player == player_idx), | |
| "hand": hand, | |
| "hand_count": len(p.hand), | |
| "mulligan_selection": list(p.mulligan_selection) if is_viewable else [], | |
| "deck_count": len(p.main_deck), | |
| "energy_deck_count": len(p.energy_deck), | |
| "discard": discard, | |
| "discard_count": len(p.discard), | |
| "energy": energy, | |
| "energy_count": len(p.energy_zone), | |
| "energy_untapped": int(p.count_untapped_energy()), | |
| "live_zone": live_zone, | |
| "live_zone_count": len(p.live_zone), | |
| "stage": stage, | |
| "success_lives": [serialize_card(cid, member_db, live_db, energy_db, is_viewable) for cid in p.success_lives], | |
| "restrictions": list(p.restrictions), | |
| "expected_yells": expected_yells, | |
| "total_hearts": total_hearts.tolist(), | |
| "total_blades": int(total_blades), | |
| "hand_costs": [p.get_member_cost(cid, member_db) if cid in member_db else 0 for cid in p.hand], | |
| "active_effects": [ | |
| { | |
| "description": format_effect_description(ce["effect"]), | |
| "source": ( | |
| member_db[get_base_id(int(ce["source_card_id"]))].name | |
| if ce.get("source_card_id") is not None and get_base_id(int(ce["source_card_id"])) in member_db | |
| else live_db[get_base_id(int(ce["source_card_id"]))].name | |
| if ce.get("source_card_id") is not None and get_base_id(int(ce["source_card_id"])) in live_db | |
| else "Effect" | |
| ), | |
| "expiry": ce.get("expiry", "TURN_END"), | |
| "source_card_id": int(ce.get("source_card_id", -1)) if ce.get("source_card_id") is not None else -1, | |
| } | |
| for ce in p.continuous_effects | |
| ], | |
| } | |
| def serialize_state(gs, viewer_idx=0, is_pvp=False, mode="pve"): | |
| active_idx = gs.current_player | |
| legal_mask = gs.get_legal_actions() | |
| legal_actions = [] | |
| p = gs.active_player | |
| member_db = gs.member_db | |
| live_db = gs.live_db | |
| energy_db = getattr(gs, "energy_db", {}) | |
| # Only populate legal actions if it is the viewer's turn, or if in PvP/Hotseat mode (show all) | |
| show_actions = is_pvp or (viewer_idx == active_idx) | |
| if show_actions: | |
| for i, v in enumerate(legal_mask): | |
| if v: | |
| desc = get_action_desc(i, gs) | |
| meta = {"id": i, "desc": desc, "name": desc, "description": desc} | |
| # Enrich with metadata for UI | |
| if 1 <= i <= 180: | |
| meta["type"] = "PLAY" | |
| meta["hand_idx"] = (i - 1) // 3 | |
| meta["area_idx"] = (i - 1) % 3 | |
| if meta["hand_idx"] < len(p.hand): | |
| cid = p.hand[meta["hand_idx"]] | |
| c = serialize_card(cid, member_db, live_db, energy_db) | |
| hand_cost = p.get_member_cost(cid, member_db) | |
| # Baton Touch Reduction (Rule 12) | |
| net_cost = hand_cost | |
| if p.stage[meta["area_idx"]] >= 0: | |
| old_cid = get_base_id(int(p.stage[meta["area_idx"]])) | |
| if old_cid in member_db: | |
| net_cost = max(0, hand_cost - member_db[old_cid].cost) | |
| meta.update( | |
| { | |
| "img": c["img"], | |
| "name": c["name"], | |
| "cost": int(net_cost), | |
| "base_cost": int(hand_cost), | |
| "card_no": c.get("card_no", "???"), | |
| "text": c.get("text", ""), | |
| } | |
| ) | |
| if cid in member_db: | |
| meta["triggers"] = [ab.trigger for ab in member_db[cid].abilities] | |
| elif 200 <= i <= 202: | |
| meta["type"] = "ABILITY" | |
| meta["area_idx"] = i - 200 | |
| cid = p.stage[meta["area_idx"]] | |
| if cid >= 0: | |
| c = serialize_card(cid, member_db, live_db, energy_db) | |
| meta.update( | |
| {"img": c["img"], "name": c["name"], "text": c.get("text", ""), "source_card_id": int(cid)} | |
| ) | |
| elif 300 <= i <= 359: | |
| meta["type"] = "MULLIGAN" | |
| meta["hand_idx"] = i - 300 | |
| elif 400 <= i <= 459: | |
| meta["type"] = "LIVE_SET" | |
| meta["hand_idx"] = i - 400 | |
| elif 500 <= i <= 559: | |
| meta["type"] = "SELECT_HAND" | |
| meta["hand_idx"] = i - 500 | |
| target_p_idx = active_idx | |
| if gs.pending_choices: | |
| target_p_idx = gs.pending_choices[0][1].get("player_id", active_idx) | |
| meta["player_id"] = target_p_idx | |
| target_p = gs.players[target_p_idx] | |
| if meta["hand_idx"] < len(target_p.hand): | |
| cid = target_p.hand[meta["hand_idx"]] | |
| c = serialize_card(cid, member_db, live_db, energy_db) | |
| meta.update( | |
| {"img": c["img"], "name": c["name"], "text": c.get("text", ""), "source_card_id": int(cid)} | |
| ) | |
| elif 560 <= i <= 562: | |
| meta["type"] = "SELECT_STAGE" | |
| meta["area_idx"] = i - 560 | |
| target_p_idx = active_idx | |
| if gs.pending_choices: | |
| target_p_idx = gs.pending_choices[0][1].get("player_id", active_idx) | |
| meta["player_id"] = target_p_idx | |
| target_p = gs.players[target_p_idx] | |
| cid = target_p.stage[meta["area_idx"]] | |
| if cid >= 0: | |
| c = serialize_card(cid, member_db, live_db, energy_db) | |
| meta.update( | |
| {"img": c["img"], "name": c["name"], "text": c.get("text", ""), "source_card_id": int(cid)} | |
| ) | |
| elif 590 <= i <= 599: | |
| meta["type"] = "ABILITY_TRIGGER" | |
| meta["index"] = i - 590 | |
| elif 600 <= i <= 659: | |
| meta["type"] = "SELECT" | |
| meta["index"] = i - 600 | |
| if gs.pending_choices: | |
| ctype, cparams = gs.pending_choices[0] | |
| if ctype == "TARGET_OPPONENT_MEMBER": | |
| opp = gs.inactive_player | |
| meta["player_id"] = opp.player_id | |
| if meta["index"] < 3: | |
| cid = opp.stage[meta["index"]] | |
| if cid >= 0: | |
| c = serialize_card(cid, member_db, live_db, energy_db) | |
| meta.update( | |
| { | |
| "img": c["img"], | |
| "name": c["name"], | |
| "text": c.get("text", ""), | |
| "source_card_id": int(cid), | |
| } | |
| ) | |
| elif ctype == "SELECT_FROM_LIST" or ctype == "SELECT_SUCCESS_LIVE": | |
| cards = cparams.get("cards", []) | |
| if meta["index"] < len(cards): | |
| cid = cards[meta["index"]] | |
| c = serialize_card(cid, member_db, live_db, energy_db) | |
| meta.update( | |
| { | |
| "img": c["img"], | |
| "name": c["name"], | |
| "text": c.get("text", ""), | |
| "source_card_id": int(cid), | |
| } | |
| ) | |
| elif 660 <= i <= 719: | |
| meta["type"] = "SELECT_DISCARD" | |
| meta["index"] = i - 660 | |
| if gs.pending_choices: | |
| ctype, cparams = gs.pending_choices[0] | |
| if ctype == "SELECT_FROM_DISCARD": | |
| cards = cparams.get("cards", []) | |
| if meta["index"] < len(cards): | |
| cid = cards[meta["index"]] | |
| c = serialize_card(cid, member_db, live_db, energy_db) | |
| meta.update( | |
| { | |
| "img": c["img"], | |
| "name": c["name"], | |
| "text": c.get("text", ""), | |
| "source_card_id": int(cid), | |
| } | |
| ) | |
| elif 570 <= i <= 579: | |
| meta["type"] = "SELECT_MODE" | |
| meta["index"] = i - 570 | |
| if gs.pending_choices: | |
| ctype, cparams = gs.pending_choices[0] | |
| if ctype == "MODAL" or ctype == "SELECT_MODE": | |
| options = cparams.get("options", []) | |
| if meta["index"] < len(options): | |
| opt = options[meta["index"]] | |
| # Option can be string or list/dict | |
| desc = str(opt) | |
| if isinstance(opt, (list, tuple)) and len(opt) > 0: | |
| desc = str(opt[0]) # Crude fallback | |
| meta["text"] = desc | |
| meta["name"] = desc | |
| elif 580 <= i <= 589: | |
| meta["type"] = "COLOR_SELECT" | |
| meta["index"] = i - 580 | |
| colors = ["Pink", "Red", "Yellow", "Green", "Blue", "Purple", "All", "None"] | |
| if meta["index"] < len(colors): | |
| meta["color"] = colors[meta["index"]] | |
| meta["name"] = colors[meta["index"]] | |
| elif 720 <= i <= 759: | |
| meta["type"] = "SELECT_FORMATION" | |
| meta["index"] = i - 720 | |
| if gs.pending_choices: | |
| ctype, cparams = gs.pending_choices[0] | |
| cards = cparams.get("cards", cparams.get("available_members", [])) | |
| if meta["index"] < len(cards): | |
| cid = cards[meta["index"]] | |
| c = serialize_card(cid, member_db, live_db, energy_db) | |
| meta.update( | |
| { | |
| "img": c["img"], | |
| "name": c["name"], | |
| "text": c.get("text", ""), | |
| "source_card_id": int(cid), | |
| } | |
| ) | |
| elif 760 <= i <= 819: | |
| meta["type"] = "SELECT_SUCCESS_LIVE" | |
| meta["index"] = i - 760 | |
| # Usually points to p.success_lives | |
| target_p_idx = active_idx | |
| if gs.pending_choices: | |
| ctype, cparams = gs.pending_choices[0] | |
| target_p_idx = cparams.get("player_id", active_idx) | |
| target_p = gs.players[target_p_idx] | |
| # If specific cards list provided in params, use that | |
| cards = [] | |
| if gs.pending_choices: | |
| _, cparams = gs.pending_choices[0] | |
| cards = cparams.get("cards", []) | |
| if not cards: | |
| cards = target_p.success_lives | |
| if meta["index"] < len(cards): | |
| cid = cards[meta["index"]] | |
| c = serialize_card(cid, member_db, live_db, energy_db) | |
| meta.update( | |
| { | |
| "img": c["img"], | |
| "name": c["name"], | |
| "text": c.get("text", ""), | |
| "source_card_id": int(cid), | |
| } | |
| ) | |
| elif 820 <= i <= 829: | |
| meta["type"] = "TARGET_LIVE" | |
| meta["index"] = i - 820 | |
| target_p_idx = active_idx | |
| if gs.pending_choices: | |
| _, cparams = gs.pending_choices[0] | |
| target_p_idx = cparams.get("player_id", active_idx) | |
| target_p = gs.players[target_p_idx] | |
| if meta["index"] < len(target_p.live_zone): | |
| cid = target_p.live_zone[meta["index"]] | |
| c = serialize_card(cid, member_db, live_db, energy_db) | |
| meta.update( | |
| { | |
| "img": c["img"], | |
| "name": c["name"], | |
| "text": c.get("text", ""), | |
| "source_card_id": int(cid), | |
| } | |
| ) | |
| elif 830 <= i <= 849: | |
| meta["type"] = "TARGET_ENERGY" | |
| meta["index"] = i - 830 | |
| target_p_idx = active_idx | |
| if gs.pending_choices: | |
| _, cparams = gs.pending_choices[0] | |
| target_p_idx = cparams.get("player_id", active_idx) | |
| target_p = gs.players[target_p_idx] | |
| if meta["index"] < len(target_p.energy_zone): | |
| cid = target_p.energy_zone[meta["index"]] | |
| c = serialize_card(cid, member_db, live_db, energy_db) | |
| meta.update( | |
| { | |
| "img": c["img"], | |
| "name": c["name"], | |
| "text": c.get("text", ""), | |
| "source_card_id": int(cid), | |
| } | |
| ) | |
| elif 850 <= i <= 909: | |
| meta["type"] = "TARGET_REMOVED" | |
| meta["index"] = i - 850 | |
| # Assuming removed_cards is on GameState | |
| removed = getattr(gs, "removed_cards", []) | |
| if meta["index"] < len(removed): | |
| cid = removed[meta["index"]] | |
| c = serialize_card(cid, member_db, live_db, energy_db) | |
| meta.update( | |
| { | |
| "img": c["img"], | |
| "name": c["name"], | |
| "text": c.get("text", ""), | |
| "source_card_id": int(cid), | |
| } | |
| ) | |
| legal_actions.append(meta) | |
| pending_choice = None | |
| if gs.pending_choices: | |
| choice_type, params_raw = gs.pending_choices[0] | |
| try: | |
| params = json.loads(params_raw) if isinstance(params_raw, str) else params_raw | |
| except: | |
| params = {} | |
| # Resolve Source Card Details | |
| source_name = params.get("source_member", "Unknown") | |
| source_img = None | |
| source_id = params.get("source_card_id") | |
| if source_id is not None: | |
| if source_id in member_db: | |
| m = member_db[source_id] | |
| source_name = m.name | |
| source_img = m.img_path | |
| elif source_id in live_db: | |
| l = live_db[source_id] | |
| source_name = l.name | |
| source_img = l.img_path | |
| pending_choice = { | |
| "type": choice_type, | |
| "description": params.get("effect_description", ""), | |
| "source_ability": params.get("source_ability", ""), | |
| "source_member": source_name, | |
| "source_img": source_img, | |
| "source_card_id": int(source_id) if source_id is not None else -1, | |
| "is_optional": params.get("is_optional", False), | |
| "params": params, | |
| } | |
| # FINAL CHECK: Correctly indented return statement | |
| return { | |
| "turn": gs.turn_number, | |
| "phase": int(gs.phase), | |
| "active_player": int(active_idx), | |
| "game_over": gs.game_over, | |
| "winner": gs.winner, | |
| "mode": mode, | |
| "is_pvp": is_pvp, | |
| "players": [ | |
| serialize_player( | |
| gs.players[0], gs, player_idx=0, viewer_idx=viewer_idx, is_viewable=(viewer_idx == 0 or is_pvp) | |
| ), | |
| serialize_player( | |
| gs.players[1], gs, player_idx=1, viewer_idx=viewer_idx, is_viewable=(viewer_idx == 1 or is_pvp) | |
| ), | |
| ], | |
| "legal_actions": legal_actions, | |
| "pending_choice": pending_choice, | |
| "rule_log": gs.rule_log[-200:], # Truncate log for transport | |
| "performance_results": getattr(gs, "performance_results", {}), | |
| "last_performance_results": getattr(gs, "last_performance_results", {}), | |
| "performance_history": getattr(gs, "performance_history", []), | |
| "looked_cards": [ | |
| serialize_card(cid, member_db, live_db, energy_db) | |
| for cid in getattr(gs.get_player(active_idx), "looked_cards", []) | |
| ], | |
| } | |