File size: 18,636 Bytes
463f868
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
from __future__ import annotations

import copy
import random
from typing import Any, Dict

import numpy as np

from engine.game.effects.choices import queue_select_from_list_choice
from engine.game.effects.hand import discard_hand_cards
from engine.models.ability import ConditionType, Effect, EffectType, TargetType
from engine.models.enums import Group, Unit
from engine.models.opcodes import Opcode


def _decode_real_value(p: Any, v: int, a: int, s_packed: int) -> int:
    """Resolve bytecode scaling/dynamic-count semantics into a concrete value."""
    real_v = v
    if (a & 0x40) == 0:
        return real_v

    cond_type = ConditionType(v)
    if cond_type == ConditionType.COUNT_STAGE:
        raw_count = len([c for c in p.stage if c >= 0])
    elif cond_type == ConditionType.COUNT_HAND:
        raw_count = len(p.hand)
    elif cond_type == ConditionType.COUNT_DISCARD:
        raw_count = len(p.discard)
    elif cond_type == ConditionType.COUNT_ENERGY:
        raw_count = len(p.energy_zone)
    elif cond_type == ConditionType.COUNT_SUCCESS_LIVE:
        raw_count = len(p.success_lives)
    elif cond_type == ConditionType.COUNT_LIVE_ZONE:
        raw_count = len(p.live_zone)
    else:
        raw_count = 0

    if (a & 0x20) != 0:
        scaling_factor = s_packed >> 4
        if scaling_factor > 0:
            return (s_packed & 0x0F) * (raw_count // scaling_factor)
        return 0
    return raw_count


def _append_deck_top_to_discard(game: Any, player: Any, count: int) -> None:
    for _ in range(count):
        if not player.main_deck:
            game._resolve_deck_refresh(player)
        if player.main_deck:
            player.discard.append(player.main_deck.pop(0))


def _append_energy_to_discard(player: Any, count: int) -> None:
    for _ in range(count):
        if player.energy_zone:
            player.discard.append(player.energy_zone.pop())


def resolve_effect_opcode(game: Any, opcode: Opcode, seg: Any, context: Dict[str, Any]) -> None:
    self = game
    """Execute a single quadruple from bytecode."""
    p = self.active_player
    opp_idx = 1 - p.player_id
    v = seg[1]
    a = seg[2]
    s_packed = seg[3]

    real_v = _decode_real_value(p, v, a, s_packed)
    s = s_packed & 0x0F

    # Condition Check Opcodes (Return/Jump logic usually handled in loop, but we need 'cond' state)
    # In this Python version, _resolve_pending_effect's loop needs to handle cond.
    # But for now, we only trigger effects.

    if self.verbose:
        print(f"DEBUG_OP: {opcode.name} v={v} a={a} s={s}")

    if opcode == Opcode.DRAW:
        self._draw_cards(p, real_v)
    elif opcode == Opcode.ADD_BLADES:
        p.continuous_effects.append(
            {
                "source_card_id": context.get("source_card_id", context.get("card_id", -1)),
                "effect": Effect(EffectType.ADD_BLADES, real_v, TargetType.SELF),
                "target_slot": s if s < 3 else -1,
                "expiry": "TURN_END",
            }
        )
    elif opcode == Opcode.ADD_HEARTS:
        p.continuous_effects.append(
            {
                "source_card_id": context.get("source_card_id", context.get("card_id", -1)),
                "effect": Effect(EffectType.ADD_HEARTS, real_v, TargetType.SELF, {"color": a & 0x3F}),
                "expiry": "TURN_END",
            }
        )
    elif opcode == Opcode.GRANT_ABILITY:
        # Granting an ability (Rule 1.3.1)
        # a: attribute of granted ability (trigger type)
        # v: Index into member_db if external, or reference to current
        # For Sumire: It's usually a predefined ID or a special payload.
        # In V2 compiler, GRANT_ABILITY often carries a whole Ability object or pre-compiled bytecode.
        # But here we handle the "Legacy" or "Simple" case for Starters.

        # If it's "SELF" (s == 0), we apply to current member.
        source_id = context.get("source_card_id", -1)
        target_p = p

        # In Sumire's case, the pseudocode is:
        # EFFECT: GRANT_ABILITY(SELF, TRIGGER="CONSTANT", CONDITION="IS_ON_STAGE", EFFECT="BOOST_SCORE(1)")
        # This is compiled into Opcode 60.

        # For simplicity in this engine version, we convert it to a Continuous Effect
        # that mimics the granted ability.
        target_slot = context.get("area", -1)
        if target_slot == -1 and source_id != -1:
            # Find where source_id is on stage
            for i, cid in enumerate(p.stage):
                if cid == source_id:
                    target_slot = i
                    break

        if target_slot != -1:
            # Create a pseudo-effect that matches the granted ability
            # Sumire grants BOOST_SCORE(1) while on stage.
            # In basic engine terms, this is just a Score Buff continuous effect.
            p.continuous_effects.append(
                {
                    "source_card_id": source_id,
                    "effect": Effect(EffectType.BOOST_SCORE, real_v, TargetType.SELF),
                    "target_slot": target_slot,
                    "expiry": "TURN_END",  # Usually until end of turn or permanent?
                    # Starter Sumire is "This turn".
                }
            )

    elif opcode == Opcode.LOOK_DECK:
        # Reveal top V cards (Target awareness)
        target_tp = self.players[opp_idx] if s == 2 else p  # s=2 is OPPONENT in TargetType
        if self.verbose:
            print(f"DEBUG: LOOK_DECK Target={target_tp.player_id} DeckLen={len(target_tp.main_deck)}")
        if not target_tp.main_deck:
            self._resolve_deck_refresh(target_tp)
        self.looked_cards = []
        for _ in range(v):
            if target_tp.main_deck:
                self.looked_cards.append(target_tp.main_deck.pop(0))
        if self.verbose:
            print(f"DEBUG: LOOK_DECK Result={self.looked_cards}")
    elif opcode == Opcode.LOOK_AND_CHOOSE:
        # Trigger SLIST choice
        if self.looked_cards:
            dest = "discard" if (a & 0x0F) == 1 else "hand"
            target_pid = opp_idx if s == 2 else p.player_id
            queue_select_from_list_choice(
                self,
                {
                    "player_id": p.player_id,
                    "source_card_id": context.get("source_card_id", context.get("card_id", -1)),
                },
                self.looked_cards,
                real_v,
                "look_and_choose",
                "Choose cards from looked list",
                target_pid,
                extra_params={"destination": dest},
            )

    elif opcode == Opcode.TAP_OPPONENT:
        is_all = (a & 0x80) != 0
        requires_selection = (a & 0x20) != 0  # Bit 5 flag
        cost_max = v
        blades_max = a & 0x1F  # Mask out flags

        # Dynamic Resolution: If cost_max is 99, try to find context card cost
        if cost_max == 99:
            scid = context.get("source_card_id", context.get("card_id", -1))
            if scid != -1 and scid in self.member_db:
                cost_max = self.member_db[scid].cost

        def passes_filter(slot_id):
            cid = opp.stage[slot_id]
            if cid < 0:
                return False
            card = self.member_db[int(cid)]
            # Cost check
            if cost_max != 99 and card.cost > cost_max:
                return False
            # Blades check
            if blades_max != 99 and card.total_blades > blades_max:
                return False
            return True

        if is_all:
            for i in range(3):
                if passes_filter(i):
                    opp.tapped_members[i] = True
                    opp.members_tapped_by_opponent_this_turn.add(opp.stage[i])
        else:
            # Check for interactive selection
            if requires_selection or (s == 2 and v == 1):  # s=2 is OPPONENT, v=1 count
                choice = context.get("choice_index", -1)
                if choice == -1:
                    # Pause for selection
                    # Create a pending choice
                    # Valid targets
                    valid_slots = [i for i in range(3) if passes_filter(i) and not opp.tapped_members[i]]

                    if valid_slots:
                        self.pending_choices.append(
                            (
                                "TARGET_OPPONENT_MEMBER",
                                {
                                    "player_id": self.current_player,
                                    "valid_slots": valid_slots,
                                    "effect": "tap_opponent",
                                    "pending_context": context,  # Store context to resume
                                    # "resume_opcode": opcode, ... (Python engine handles resume differently usually)
                                    # Using standard pending_choices format
                                },
                            )
                        )
                        # In Python engine, pending_choices usually halts execution flow implicitly or explicitly?
                        # resolve_bytecode doesn't return status.
                        # We might need to ensure this halts.
                        # But standard python engine relies on pending_choices check in outer loop.
                        pass
                else:
                    # Apply to chosen slot
                    if 0 <= choice < 3 and passes_filter(choice):
                        opp.tapped_members[choice] = True
                        opp.members_tapped_by_opponent_this_turn.add(opp.stage[choice])
            elif s < 3:  # Fallback for directed target (if s is actually a fixed slot index)
                # Note: s=2 (Right Slot) vs s=2 (Opponent TargetType) is ambiguous without flags.
                # This is why we added the flag.
                # If flag is NOT set, and s < 3, assume fixed slot?
                # But previously s=2 tapped right slot.
                if passes_filter(s):
                    opp.tapped_members[s] = True
                    opp.members_tapped_by_opponent_this_turn.add(opp.stage[s])
    elif opcode == Opcode.ACTIVATE_MEMBER:
        if s < 3:
            p.tapped_members[s] = False
    elif opcode == Opcode.REVEAL_CARDS:
        # s=1 means reveal looking cards (usually for yel animation/trigger)
        pass
    elif opcode == Opcode.RECOVER_LIVE:
        # Trigger interactive selection instead of broken automatic recovery
        live_cards_in_discard = [cid for cid in p.discard if int(cid) in self.live_db]
        if live_cards_in_discard:
            self.pending_choices.append(
                (
                    "SELECT_FROM_DISCARD",
                    {
                        "source_card_id": context.get("source_card_id", -1),
                        "cards": live_cards_in_discard,
                        "count": real_v,
                        "filter": "live",
                        "effect": "return_to_hand",
                        "effect_description": "蝗槫庶縺吶k繝ゥ繧、繝悶r驕ク繧薙〒縺上□縺輔>",
                    },
                )
            )
    elif opcode == Opcode.RECOVER_MEMBER:
        # Trigger interactive selection instead of broken automatic recovery
        member_cards_in_discard = [cid for cid in p.discard if int(cid) in self.member_db]
        if member_cards_in_discard:
            self.pending_choices.append(
                (
                    "SELECT_FROM_DISCARD",
                    {
                        "source_card_id": context.get("source_card_id", -1),
                        "cards": member_cards_in_discard,
                        "count": real_v,
                        "filter": "member",
                        "effect": "return_to_hand",
                        "effect_description": "蝗槫庶縺吶k繝。繝ウ繝舌・繧帝∈繧薙〒縺上□縺輔>",
                    },
                )
            )
    elif opcode == Opcode.SWAP_CARDS:
        # Discard v, then draw v
        for _ in range(v):
            if p.hand:
                cid = p.hand.pop(0)
                p.hand_added_turn.pop(0)
                p.discard.append(cid)
        self._draw_cards(p, v)
    elif opcode == Opcode.REDUCE_COST:
        p.continuous_effects.append(
            {
                "source_card_id": context.get("source_card_id", context.get("card_id", -1)),
                "effect": Effect(EffectType.REDUCE_COST, v, TargetType.SELF),
                "target_slot": s if s < 3 else -1,
                "expiry": "TURN_END",
            }
        )
    elif opcode == Opcode.REDUCE_HEART_REQ:
        p.continuous_effects.append(
            {
                "source_card_id": context.get("source_card_id", context.get("card_id", -1)),
                "effect": Effect(EffectType.REDUCE_HEART_REQ, v, TargetType.SELF),
                "expiry": "LIVE_END" if context.get("until") == "live_end" else "TURN_END",
            }
        )
    elif opcode == Opcode.BATON_TOUCH_MOD:
        p.continuous_effects.append(
            {
                "source_card_id": context.get("source_card_id", context.get("card_id", -1)),
                "effect": Effect(EffectType.BATON_TOUCH_MOD, v, TargetType.SELF),
                "expiry": "TURN_END",
            }
        )
    elif opcode == Opcode.META_RULE:
        # Handle specific meta rules that have engine impact (cheer_mod, etc)
        rule_type = context.get("type", "")
        if rule_type == "cheer_mod":
            p.continuous_effects.append(
                {
                    "source_card_id": context.get("source_card_id", context.get("card_id", -1)),
                    "effect": Effect(EffectType.META_RULE, v, TargetType.SELF, {"type": "cheer_mod"}),
                    "expiry": "LIVE_END" if context.get("until") == "live_end" else "TURN_END",
                }
            )
        elif rule_type == "fragment_cleanup":
            pass  # Already handled by parser filter
        else:
            p.meta_rules.add(str(rule_type))
    elif opcode == Opcode.TRANSFORM_COLOR:
        target = context.get("target", "base_hearts")
        p.continuous_effects.append(
            {
                "source_card_id": context.get("source_card_id", context.get("card_id", -1)),
                "effect": Effect(EffectType.TRANSFORM_COLOR, v, TargetType.SELF, {"target": target, "color": a}),
                "expiry": "LIVE_END" if context.get("until") == "live_end" else "TURN_END",
            }
        )
    elif opcode == Opcode.PLAY_MEMBER_FROM_HAND:
        # v = count, a = source_attr (e.g. Group)
        # From hand is standard, but if context says discard:
        source_zone = context.get("from", "hand")
        self.pending_choices.append(
            (
                "TARGET_MEMBER_SLOT",
                {
                    **context,
                    "player_id": p.player_id,
                    "effect": "place_member",
                    "source_zone": source_zone,
                    "count": v,
                    "filter_group": a if a != 0 else None,
                },
            )
        )
    elif opcode == Opcode.FLAVOR_ACTION:
        # Trigger modal choice "What do you like?"
        self.pending_choices.append(
            (
                "MODAL_CHOICE",
                {
                    **context,
                    "player_id": opp_idx,
                    "title": "What do you like?",
                    "options": [
                        "Choco Mint",
                        "Strawberry Flavor",
                        "Cookies & Cream",
                        "You",
                        "Anything else",
                    ],
                    "reason": "flavor_action",
                },
            )
        )
    elif opcode == Opcode.ADD_TO_HAND:
        # v = count, a = source (0=looked, 1=discard, 2=deck)
        if a == 0 and self.looked_cards:
            for _ in range(v):
                if self.looked_cards:
                    cid = self.looked_cards.pop(0)
                    p.hand.append(cid)
                    p.hand_added_turn.append(self.turn_number)
        elif a == 1:
            # Handled by RECOVER_LIVE/MEMBER usually, but for generic:
            for _ in range(v):
                if p.discard:
                    cid = p.discard.pop()
                    p.hand.append(cid)
                    p.hand_added_turn.append(self.turn_number)
        elif a == 2:
            self._draw_cards(p, v)
    elif opcode == Opcode.BOOST_SCORE:
        p.live_score_bonus += v
    elif opcode == Opcode.ENERGY_CHARGE:
        count = v
        for _ in range(count):
            if p.main_deck:
                cid = p.main_deck.pop(0)
                p.energy_zone.append(cid)
                p.tapped_energy[len(p.energy_zone) - 1] = False
            elif p.discard:
                self._resolve_deck_refresh(p)
                if p.main_deck:
                    cid = p.main_deck.pop(0)
                    p.energy_zone.append(cid)
                    p.tapped_energy[len(p.energy_zone) - 1] = False
    elif opcode == Opcode.MOVE_MEMBER:
        dest = context.get("target_slot", -1)
        if 0 <= s < 3 and 0 <= dest < 3:
            self._move_member(p, s, dest)
    elif opcode == Opcode.MOVE_TO_DISCARD:
        # v = count, a = source (1=deck_top, 2=hand, 3=energy), s = target_slot
        if a == 1:  # From Deck Top
            _append_deck_top_to_discard(self, p, v)
        elif a == 2:  # From Hand
            discard_hand_cards(p, v)
        elif a == 3:  # From Energy
            _append_energy_to_discard(p, v)
        elif s == 0:  # Target SELF (Member on stage)
            scid = context.get("source_card_id", context.get("card_id", -1))
            for i in range(3):
                if p.stage[i] == scid:
                    p.stage[i] = -1
                    p.tapped_members[i] = False
                    p.discard.append(scid)
                    break
    elif opcode == Opcode.JUMP_IF_FALSE:
        # This requires a more complex loop in _resolve_pending_effect
        # For now, standard step() might not use jumps heavily in Python mode
        pass


__all__ = ["resolve_effect_opcode"]