Spaces:
Running
Running
| import warnings | |
| import numpy as np | |
| from numba import njit, prange | |
| # Suppress Numba cache warnings (harmless in testing environments) | |
| warnings.filterwarnings("ignore", message=".*'cache' is set for njit and is ignored.*") | |
| # ============================================================================= | |
| # HYPER-OPTIMIZED VM CORE (Production Version) | |
| # ============================================================================= | |
| # ContextIndex Mappings (Raw Ints) | |
| CV = 20 | |
| AT = 22 | |
| TS = 12 | |
| TI = 13 | |
| SZ = 7 | |
| CH = 15 | |
| SID = 5 | |
| # Backward compatibility aliases | |
| CTX_VALUE = CV | |
| CTX_ATTR = AT | |
| CTX_TARGET_SLOT = TS | |
| CTX_TARGET_PLAYER_ID = TI | |
| CTX_SOURCE_ZONE_IDX = SZ | |
| CTX_CHOICE_INDEX = CH | |
| # GlobalContext Mappings | |
| SC = 0 | |
| OS = 1 | |
| TR = 2 | |
| HD = 3 | |
| DI = 4 | |
| EN = 5 | |
| DK = 6 | |
| OT = 7 | |
| OT = 7 | |
| PH = 8 | |
| LS = 110 # Live Score Bonus (Temporary, moved to 110 to avoid conflict with OD) | |
| EH = 111 # Excess Hearts (Current Live Performance) | |
| PREV_CID_IDX = 60 # Previous Card ID for Baton Pass (Index 60) | |
| # Unique ID (UID) System | |
| BASE_ID_MASK = 0xFFFFF | |
| def get_base_id(uid: int) -> int: | |
| """Extract the base card definition ID (0-1999) from a UID.""" | |
| return uid & BASE_ID_MASK | |
| # Opcodes | |
| O_DRAW = 10 | |
| O_BLADES = 11 | |
| O_HEARTS = 12 | |
| O_REDUCE_COST = 13 | |
| O_LOOK_DECK = 14 | |
| O_RECOV_L = 15 | |
| O_BOOST = 16 | |
| O_RECOV_M = 17 | |
| O_BUFF = 18 | |
| O_MOVE_MEMBER = 20 | |
| O_SWAP_CARDS = 21 | |
| O_SEARCH_DECK = 22 | |
| O_CHARGE = 23 | |
| O_ORDER_DECK = 28 | |
| O_SET_BLADES = 24 | |
| O_SELECT_MODE = 30 | |
| O_TAP_O = 32 | |
| O_PLACE_UNDER = 33 | |
| O_LOOK_AND_CHOOSE = 41 | |
| O_ACTIVATE_MEMBER = 43 | |
| O_ADD_H = 44 | |
| O_REPLACE_EFFECT = 46 | |
| O_TRIGGER_REMOTE = 47 | |
| O_TRANSFORM_COLOR = 39 | |
| O_TAP_M = 53 | |
| O_REDUCE_HEART_REQ = 48 | |
| O_REVEAL_CARDS = 26 | |
| O_MOVE_TO_DISCARD = 58 | |
| O_RETURN = 1 | |
| O_JUMP = 2 | |
| O_JUMP_F = 3 | |
| O_REVEAL_HAND_ALL = 6 # Cost Type (Internal reminder) | |
| # Conditions | |
| C_TR1 = 200 | |
| C_CLR = 202 | |
| C_STG = 203 | |
| C_HND = 204 | |
| C_CTR = 206 | |
| C_LLD = 207 | |
| C_GRP = 208 | |
| C_OPH = 210 | |
| C_ENR = 213 | |
| C_OPH = 210 | |
| C_ENR = 213 | |
| C_CMP = 220 | |
| C_BLD = 224 | |
| C_HRT = 223 | |
| C_HAS_CHOICE = 221 | |
| C_OPPONENT_CHOICE = 222 | |
| C_BATON = 231 # Baton Pass condition (ID 231) | |
| # Conditions | |
| C_TR1 = 200 | |
| C_CLR = 202 | |
| C_STG = 203 | |
| C_HND = 204 | |
| C_CTR = 206 | |
| C_LLD = 207 | |
| C_GRP = 208 | |
| C_OPH = 210 | |
| C_ENR = 213 | |
| C_OPH = 210 | |
| C_ENR = 213 | |
| C_CMP = 220 | |
| C_BLD = 224 | |
| C_HRT = 223 | |
| C_HAS_CHOICE = 221 | |
| C_OPPONENT_CHOICE = 222 | |
| def resolve_bytecode( | |
| bytecode, | |
| flat_ctx, | |
| global_ctx, | |
| player_id, | |
| p_hand, | |
| p_deck, | |
| p_stage, | |
| p_energy_vec, | |
| p_energy_count, | |
| p_cont_vec, | |
| out_cptr, # Modified: Pass by ref array (size 1) | |
| p_tapped, | |
| p_live, | |
| opp_tapped, | |
| p_trash, # Added | |
| b_map, | |
| b_idx, | |
| out_bonus, # Modified: Pass by ref array (size 1) | |
| card_stats, # Added: NumPy array for card properties | |
| opp_stage, # Added | |
| opp_tapped_count=None, # Optional tracking | |
| ): | |
| # Manual Stack Unrolling (Depth 4) - Eliminates allocations in hot loop | |
| stack_ptr = 0 | |
| bc0 = bytecode | |
| ip0 = 0 | |
| bc1 = bytecode | |
| ip1 = 0 | |
| bc2 = bytecode | |
| ip2 = 0 | |
| bc3 = bytecode | |
| ip3 = 0 | |
| cptr = out_cptr[0] | |
| cond = True | |
| safety_counter = 0 | |
| while stack_ptr >= 0 and safety_counter < 1000: | |
| safety_counter += 1 | |
| # Current Frame Selection (Type-stable for Numba) | |
| if stack_ptr == 0: | |
| cur_bc = bc0 | |
| ip = ip0 | |
| elif stack_ptr == 1: | |
| cur_bc = bc1 | |
| ip = ip1 | |
| elif stack_ptr == 2: | |
| cur_bc = bc2 | |
| ip = ip2 | |
| else: | |
| cur_bc = bc3 | |
| ip = ip3 | |
| blen = cur_bc.shape[0] | |
| if ip >= blen: | |
| stack_ptr -= 1 | |
| continue | |
| op = cur_bc[ip, 0] | |
| v = cur_bc[ip, 1] | |
| a = cur_bc[ip, 2] | |
| s = cur_bc[ip, 3] | |
| # Advance IP logic: Pre-increment local copy | |
| next_ip = ip + 1 | |
| if stack_ptr == 0: | |
| ip0 = next_ip | |
| elif stack_ptr == 1: | |
| ip1 = next_ip | |
| elif stack_ptr == 2: | |
| ip2 = next_ip | |
| else: | |
| ip3 = next_ip | |
| if op == 0: | |
| continue | |
| if op == O_RETURN: | |
| stack_ptr -= 1 | |
| continue | |
| # Negation Flag Handling | |
| is_negated = False | |
| if op >= 1000: | |
| is_negated = True | |
| op -= 1000 | |
| # Dynamic Target Handling (MEMBER_SELECT) | |
| if s == 10: | |
| s = int(flat_ctx[TS]) | |
| if op == O_JUMP: | |
| new_ip = ip + v | |
| if stack_ptr == 0: | |
| ip0 = new_ip if 0 <= new_ip < blen else blen | |
| elif stack_ptr == 1: | |
| ip1 = new_ip if 0 <= new_ip < blen else blen | |
| elif stack_ptr == 2: | |
| ip2 = new_ip if 0 <= new_ip < blen else blen | |
| else: | |
| ip3 = new_ip if 0 <= new_ip < blen else blen | |
| continue | |
| if op == O_JUMP_F: | |
| if not cond: | |
| new_ip = ip + v | |
| if stack_ptr == 0: | |
| ip0 = new_ip if 0 <= new_ip < blen else blen | |
| elif stack_ptr == 1: | |
| ip1 = new_ip if 0 <= new_ip < blen else blen | |
| elif stack_ptr == 2: | |
| ip2 = new_ip if 0 <= new_ip < blen else blen | |
| else: | |
| ip3 = new_ip if 0 <= new_ip < blen else blen | |
| continue | |
| continue | |
| if op == O_SELECT_MODE: | |
| choice = int(flat_ctx[CH]) | |
| jumped = False | |
| if 0 <= choice < v: | |
| jump_ip = ip + 1 + choice | |
| if jump_ip < blen: | |
| offset = cur_bc[jump_ip, 1] | |
| new_ip = jump_ip + offset | |
| if 0 <= new_ip < blen: | |
| if stack_ptr == 0: | |
| ip0 = new_ip | |
| elif stack_ptr == 1: | |
| ip1 = new_ip | |
| elif stack_ptr == 2: | |
| ip2 = new_ip | |
| else: | |
| ip3 = new_ip | |
| jumped = True | |
| if not jumped: | |
| new_ip = ip + (v + 1) | |
| if stack_ptr == 0: | |
| ip0 = new_ip | |
| elif stack_ptr == 1: | |
| ip1 = new_ip | |
| elif stack_ptr == 2: | |
| ip2 = new_ip | |
| else: | |
| ip3 = new_ip | |
| continue | |
| if op >= 200: | |
| if op == C_TR1: | |
| cond = global_ctx[TR] == 1 | |
| elif op == C_STG: | |
| ct = 0 | |
| for i in range(3): | |
| if p_stage[i] != -1: | |
| ct += 1 | |
| cond = ct >= v | |
| elif op == C_HND: | |
| cond = global_ctx[HD] >= v | |
| elif op == C_LLD: | |
| cond = global_ctx[SC] > global_ctx[OS] | |
| elif op == C_CLR: | |
| if 0 <= a <= 5: | |
| cond = global_ctx[10 + a] > 0 | |
| else: | |
| cond = False | |
| elif op == C_GRP: | |
| # C_GRP: Count cards of a group on stage or live zone | |
| # v = min count, a = group ID, s = zone (0=stage, 1=live) | |
| target_group = a | |
| grp_count = 0 | |
| if s == 0: # Stage | |
| for slot_k in range(3): | |
| cid = p_stage[slot_k] | |
| if cid >= 0: | |
| bid = get_base_id(cid) | |
| if bid < card_stats.shape[0]: | |
| # Groups are stored at index 6 (primary group) | |
| if card_stats[bid, 6] == target_group: | |
| grp_count += 1 | |
| elif s == 1: # Live Zone | |
| for lz_k in range(p_live.shape[0]): | |
| cid = p_live[lz_k] | |
| if cid >= 0: | |
| bid = get_base_id(cid) | |
| if bid < card_stats.shape[0]: | |
| if card_stats[bid, 6] == target_group: | |
| grp_count += 1 | |
| cond = grp_count >= v | |
| elif op == C_BLD: | |
| # C_BLD: Check if total blades on stage meets requirement | |
| # Rule 9.9: Tapped (Resting) members do not contribute. | |
| total_blades_check = 0 | |
| for slot_k in range(3): | |
| cid = p_stage[slot_k] | |
| if cid >= 0 and p_tapped[slot_k] == 0: | |
| bid = get_base_id(cid) | |
| if bid < card_stats.shape[0]: | |
| total_blades_check += card_stats[bid, 1] # Blades at index 1 | |
| # Decode slot/comparison | |
| real_slot = s & 0x0F | |
| comp = (s >> 4) & 0x0F | |
| if comp == 0: | |
| cond = total_blades_check >= v | |
| elif comp == 1: | |
| cond = total_blades_check <= v | |
| elif comp == 2: | |
| cond = total_blades_check > v | |
| elif comp == 3: | |
| cond = total_blades_check < v | |
| else: | |
| cond = total_blades_check == v | |
| elif op == C_HRT: | |
| # C_HRT: Check if total hearts of color `a` on stage meets requirement | |
| total_hearts_check = 0 | |
| # Decode slot/comparison | |
| real_slot = s & 0x0F | |
| comp = (s >> 4) & 0x0F | |
| if real_slot == 2: # Live Result / Excess Hearts | |
| total_hearts_check = global_ctx[EH] | |
| else: | |
| for slot_k in range(3): | |
| cid = p_stage[slot_k] | |
| # Rule 9.9: Tapped members do not contribute hearts either. | |
| if cid >= 0 and p_tapped[slot_k] == 0: | |
| bid = get_base_id(cid) | |
| if bid < card_stats.shape[0]: | |
| if 0 <= a <= 6: | |
| total_hearts_check += card_stats[bid, 12 + a] # Hearts at 12-18 | |
| else: | |
| # Sum all hearts | |
| for h_k in range(7): | |
| total_hearts_check += card_stats[bid, 12 + h_k] | |
| if comp == 0: | |
| cond = total_hearts_check >= v | |
| elif comp == 1: | |
| cond = total_hearts_check <= v | |
| elif comp == 2: | |
| cond = total_hearts_check > v | |
| elif comp == 3: | |
| cond = total_hearts_check < v | |
| else: | |
| cond = total_hearts_check == v | |
| elif op == C_ENR: | |
| cond = global_ctx[EN] >= v | |
| elif op == C_CTR: | |
| cond = flat_ctx[SZ] == 1 | |
| elif op == C_CMP: | |
| if v > 0: | |
| cond = global_ctx[SC] >= v | |
| else: | |
| cond = global_ctx[SC] > global_ctx[OS] | |
| elif op == C_OPH: | |
| ct = global_ctx[OT] | |
| if v > 0: | |
| cond = ct >= v | |
| else: | |
| cond = ct > 0 | |
| elif op == 230: # C_LIVE_ZONE | |
| # Placeholder: correctly count cards in successful live zone if needed | |
| cond = True | |
| elif op == 212: # C_MODAL_ANSWER | |
| cond = global_ctx[CH] == v | |
| elif op == C_HAS_CHOICE: | |
| cond = True | |
| elif op == C_OPPONENT_CHOICE: | |
| # Check if opponent tapped ANY card this turn (safe approximation) | |
| # We check the passed `opp_tapped` array. | |
| cond = False | |
| for k in range(3): | |
| if opp_tapped[k] > 0: | |
| cond = True | |
| break | |
| elif op == C_BATON: | |
| # Baton Pass condition: Check if the previous card's Character ID matches target | |
| prev_cid = global_ctx[PREV_CID_IDX] | |
| if prev_cid >= 0: | |
| prev_bid = get_base_id(prev_cid) | |
| if prev_bid < card_stats.shape[0]: | |
| # Check if the previous card's Character ID (stored in stats index 19) matches the target | |
| cond = card_stats[prev_bid, 19] == v | |
| else: | |
| cond = False | |
| else: | |
| cond = False | |
| if is_negated: | |
| cond = not cond | |
| if not cond: | |
| stack_ptr -= 1 | |
| continue | |
| else: | |
| if cond: | |
| if op == O_DRAW: | |
| drawn = 0 | |
| for d_idx in range(60): | |
| if p_deck[d_idx] > 0: | |
| card_id = p_deck[d_idx] | |
| p_deck[d_idx] = 0 | |
| global_ctx[DK] -= 1 | |
| for h_idx in range(60): | |
| if p_hand[h_idx] == 0: | |
| p_hand[h_idx] = card_id | |
| global_ctx[HD] += 1 | |
| break | |
| drawn += 1 | |
| if drawn >= v: | |
| break | |
| elif op == O_TRANSFORM_COLOR: | |
| # Rule 11.12: Transformation effects. | |
| # Add to continuous effects buffer. | |
| # Entry: [type, value, attr, slot, p_id, duration, ...] | |
| # and TRANSFORM_COLOR usually lasts until LIVE_END (duration=2 in this implementation?) | |
| # attr is target_color index | |
| if ( | |
| cptr < p_cont_vec.shape[0] | |
| ): # Changed from p_ptr[0] to cptr, and p_cont_vec.shape[1] to p_cont_vec.shape[0] | |
| idx = cptr | |
| p_cont_vec[idx, 0] = O_TRANSFORM_COLOR | |
| p_cont_vec[idx, 1] = v | |
| p_cont_vec[idx, 2] = a | |
| p_cont_vec[idx, 3] = s | |
| p_cont_vec[idx, 4] = player_id | |
| p_cont_vec[idx, 5] = 2 # Duration: LIVE_END | |
| cptr += 1 # Changed from p_ptr[0] += 1 to cptr += 1 | |
| elif op == O_CHARGE: | |
| charged = 0 | |
| for d_idx in range(60): | |
| if p_deck[d_idx] > 0: | |
| card_id = p_deck[d_idx] | |
| p_deck[d_idx] = 0 | |
| global_ctx[DK] -= 1 | |
| if 0 <= s < 3: | |
| for e_idx in range(32): | |
| if p_energy_vec[s, e_idx] == 0: | |
| p_energy_vec[s, e_idx] = card_id | |
| p_energy_count[s] += 1 | |
| # Rule 10.6: Energy under members does NOT count as global energy | |
| # global_ctx[EN] += 1 <-- REMOVED | |
| break | |
| charged += 1 | |
| if charged >= v: | |
| break | |
| elif op == O_BLADES: | |
| if s >= 0 and cptr < 32: | |
| p_cont_vec[cptr, 0] = 1 | |
| p_cont_vec[cptr, 1] = v | |
| p_cont_vec[cptr, 2] = 4 | |
| p_cont_vec[cptr, 3] = s | |
| p_cont_vec[cptr, 8] = int(flat_ctx[SID]) | |
| p_cont_vec[cptr, 9] = 1 | |
| cptr += 1 | |
| elif op == O_HEARTS: | |
| if cptr < 32: | |
| p_cont_vec[cptr, 0] = 2 | |
| p_cont_vec[cptr, 1] = v | |
| p_cont_vec[cptr, 5] = a | |
| p_cont_vec[cptr, 8] = int(flat_ctx[SID]) | |
| p_cont_vec[cptr, 9] = 1 | |
| cptr += 1 | |
| if 10 + a < 128: | |
| global_ctx[10 + a] += v | |
| elif op == O_REDUCE_COST: | |
| if cptr < 32: | |
| p_cont_vec[cptr, 0] = 3 | |
| p_cont_vec[cptr, 1] = v | |
| p_cont_vec[cptr, 2] = s | |
| p_cont_vec[cptr, 8] = int(flat_ctx[SID]) | |
| p_cont_vec[cptr, 9] = 1 | |
| cptr += 1 | |
| elif op == O_REDUCE_HEART_REQ: | |
| if cptr < 32: | |
| p_cont_vec[cptr, 0] = 48 | |
| p_cont_vec[cptr, 1] = v | |
| p_cont_vec[cptr, 2] = s | |
| p_cont_vec[cptr, 8] = int(flat_ctx[SID]) | |
| p_cont_vec[cptr, 9] = 1 | |
| cptr += 1 | |
| elif op == O_SET_BLADES: | |
| if s >= 0 and cptr < 32: | |
| p_cont_vec[cptr, 0] = 24 | |
| p_cont_vec[cptr, 1] = v | |
| p_cont_vec[cptr, 2] = s | |
| p_cont_vec[cptr, 8] = int(flat_ctx[SID]) | |
| p_cont_vec[cptr, 9] = 1 | |
| cptr += 1 | |
| elif op == O_REPLACE_EFFECT: | |
| if cptr < 32: | |
| p_cont_vec[cptr, 0] = 46 | |
| p_cont_vec[cptr, 1] = v | |
| p_cont_vec[cptr, 2] = s | |
| p_cont_vec[cptr, 9] = 1 | |
| cptr += 1 | |
| elif op == O_LOOK_DECK: | |
| # Look deck in VM just processes the look (Revealing cards to a buffer) | |
| # For now, we skip if it's purely for selection (handled by 41) | |
| # Implementation: No-op for state, but placeholder for side-effects | |
| continue | |
| elif op == O_REVEAL_CARDS: | |
| # Reveal cards (placeholder for state-based effects) | |
| continue | |
| elif op == O_RECOV_L: | |
| # Heuristic: Recover highest ID Live Card | |
| best_idx = -1 | |
| best_id = -1 | |
| for tr_k in range(p_trash.shape[0]): | |
| tid = p_trash[tr_k] | |
| if tid > 0: | |
| tbid = get_base_id(tid) | |
| if tbid < card_stats.shape[0] and card_stats[tbid, 10] == 2: # Live | |
| if tid > best_id: | |
| best_id = tid | |
| best_idx = tr_k | |
| if best_idx != -1: | |
| # Move to hand | |
| moved = False | |
| for h_idx in range(60): | |
| if p_hand[h_idx] == 0: | |
| p_hand[h_idx] = best_id | |
| global_ctx[HD] += 1 | |
| p_trash[best_idx] = 0 | |
| global_ctx[4] -= 1 # TR | |
| moved = True | |
| break | |
| if not moved: | |
| # Hand full: Fallback to discard or stay in trash (which it already is) | |
| continue | |
| elif op == O_RECOV_M: | |
| # Heuristic: Recover highest ID Member Card | |
| best_idx = -1 | |
| best_id = -1 | |
| for tr_k in range(p_trash.shape[0]): | |
| tid = p_trash[tr_k] | |
| if tid > 0: | |
| tbid = get_base_id(tid) | |
| if tbid < card_stats.shape[0] and card_stats[tbid, 10] == 1: # Member | |
| # Optional: Add Cost/Power check if needed, for now Max ID is decent proxy | |
| if tid > best_id: | |
| best_id = tid | |
| best_idx = tr_k | |
| if best_idx != -1: | |
| # Move to hand | |
| moved = False | |
| for h_idx in range(60): | |
| if p_hand[h_idx] == 0: | |
| p_hand[h_idx] = best_id | |
| global_ctx[HD] += 1 | |
| p_trash[best_idx] = 0 | |
| global_ctx[4] -= 1 # TR | |
| moved = True | |
| break | |
| elif op == O_ACTIVATE_MEMBER: | |
| if 0 <= s < 3: | |
| p_tapped[s] = 0 | |
| elif op == O_SWAP_CARDS: | |
| removed = 0 | |
| for h_idx in range(60): | |
| if p_hand[h_idx] > 0: | |
| cid = p_hand[h_idx] | |
| p_hand[h_idx] = 0 | |
| global_ctx[HD] -= 1 | |
| removed += 1 | |
| if removed >= v: | |
| break | |
| if s == 0: # Only draw if mode is 0 (Swap). s=1 implies Discard Only cost. | |
| drawn = 0 | |
| for d_idx in range(60): | |
| if p_deck[d_idx] > 0: | |
| card_id = p_deck[d_idx] | |
| p_deck[d_idx] = 0 | |
| for h_idx in range(60): | |
| if p_hand[h_idx] == 0: | |
| p_hand[h_idx] = card_id | |
| break | |
| global_ctx[DK] -= 1 | |
| global_ctx[HD] += 1 | |
| drawn += 1 | |
| if drawn >= v: | |
| break | |
| elif op == O_PLACE_UNDER: | |
| placed = 0 | |
| if a == 1: # From Energy Zone | |
| for _ in range(v): | |
| if global_ctx[EN] > 0: | |
| global_ctx[EN] -= 1 | |
| if 0 <= s < 3: | |
| for e_idx in range(32): | |
| if p_energy_vec[s, e_idx] == 0: | |
| p_energy_vec[s, e_idx] = 2000 # Dummy energy card | |
| p_energy_count[s] += 1 | |
| break | |
| placed += 1 | |
| if placed >= v: | |
| break | |
| else: # From Hand (Default) | |
| for h_idx in range(59, -1, -1): | |
| if p_hand[h_idx] > 0: | |
| cid = p_hand[h_idx] | |
| p_hand[h_idx] = 0 | |
| global_ctx[HD] -= 1 | |
| if 0 <= s < 3: | |
| for e_idx in range(32): | |
| if p_energy_vec[s, e_idx] == 0: | |
| p_energy_vec[s, e_idx] = cid | |
| p_energy_count[s] += 1 | |
| break | |
| placed += 1 | |
| if placed >= v: | |
| break | |
| elif op == O_MOVE_MEMBER: | |
| dest_slot = int(flat_ctx[TS]) | |
| if 0 <= s < 3 and 0 <= dest_slot < 3 and s != dest_slot: | |
| temp_id = p_stage[s] | |
| p_stage[s] = p_stage[dest_slot] | |
| p_stage[dest_slot] = temp_id | |
| temp_tap = p_tapped[s] | |
| p_tapped[s] = p_tapped[dest_slot] | |
| p_tapped[dest_slot] = temp_tap | |
| for e_idx in range(32): | |
| temp_e = p_energy_vec[s, e_idx] | |
| p_energy_vec[s, e_idx] = p_energy_vec[dest_slot, e_idx] | |
| p_energy_vec[dest_slot, e_idx] = temp_e | |
| temp_ec = p_energy_count[s] | |
| p_energy_count[s] = p_energy_count[dest_slot] | |
| p_energy_count[dest_slot] = temp_ec | |
| elif op == O_TAP_M: | |
| # Tap self or other member (usually based on TargetSlot/s) | |
| if 0 <= s < 3: | |
| p_tapped[s] = True | |
| elif ( | |
| s == 10 | |
| ): # TargetSlot 10 usually means select manually, but in Numba we might just tap all if instructed | |
| p_tapped[:] = True | |
| elif op == O_TAP_O: | |
| is_all = (a & 0x80) != 0 | |
| c_max = v | |
| b_max = a & 0x7F | |
| # Decode real slot | |
| real_slot = s & 0x0F | |
| for slot_k in range(3): | |
| if not is_all and slot_k != real_slot: | |
| continue | |
| cid = opp_stage[slot_k] | |
| if cid >= 0: | |
| bid = get_base_id(cid) | |
| if bid < card_stats.shape[0]: | |
| # Filter checks | |
| if c_max != 99 and card_stats[bid, 0] > c_max: | |
| continue | |
| if b_max != 99 and card_stats[bid, 1] > b_max: | |
| continue | |
| opp_tapped[slot_k] = 1 | |
| elif op == O_BUFF: | |
| if cptr < 32: | |
| p_cont_vec[cptr, 0] = 8 | |
| p_cont_vec[cptr, 1] = v | |
| p_cont_vec[cptr, 2] = s | |
| p_cont_vec[cptr, 8] = int(flat_ctx[SID]) | |
| p_cont_vec[cptr, 9] = 1 | |
| cptr += 1 | |
| elif op == O_BOOST: | |
| # out_bonus[0] += v # WRONG: This was adding to Permanent Score | |
| global_ctx[LS] += v # CORRECT: Add to Temporary Live Score Context | |
| elif op == O_LOOK_AND_CHOOSE: | |
| choice_idx = int(flat_ctx[CH]) | |
| if choice_idx < 0 or choice_idx >= v: | |
| choice_idx = 0 | |
| indices = np.full(v, -1, dtype=np.int32) | |
| ptr = 0 | |
| for d_idx in range(60): | |
| if p_deck[d_idx] > 0: | |
| indices[ptr] = d_idx | |
| ptr += 1 | |
| if ptr >= v: | |
| break | |
| if ptr > 0: | |
| if choice_idx >= ptr: | |
| choice_idx = 0 | |
| real_idx = indices[choice_idx] | |
| chosen_card = -1 | |
| if real_idx != -1: | |
| chosen_card = p_deck[real_idx] | |
| if chosen_card > 0: | |
| for h_idx in range(60): | |
| if p_hand[h_idx] == 0: | |
| p_hand[h_idx] = chosen_card | |
| global_ctx[HD] += 1 | |
| break | |
| for k in range(ptr): | |
| rid = indices[k] | |
| if rid != -1: | |
| cid = p_deck[rid] | |
| p_deck[rid] = 0 | |
| global_ctx[DK] -= 1 | |
| if rid != real_idx: # Fix: Don't mill the card added to hand | |
| move_to_trash(0, cid, p_trash.reshape(1, -1), global_ctx.reshape(1, -1), 2) | |
| elif op == O_ORDER_DECK: | |
| indices = np.full(v, -1, dtype=np.int32) | |
| vals = np.full(v, 0, dtype=np.int32) | |
| ptr = 0 | |
| for d_idx in range(60): | |
| if p_deck[d_idx] > 0: | |
| indices[ptr] = d_idx | |
| vals[ptr] = p_deck[d_idx] | |
| ptr += 1 | |
| if ptr >= v: | |
| break | |
| if ptr > 1: | |
| for k in range(ptr // 2): | |
| temp = vals[k] | |
| vals[k] = vals[ptr - 1 - k] | |
| vals[ptr - 1 - k] = temp | |
| for k in range(ptr): | |
| p_deck[indices[k]] = vals[k] | |
| elif op == O_ADD_H: | |
| drawn = 0 | |
| for d_idx in range(60): | |
| if p_deck[d_idx] > 0: | |
| card_id = p_deck[d_idx] | |
| p_deck[d_idx] = 0 | |
| global_ctx[DK] -= 1 | |
| for h_idx in range(60): | |
| if p_hand[h_idx] == 0: | |
| p_hand[h_idx] = card_id | |
| global_ctx[HD] += 1 | |
| break | |
| drawn += 1 | |
| if drawn >= v: | |
| break | |
| elif op == O_SEARCH_DECK: | |
| target_idx = int(flat_ctx[TS]) | |
| if 0 <= target_idx < 60 and p_deck[target_idx] > 0: | |
| card_to_move = p_deck[target_idx] | |
| p_deck[target_idx] = 0 | |
| for h_idx in range(60): | |
| if p_hand[h_idx] == 0: | |
| p_hand[h_idx] = card_to_move | |
| global_ctx[HD] += 1 | |
| global_ctx[DK] -= 1 | |
| break | |
| else: | |
| for d_idx in range(60): | |
| if p_deck[d_idx] > 0: | |
| card_to_move = p_deck[d_idx] | |
| p_deck[d_idx] = 0 | |
| for h_idx in range(60): | |
| if p_hand[h_idx] == 0: | |
| p_hand[h_idx] = card_to_move | |
| global_ctx[HD] += 1 | |
| global_ctx[DK] -= 1 | |
| break | |
| break | |
| elif op == O_MOVE_TO_DISCARD: | |
| # v=count, a=source(1=deck,2=hand,3=energy), s=target(0=self) | |
| if a == 1: # Deck | |
| moved = 0 | |
| for d_idx in range(60): | |
| if p_deck[d_idx] > 0: | |
| cid = p_deck[d_idx] | |
| p_deck[d_idx] = 0 | |
| global_ctx[DK] -= 1 | |
| # Add to trash inline | |
| for tr_k in range(60): | |
| if p_trash[tr_k] == 0: | |
| p_trash[tr_k] = cid | |
| global_ctx[DI] += 1 | |
| break | |
| moved += 1 | |
| if moved >= v: | |
| break | |
| elif a == 2: # Hand | |
| moved = 0 | |
| for h_idx in range(59, -1, -1): | |
| if p_hand[h_idx] > 0: | |
| cid = p_hand[h_idx] | |
| p_hand[h_idx] = 0 | |
| global_ctx[HD] -= 1 | |
| for tr_k in range(60): | |
| if p_trash[tr_k] == 0: | |
| p_trash[tr_k] = cid | |
| global_ctx[DI] += 1 | |
| break | |
| moved += 1 | |
| if moved >= v: | |
| break | |
| # a=3 Energy not fully supported in fast_logic yet (requires attached energy iter) | |
| elif s == 0: # Self (Stage) | |
| scid = int(flat_ctx[SID]) | |
| for s_k in range(3): | |
| if p_stage[s_k] == scid: | |
| p_stage[s_k] = -1 | |
| p_tapped[s_k] = 0 | |
| for tr_k in range(60): | |
| if p_trash[tr_k] == 0: | |
| p_trash[tr_k] = scid | |
| global_ctx[DI] += 1 | |
| break | |
| break | |
| elif op == O_TRIGGER_REMOTE: | |
| target_slot = s | |
| target_card_id = -1 | |
| if 0 <= target_slot < 3: | |
| target_card_id = p_stage[target_slot] | |
| if target_card_id >= 0: | |
| target_bid = get_base_id(target_card_id) | |
| if target_bid < b_idx.shape[0]: | |
| map_idx = b_idx[target_bid, 0] | |
| if map_idx >= 0 and stack_ptr < 3: | |
| stack_ptr += 1 | |
| if stack_ptr == 1: | |
| bc1 = b_map[map_idx] | |
| ip1 = 0 | |
| elif stack_ptr == 2: | |
| bc2 = b_map[map_idx] | |
| ip2 = 0 | |
| else: | |
| bc3 = b_map[map_idx] | |
| ip3 = 0 | |
| continue | |
| out_cptr[0] = cptr | |
| def batch_resolve_bytecode( | |
| batch_bytecode, | |
| batch_flat_ctx, | |
| batch_global_ctx, | |
| player_id, | |
| p_hand, | |
| p_deck, | |
| p_stage, | |
| p_energy_vec, | |
| p_energy_count, | |
| p_cont_vec, | |
| p_cont_ptr, | |
| p_tapped, | |
| p_live, | |
| opp_tapped, | |
| p_trash, | |
| b_map, | |
| b_idx, | |
| card_stats, # Added: NumPy array for card properties | |
| opp_stage, # Added | |
| ): | |
| num_envs = batch_bytecode.shape[0] | |
| # Pre-allocate bonus array to avoid allocations in prange | |
| batch_bonus = np.zeros(num_envs, dtype=np.int32) | |
| for i in prange(num_envs): | |
| cptr_slice = p_cont_ptr[i : i + 1] | |
| out_bonus_slice = batch_bonus[i : i + 1] | |
| resolve_bytecode( | |
| batch_bytecode[i], | |
| batch_flat_ctx[i], | |
| batch_global_ctx[i], | |
| player_id, | |
| p_hand[i], | |
| p_deck[i], | |
| p_stage[i], | |
| p_energy_vec[i], | |
| p_energy_count[i], | |
| p_cont_vec[i], | |
| cptr_slice, | |
| p_tapped[i], | |
| p_live[i], | |
| opp_tapped[i], | |
| p_trash[i], | |
| b_map, | |
| b_idx, | |
| out_bonus_slice, | |
| card_stats, | |
| opp_stage[i], # Pass opponent stage | |
| ) | |
| def copy_state(s_stg, s_ev, s_ec, s_cv, d_stg, d_ev, d_ec, d_cv): | |
| d_stg[:] = s_stg[:] | |
| d_ev[:] = s_ev[:] | |
| d_ec[:] = s_ec[:] | |
| d_cv[:] = s_cv[:] | |
| def move_to_trash(i, card_id, batch_trash, batch_global_ctx, TR_idx): | |
| if card_id <= 0: | |
| return | |
| for k in range(batch_trash.shape[1]): | |
| if batch_trash[i, k] == 0: | |
| batch_trash[i, k] = card_id | |
| batch_global_ctx[i, TR_idx] += 1 | |
| break | |
| def resolve_live_single( | |
| i, | |
| live_id, | |
| batch_stage, | |
| batch_live, | |
| batch_scores, | |
| batch_global_ctx, | |
| batch_deck, | |
| batch_hand, | |
| batch_trash, | |
| card_stats, | |
| p_cont_vec, | |
| p_cont_ptr, | |
| b_map, | |
| b_idx, | |
| p_tapped, | |
| ): | |
| # Rule 8.3.4: Non-Live cards are discarded before performance | |
| live_bid = get_base_id(live_id) | |
| if live_bid >= card_stats.shape[0] or card_stats[live_bid, 10] != 2: | |
| # Find and remove from live zone | |
| for j in range(batch_live.shape[1]): | |
| if batch_live[i, j] == live_id: | |
| move_to_trash(i, live_id, batch_trash, batch_global_ctx, 2) | |
| batch_live[i, j] = 0 | |
| break | |
| return 0 | |
| # 1. Verify availability in Live Zone | |
| live_idx = -1 | |
| for j in range(batch_live.shape[1]): | |
| if batch_live[i, j] == live_id: | |
| live_idx = j | |
| break | |
| if live_idx == -1: | |
| return 0 | |
| batch_global_ctx[i, LS] = 0 # Ensure reset score bonus | |
| batch_global_ctx[i, EH] = 0 # Reset excess hearts | |
| # 1. Trigger ON_LIVE_START (TriggerType = 2) | |
| for ab_idx in range(4): | |
| trigger_off = 20 if ab_idx == 0 else (20 + 16 + (ab_idx - 1) * 12) | |
| if card_stats[live_bid, trigger_off] == 2: # ON_LIVE_START | |
| map_idx = b_idx[live_bid, ab_idx] | |
| if map_idx >= 0: | |
| # Execution context | |
| flat_ctx = np.zeros(64, dtype=np.int32) | |
| dummy_opp_tapped = np.zeros(3, dtype=np.int32) | |
| out_bonus = np.zeros(1, dtype=np.int32) | |
| p_tapped_dummy = np.zeros(16, dtype=np.int32) | |
| resolve_bytecode( | |
| b_map[map_idx], | |
| flat_ctx, | |
| batch_global_ctx[i], | |
| 0, | |
| batch_hand[i], | |
| batch_deck[i], | |
| batch_stage[i], | |
| np.zeros((3, 32), dtype=np.int32), | |
| np.zeros(3, dtype=np.int32), | |
| p_cont_vec[i], | |
| p_cont_ptr[i : i + 1], | |
| p_tapped, | |
| batch_live[i], | |
| dummy_opp_tapped, | |
| batch_trash[i], | |
| b_map, | |
| b_idx, | |
| out_bonus, | |
| card_stats, | |
| batch_stage[i], | |
| ) | |
| # 2. Check Requirements | |
| # Get Live Stats (indices 12-18 for requirements) | |
| req_pink = card_stats[live_bid, 12] | |
| req_red = card_stats[live_bid, 13] | |
| req_yel = card_stats[live_bid, 14] | |
| req_grn = card_stats[live_bid, 15] | |
| req_blu = card_stats[live_bid, 16] | |
| req_pur = card_stats[live_bid, 17] | |
| # Sum Stage Stats | |
| stage_hearts = np.zeros(7, dtype=np.int32) | |
| stage_all = 0 | |
| total_blades = 0 | |
| for slot in range(3): | |
| # Rule 9.9: Resting members (tapped) do not contribute to performance. | |
| cid = batch_stage[i, slot] | |
| if cid >= 0 and p_tapped[slot] == 0: | |
| bid = get_base_id(cid) | |
| if bid < card_stats.shape[0]: | |
| slot_blades = card_stats[bid, 1] | |
| slot_hearts = np.zeros(7, dtype=np.int32) | |
| for cidx in range(6): | |
| slot_hearts[cidx] = card_stats[bid, 12 + cidx] | |
| slot_all = card_stats[bid, 18] | |
| # Apply continuous effects | |
| for ce_idx in range(p_cont_ptr[i]): | |
| op = p_cont_vec[i, ce_idx, 0] | |
| val = p_cont_vec[i, ce_idx, 1] | |
| target_slot = p_cont_vec[i, ce_idx, 2] | |
| active = p_cont_vec[i, ce_idx, 9] | |
| if active == 1 and (target_slot == -1 or target_slot == slot): | |
| if op == 1 or op == 8: # BLADES / BUFF | |
| slot_blades += val | |
| elif op == 24: # SET_BLADES | |
| slot_blades = val | |
| elif op == 2: # HEARTS | |
| a_color = p_cont_vec[i, ce_idx, 5] | |
| if 0 <= a_color <= 5: | |
| slot_hearts[a_color] += val | |
| elif a_color == 6: # Any/Rainbow | |
| slot_all += val | |
| total_blades += max(0, slot_blades) | |
| for cidx in range(6): | |
| stage_hearts[cidx] += slot_hearts[cidx] | |
| stage_all += slot_all | |
| # Apply Heart Requirement Reductions | |
| for ce_idx in range(p_cont_ptr[i]): | |
| op = p_cont_vec[i, ce_idx, 0] | |
| val = p_cont_vec[i, ce_idx, 1] | |
| active = p_cont_vec[i, ce_idx, 9] | |
| if active == 1 and op == 48: # REDUCE_HEART_REQ | |
| target_color = p_cont_vec[i, ce_idx, 2] # 0-5 or 6 (Any) | |
| if target_color == 0: | |
| req_pink = max(0, req_pink - val) | |
| elif target_color == 1: | |
| req_red = max(0, req_red - val) | |
| elif target_color == 2: | |
| req_yel = max(0, req_yel - val) | |
| elif target_color == 3: | |
| req_grn = max(0, req_grn - val) | |
| elif target_color == 4: | |
| req_blu = max(0, req_blu - val) | |
| elif target_color == 5: | |
| req_pur = max(0, req_pur - val) | |
| elif target_color == 6: # Reduction for 'Any' requirement | |
| # In this simplified model, card_stats[live_bid, 18] might be 'Any' | |
| # but it's not being checked yet. Let's assume for now | |
| # that we don't have a specific index for 'Any' req in card_stats. | |
| # Actually, let's look for where 'any' might be. | |
| pass | |
| # --- RULE ACCURACY: Yells (Rule 8.3) --- | |
| # Draw 'total_blades' cards from deck, apply their blade_hearts and volume/draw icons. | |
| volume_bonus = 0 | |
| draw_bonus = 0 | |
| yells_processed = 0 | |
| # Use a while loop to handle potential deck refreshes | |
| while yells_processed < total_blades: | |
| # Check if we need to refresh (deck empty/exhausted) | |
| # Scan for next valid card | |
| found_card = False | |
| deck_len = batch_deck.shape[1] | |
| d_idx = -1 | |
| for k in range(deck_len): | |
| if batch_deck[i, k] > 0: | |
| d_idx = k | |
| found_card = True | |
| break | |
| if not found_card: | |
| # Deck is empty, trigger refresh logic | |
| check_deck_refresh(i, batch_deck, batch_trash, batch_global_ctx, 6, 2) | |
| # Try to find again | |
| for k in range(deck_len): | |
| if batch_deck[i, k] > 0: | |
| d_idx = k | |
| found_card = True | |
| break | |
| # If still no card (Deck + Trash were empty), we stop yelling. | |
| if not found_card: | |
| break | |
| # Process the found card | |
| if d_idx >= 0: | |
| yid = batch_deck[i, d_idx] | |
| if yid > 0 and yid < card_stats.shape[0]: | |
| # Extract blade_hearts [40:47] | |
| bh = np.zeros(7, dtype=np.int32) | |
| for cidx in range(7): | |
| bh[cidx] = card_stats[yid, 40 + cidx] | |
| # Apply TRANSFORM_COLOR (Rule 11.12) | |
| for ce_idx in range(p_cont_ptr[i]): | |
| if p_cont_vec[i, ce_idx, 0] == O_TRANSFORM_COLOR: | |
| target_idx = p_cont_vec[i, ce_idx, 2] | |
| if 0 <= target_idx <= 5: | |
| total_affected = 0 | |
| # Transform Pink, Red, Yellow, Green, Blue, (and All if applicable) | |
| # Rule for Dazzling Game: 0,1,2,3,4,6 -> target_idx | |
| for src_idx in [0, 1, 2, 3, 4, 6]: | |
| total_affected += bh[src_idx] | |
| bh[src_idx] = 0 | |
| bh[target_idx] += total_affected | |
| # Add to totals | |
| for cidx in range(7): | |
| stage_hearts[cidx] += bh[cidx] | |
| # Bonus Icons | |
| volume_bonus += card_stats[yid, 4] | |
| draw_bonus += card_stats[yid, 5] | |
| # Discard (Remove from deck) | |
| batch_deck[i, d_idx] = 0 | |
| if batch_global_ctx[i, 6] > 0: | |
| batch_global_ctx[i, 6] -= 1 # DK | |
| move_to_trash(i, yid, batch_trash, batch_global_ctx, 2) | |
| yells_processed += 1 | |
| else: | |
| # Invalid card ID or padding | |
| batch_deck[i, d_idx] = 0 | |
| yells_processed += 1 | |
| else: | |
| break | |
| # Apply Draw Bonus | |
| cards_drawn = 0 | |
| while cards_drawn < draw_bonus: | |
| found_card = False | |
| d_idx = -1 | |
| deck_len = batch_deck.shape[1] | |
| for k in range(deck_len): | |
| if batch_deck[i, k] > 0: | |
| d_idx = k | |
| found_card = True | |
| break | |
| if not found_card: | |
| check_deck_refresh(i, batch_deck, batch_trash, batch_global_ctx, 6, 2) | |
| for k in range(deck_len): | |
| if batch_deck[i, k] > 0: | |
| d_idx = k | |
| found_card = True | |
| break | |
| if not found_card: | |
| break | |
| if found_card and d_idx != -1: | |
| # Draw the card | |
| top_c = batch_deck[i, d_idx] | |
| # Move to Hand | |
| placed = False | |
| for h_ptr in range(batch_hand.shape[1]): | |
| if batch_hand[i, h_ptr] == 0: | |
| batch_hand[i, h_ptr] = top_c | |
| placed = True | |
| break | |
| # Remove from Deck regardless (it left the deck) | |
| batch_deck[i, d_idx] = 0 | |
| if batch_global_ctx[i, 6] > 0: | |
| batch_global_ctx[i, 6] -= 1 # DK | |
| if placed: | |
| batch_global_ctx[i, 3] += 1 # HD | |
| else: | |
| # Hand full, discard to trash | |
| move_to_trash(i, top_c, batch_trash, batch_global_ctx, 2) | |
| else: | |
| break | |
| cards_drawn += 1 | |
| # Apply Heart Requirement Reductions | |
| req_any = card_stats[live_bid, 18] | |
| for ce_idx in range(p_cont_ptr[i]): | |
| op = p_cont_vec[i, ce_idx, 0] | |
| val = p_cont_vec[i, ce_idx, 1] | |
| active = p_cont_vec[i, ce_idx, 9] | |
| if active == 1 and op == 48: # REDUCE_HEART_REQ | |
| target_color = p_cont_vec[i, ce_idx, 2] # 0-5 or 6 (Any) | |
| if target_color == 0: | |
| req_pink = max(0, req_pink - val) | |
| elif target_color == 1: | |
| req_red = max(0, req_red - val) | |
| elif target_color == 2: | |
| req_yel = max(0, req_yel - val) | |
| elif target_color == 3: | |
| req_grn = max(0, req_grn - val) | |
| elif target_color == 4: | |
| req_blu = max(0, req_blu - val) | |
| elif target_color == 5: | |
| req_pur = max(0, req_pur - val) | |
| elif target_color == 6: | |
| req_any = max(0, req_any - val) | |
| # Verify Requirements (Greedy points matching) | |
| met = True | |
| temp_all = stage_all | |
| req_list = [req_pink, req_red, req_yel, req_grn, req_blu, req_pur] | |
| for cidx in range(6): | |
| needed = req_list[cidx] | |
| have = stage_hearts[cidx] | |
| if have < needed: | |
| deficit = needed - have | |
| if temp_all >= deficit: | |
| temp_all -= deficit | |
| else: | |
| met = False | |
| break | |
| if met: | |
| remaining = temp_all | |
| for cidx in range(6): | |
| remaining += max(0, stage_hearts[cidx] - req_list[cidx]) | |
| if remaining < req_any: | |
| met = False | |
| # 3. Apply Result (Defer to Battle Phase) | |
| if met and total_blades > 0: | |
| # SUCCESS PENDING | |
| # --- RULE ACCURACY: Excess Hearts (Rule 8.3) --- | |
| total_p_hearts = ( | |
| stage_hearts[0] | |
| + stage_hearts[1] | |
| + stage_hearts[2] | |
| + stage_hearts[3] | |
| + stage_hearts[4] | |
| + stage_hearts[5] | |
| + stage_all | |
| ) | |
| req_total = req_pink + req_red + req_yel + req_grn + req_blu + req_pur | |
| batch_global_ctx[i, EH] = max(0, total_p_hearts - req_total) | |
| # 4. Trigger ON_LIVE_SUCCESS (TriggerType = 3) | |
| for ab_idx in range(4): | |
| trigger_off = 20 if ab_idx == 0 else (20 + 16 + (ab_idx - 1) * 12) | |
| if card_stats[live_id, trigger_off] == 3: # ON_LIVE_SUCCESS | |
| map_idx = b_idx[live_id, ab_idx] | |
| if map_idx >= 0: | |
| # Execution context | |
| flat_ctx = np.zeros(64, dtype=np.int32) | |
| dummy_opp_tapped = np.zeros(3, dtype=np.int32) | |
| out_bonus_ab = np.zeros(1, dtype=np.int32) | |
| p_tapped_dummy = np.zeros(16, dtype=np.int32) | |
| resolve_bytecode( | |
| b_map[map_idx], | |
| flat_ctx, | |
| batch_global_ctx[i], | |
| 0, | |
| batch_hand[i], | |
| batch_deck[i], | |
| batch_stage[i], | |
| np.zeros((3, 32), dtype=np.int32), | |
| np.zeros(3, dtype=np.int32), | |
| p_cont_vec[i], | |
| p_cont_ptr[i : i + 1], | |
| p_tapped_dummy, | |
| batch_live[i], | |
| dummy_opp_tapped, | |
| batch_trash[i], | |
| b_map, | |
| b_idx, | |
| out_bonus_ab, | |
| card_stats, | |
| opp_stage[i], # Opponent stage | |
| ) | |
| base_score = card_stats[live_id, 38] | |
| if base_score <= 0: | |
| base_score = 1 # Safety | |
| total_score = base_score + volume_bonus + batch_global_ctx[i, LS] | |
| batch_global_ctx[i, LS] = 0 # Reset after applying | |
| batch_global_ctx[i, EH] = 0 # Reset after triggering success | |
| # Clear Stage MEMBERS (Rule 8.3.17) - Members are cleared after performance regardless of battle result? | |
| # Rule 8.4.8 says "Clear ... remaining cards". Rule 8.3.17 says "Clear stage". | |
| # Assume stage clear happens here. | |
| for slot in range(3): | |
| cid = batch_stage[i, slot] | |
| if cid > 0: | |
| move_to_trash(i, cid, batch_trash, batch_global_ctx, 2) | |
| batch_stage[i, slot] = -1 | |
| # We DO NOT remove the live card yet. It waits for battle. | |
| return total_score | |
| else: | |
| # FAILURE - Cards in zone are discarded after performance (Rule 8.3.16) | |
| if live_idx >= 0: | |
| move_to_trash(i, live_id, batch_trash, batch_global_ctx, 2) | |
| batch_live[i, live_idx] = 0 | |
| # Also clear stage on failure | |
| for slot in range(3): | |
| cid = batch_stage[i, slot] | |
| if cid > 0: | |
| move_to_trash(i, cid, batch_trash, batch_global_ctx, 2) | |
| batch_stage[i, slot] = -1 | |
| return 0 | |
| def p_deck_len_helper(batch_deck, i): | |
| return batch_deck.shape[1] | |
| def p_hand_len_helper(batch_hand, i): | |
| return batch_hand.shape[1] | |
| def check_deck_refresh(i, p_deck, p_trash, g_ctx, DK_idx, TR_idx): | |
| # Rule 10.2: If deck empty and trash has cards, refresh. | |
| if g_ctx[i, DK_idx] <= 0 and g_ctx[i, TR_idx] > 0: | |
| # Move trash to deck | |
| d_ptr = 0 | |
| for k in range(p_trash.shape[1]): | |
| cid = p_trash[i, k] | |
| if cid > 0: | |
| p_deck[i, d_ptr] = cid | |
| p_trash[i, k] = 0 | |
| d_ptr += 1 | |
| g_ctx[i, DK_idx] = d_ptr | |
| g_ctx[i, TR_idx] = 0 | |
| # Shuffle Deck | |
| if d_ptr > 1: | |
| for k in range(d_ptr - 1, 0, -1): | |
| j = np.random.randint(0, k + 1) | |
| tmp = p_deck[i, k] | |
| p_deck[i, k] = p_deck[i, j] | |
| p_deck[i, j] = tmp | |
| def resolve_live_performance( | |
| num_envs: int, | |
| action_ids: np.ndarray, | |
| batch_stage: np.ndarray, | |
| batch_live: np.ndarray, | |
| batch_scores: np.ndarray, | |
| batch_global_ctx: np.ndarray, | |
| batch_deck: np.ndarray, | |
| batch_hand: np.ndarray, | |
| batch_trash: np.ndarray, | |
| card_stats: np.ndarray, | |
| batch_cont_vec: np.ndarray, | |
| batch_cont_ptr: np.ndarray, | |
| batch_tapped: np.ndarray, | |
| b_map: np.ndarray, | |
| b_idx: np.ndarray, | |
| ): | |
| for i in range(num_envs): | |
| resolve_live_single( | |
| i, | |
| action_ids[i], | |
| batch_stage, | |
| batch_live, | |
| batch_scores, | |
| batch_global_ctx, | |
| batch_deck, | |
| batch_hand, | |
| batch_trash, | |
| card_stats, | |
| batch_cont_vec[i], | |
| batch_cont_ptr[i : i + 1], | |
| b_map, | |
| b_idx, | |
| batch_tapped[i], | |
| ) | |
| def batch_apply_action( | |
| actions, | |
| pid, | |
| p_stg, | |
| p_ev, | |
| p_ec, | |
| p_cv, | |
| p_cp, | |
| p_tap, | |
| p_sb, | |
| p_lr, | |
| o_tap, | |
| f_ctx_batch, | |
| g_ctx_batch, | |
| p_h, | |
| p_d, | |
| p_tr, # Added | |
| b_map, | |
| b_idx, | |
| card_stats, | |
| ): | |
| num_envs = actions.shape[0] | |
| batch_delta_bonus = np.zeros(num_envs, dtype=np.int32) | |
| for i in prange(num_envs): | |
| g_ctx_batch[i, SC] = p_sb[i] | |
| act_id = actions[i] | |
| if act_id == 0: | |
| # Pass triggers Performance Phase (Rule 8) for all set lives | |
| for z_idx in range(10): # Limit scan | |
| lid = p_lr[i, z_idx] | |
| if lid > 0: | |
| resolve_live_single( | |
| i, | |
| lid, | |
| p_stg, | |
| p_lr, | |
| p_sb, | |
| g_ctx_batch, | |
| p_d, | |
| p_h, | |
| p_tr, | |
| card_stats, | |
| p_cv[i], | |
| p_cp[i], | |
| b_map, | |
| b_idx, | |
| p_tap[i], | |
| ) | |
| g_ctx_batch[i, PH] = 8 | |
| elif 1 <= act_id <= 180: | |
| # Member play logic... (Keeping original stable version) | |
| adj = act_id - 1 | |
| hand_idx = adj // 3 | |
| slot = adj % 3 | |
| if hand_idx < p_h.shape[1]: | |
| card_id = p_h[i, hand_idx] | |
| if card_id > 0 and card_id < card_stats.shape[0]: | |
| cost = card_stats[card_id, 0] | |
| effective_cost = cost | |
| prev_cid = p_stg[i, slot] | |
| if prev_cid >= 0 and prev_cid < card_stats.shape[0]: | |
| prev_cost = card_stats[prev_cid, 0] | |
| effective_cost = cost - prev_cost | |
| if effective_cost < 0: | |
| effective_cost = 0 | |
| # Rule 6.4.1 & Parity with get_member_cost: Substract continuous cost reductions | |
| total_reduction = 0 | |
| for ce_k in range(p_cp[i, 0]): # p_cp is ptr array | |
| if p_cv[i, ce_k, 0] == 3: # REDUCE_COST type | |
| total_reduction += p_cv[i, ce_k, 1] | |
| effective_cost -= total_reduction | |
| if effective_cost < 0: | |
| effective_cost = 0 | |
| # Capture the ID of the card being replaced (if any) for Baton Pass | |
| prev_cid = p_stg[i, slot] | |
| g_ctx_batch[i, PREV_CID_IDX] = prev_cid | |
| ec = g_ctx_batch[i, EN] | |
| if ec > 12: | |
| ec = 12 | |
| paid = 0 | |
| if effective_cost > 0: | |
| for e_idx in range(ec): | |
| if 3 + e_idx < 16: | |
| if p_tap[i, 3 + e_idx] == 0: | |
| p_tap[i, 3 + e_idx] = 1 | |
| paid += 1 | |
| if paid >= effective_cost: | |
| break | |
| else: | |
| break | |
| if prev_cid > 0: | |
| move_to_trash(i, prev_cid, p_tr, g_ctx_batch, 2) | |
| p_stg[i, slot] = card_id | |
| # Fix Duplication: Remove from hand | |
| p_h[i, hand_idx] = 0 | |
| g_ctx_batch[i, 3] -= 1 | |
| # Resolve Auto-Effects (On Play) | |
| if card_id > 0 and card_id < b_idx.shape[0]: | |
| map_idx = b_idx[card_id, 0] | |
| g_ctx_batch[i, 51 + slot] = 1 | |
| if card_id < b_idx.shape[0]: | |
| map_idx = b_idx[card_id, 0] | |
| if map_idx >= 0: | |
| code_seq = b_map[map_idx] | |
| f_ctx_batch[i, 7] = 1 | |
| f_ctx_batch[i, SID] = card_id | |
| p_cp_slice = p_cp[i : i + 1] | |
| out_bonus_slice = batch_delta_bonus[i : i + 1] | |
| out_bonus_slice[0] = 0 | |
| resolve_bytecode( | |
| code_seq, | |
| f_ctx_batch[i], | |
| g_ctx_batch[i], | |
| pid, | |
| p_h[i], | |
| p_d[i], | |
| p_stg[i], | |
| p_ev[i], | |
| p_ec[i], | |
| p_cv[i], | |
| p_cp_slice, | |
| p_tap[i], | |
| p_lr[i], | |
| o_tap[i], | |
| p_tr[i], | |
| b_map, | |
| b_idx, | |
| out_bonus_slice, | |
| card_stats, | |
| o_tap[i], | |
| ) | |
| p_sb[i] += out_bonus_slice[0] | |
| f_ctx_batch[i, 7] = 0 | |
| elif 200 <= act_id <= 202: | |
| # Activation logic... | |
| slot = act_id - 200 | |
| card_id = p_stg[i, slot] | |
| if card_id >= 0 and card_id < b_idx.shape[0]: | |
| map_idx = b_idx[card_id, 0] | |
| if map_idx >= 0: | |
| code_seq = b_map[map_idx] | |
| f_ctx_batch[i, 7] = 1 | |
| f_ctx_batch[i, SID] = card_id | |
| p_cp_slice = p_cp[i : i + 1] | |
| out_bonus_slice = batch_delta_bonus[i : i + 1] | |
| out_bonus_slice[0] = 0 | |
| resolve_bytecode( | |
| code_seq, | |
| f_ctx_batch[i], | |
| g_ctx_batch[i], | |
| pid, | |
| p_h[i], | |
| p_d[i], | |
| p_stg[i], | |
| p_ev[i], | |
| p_ec[i], | |
| p_cv[i], | |
| p_cp_slice, | |
| p_tap[i], | |
| p_lr[i], | |
| o_tap[i], | |
| p_tr[i], | |
| b_map, | |
| b_idx, | |
| out_bonus_slice, | |
| card_stats, | |
| o_tap[i], | |
| ) | |
| p_sb[i] += out_bonus_slice[0] | |
| f_ctx_batch[i, 7] = 0 | |
| p_tap[i, slot] = 1 | |
| elif 400 <= act_id <= 459: | |
| # Set Live Card from Hand (Rule 8.3) | |
| hand_idx = act_id - 400 | |
| if hand_idx < p_h.shape[1]: | |
| card_id = p_h[i, hand_idx] | |
| if card_id > 0: # Allow any card (Rule 8.3 & 8.2.2) | |
| # Find empty slot in live zone (max 3) | |
| for z_idx in range(p_lr.shape[1]): | |
| if p_lr[i, z_idx] == 0: | |
| p_lr[i, z_idx] = card_id | |
| p_h[i, hand_idx] = 0 | |
| g_ctx_batch[i, 3] -= 1 # HD | |
| # Rule 8.2.2: Draw 1 card after placing | |
| # Check Refresh first | |
| check_deck_refresh(i, p_d, p_tr, g_ctx_batch, 6, 2) | |
| # Find top card | |
| d_idx = -1 | |
| for k in range(p_d.shape[1]): # Fixed 60 -> shape[1] | |
| if p_d[i, k] > 0: | |
| d_idx = k | |
| break | |
| if d_idx != -1: | |
| top_c = p_d[i, d_idx] | |
| p_d[i, d_idx] = 0 | |
| g_ctx_batch[i, 6] -= 1 # DK | |
| # Add to hand | |
| placed = False | |
| for h_ptr in range(p_h.shape[1]): | |
| if p_h[i, h_ptr] == 0: | |
| p_h[i, h_ptr] = top_c | |
| g_ctx_batch[i, 3] += 1 # HD | |
| placed = True | |
| break | |
| if not placed: | |
| # Hand full, discard | |
| move_to_trash(i, top_c, p_tr, g_ctx_batch, 2) | |
| break | |
| elif 500 <= act_id <= 559: # STANDARD Hand Selection | |
| f_ctx_batch[i, 15] = act_id - 500 | |
| g_ctx_batch[i, 8] = 4 | |
| elif 600 <= act_id <= 611: # STANDARD Energy Selection | |
| f_ctx_batch[i, 15] = act_id - 600 | |
| g_ctx_batch[i, 8] = 4 | |
| elif 100 <= act_id <= 159: # ATTENTION Hand Selection | |
| f_ctx_batch[i, 15] = act_id - 100 | |
| g_ctx_batch[i, 8] = 4 | |
| elif 160 <= act_id <= 171: # ATTENTION Energy Selection | |
| f_ctx_batch[i, 15] = act_id - 160 | |
| g_ctx_batch[i, 8] = 4 | |
| p_sb[i] = g_ctx_batch[i, SC] | |
| def apply_action(aid, pid, p_stg, p_ev, p_ec, p_cv, p_cp, p_tap, p_sb, p_lr, o_tap, f_ctx, g_ctx, p_h, p_d, p_tr): | |
| # Specialized fast-path for Action 1 (Simulation) | |
| if aid == 1: | |
| bc = np.zeros((1, 4), dtype=np.int32) | |
| bc[0, 0] = 11 # O_BLADES | |
| bc[0, 1] = 1 | |
| bc[0, 3] = 0 | |
| d_map = np.zeros((1, 1, 4), dtype=np.int32) | |
| d_idx = np.zeros((1, 4), dtype=np.int32) | |
| cptr_arr = np.array([p_cp], dtype=np.int32) | |
| bn_arr = np.zeros(1, dtype=np.int32) | |
| resolve_bytecode( | |
| bc, | |
| f_ctx, | |
| g_ctx, | |
| pid, | |
| p_h, | |
| p_d, | |
| p_stg, | |
| p_ev, | |
| p_ec, | |
| p_cv, | |
| cptr_arr, | |
| p_tap, | |
| p_lr, | |
| o_tap, | |
| p_tr, # Pass trash | |
| d_map, | |
| d_idx, | |
| bn_arr, | |
| o_tap, # Pass opp_tapped count | |
| ) | |
| return cptr_arr[0], p_sb + bn_arr[0] | |
| return p_cp, p_sb | |
| def _move_to_trash_single(card_id, p_trash, p_global_ctx, TR_idx): | |
| if card_id <= 0: | |
| return | |
| for k in range(p_trash.shape[0]): | |
| if p_trash[k] == 0: | |
| p_trash[k] = card_id | |
| p_global_ctx[TR_idx] += 1 | |
| break | |
| def _check_deck_refresh_single(p_deck, p_trash, p_global_ctx, DK_idx, TR_idx): | |
| if p_global_ctx[DK_idx] <= 0 and p_global_ctx[TR_idx] > 0: | |
| # Move trash to deck | |
| d_ptr = 0 | |
| for k in range(p_trash.shape[0]): | |
| if p_trash[k] > 0: | |
| p_deck[d_ptr] = p_trash[k] | |
| p_trash[k] = 0 | |
| d_ptr += 1 | |
| p_global_ctx[DK_idx] = d_ptr | |
| p_global_ctx[TR_idx] = 0 | |
| # Shuffle | |
| if d_ptr > 1: | |
| for k in range(d_ptr - 1, 0, -1): | |
| j = np.random.randint(0, k + 1) | |
| tmp = p_deck[k] | |
| p_deck[k] = p_deck[j] | |
| p_deck[j] = tmp | |
| def select_heuristic_action( | |
| p_hand, | |
| p_deck, | |
| p_stage, | |
| p_energy_vec, | |
| p_energy_count, | |
| p_tapped, | |
| p_live, | |
| p_scores, | |
| p_global_ctx, | |
| p_trash, | |
| o_tapped, | |
| card_stats, | |
| bytecode_index, | |
| ): | |
| """ | |
| Selects a single heuristic action for the given player state. | |
| Returns: action_id (int) | |
| """ | |
| # --- 1. Select Action (Score-Based Heuristic) --- | |
| best_action = 0 | |
| best_score = -1.0 | |
| # A. Hand Actions (Play Member / Set Live) | |
| for h in range(p_hand.shape[0]): | |
| cid = p_hand[h] | |
| if cid > 0: | |
| if cid > 900: # Live Card (Simplified Check > 900) | |
| # Should verify type in card_stats if available, but >900 heuristic is okay for now | |
| if card_stats[cid, 10] == 2: # Live | |
| # Prefer setting live | |
| empty_live = 0 | |
| for z in range(p_live.shape[0]): | |
| if p_live[z] == 0: | |
| empty_live += 1 | |
| if empty_live > 0: | |
| score = 100.0 + np.random.random() * 10.0 # High priority | |
| if score > best_score: | |
| best_score = score | |
| best_action = 400 + h | |
| else: # Member Card | |
| if card_stats[cid, 10] == 1: # Member | |
| cost = card_stats[cid, 0] | |
| ec = p_global_ctx[5] # EN | |
| if ec >= cost: | |
| score = (cost * 15.0) + (np.random.random() * 5.0) # Favor high cost | |
| # If we have lots of energy, prioritize spending it | |
| if ec > 5: | |
| score += 5.0 | |
| # Check slots | |
| for s in range(3): | |
| # Prefer empty slots or replacing weak low-cost cards | |
| prev_cid = p_stage[s] | |
| effective_cost = cost | |
| if prev_cid > 0: | |
| prev_cost = card_stats[prev_cid, 0] | |
| effective_cost = max(0, cost - prev_cost) | |
| # Bonus for upgrading | |
| if cost > prev_cost: | |
| score += 10.0 | |
| # Tapping Check (simplified heuristic) | |
| # Assume if we have EC >= EffCost, we can pay. | |
| if ec >= effective_cost: | |
| current_act = (h * 3) + s + 1 | |
| if score > best_score: | |
| best_score = score | |
| best_action = current_act | |
| # B. Stage Actions (Activate) | |
| for s in range(3): | |
| cid = p_stage[s] | |
| if cid > 0 and not p_tapped[s]: | |
| if cid < bytecode_index.shape[0]: | |
| if bytecode_index[cid, 0] >= 0: | |
| # Activation is usually good (draw, boost, etc.) | |
| score = 25.0 + np.random.random() * 5.0 | |
| if score > best_score: | |
| best_score = score | |
| best_action = 200 + s | |
| return best_action | |
| def run_opponent_turn_loop( | |
| p_hand, | |
| p_deck, | |
| p_stage, | |
| p_energy_vec, | |
| p_energy_count, | |
| p_tapped, | |
| p_live, | |
| p_scores, | |
| p_global_ctx, | |
| p_trash, | |
| p_continuous_vec, | |
| p_continuous_ptr, | |
| o_tapped, | |
| card_stats, | |
| bytecode_map, | |
| bytecode_index, | |
| ): | |
| """ | |
| Simulates a full opponent turn by looping actions until Pass (0) is chosen. | |
| Operating on single-environment slices (1D/2D). | |
| """ | |
| # Safety limit to prevent infinite loops | |
| for step_count in range(20): | |
| action = select_heuristic_action( | |
| p_hand, | |
| p_deck, | |
| p_stage, | |
| p_energy_vec, | |
| p_energy_count, | |
| p_tapped, | |
| p_live, | |
| p_scores, | |
| p_global_ctx, | |
| p_trash, | |
| o_tapped, | |
| card_stats, | |
| bytecode_index, | |
| ) | |
| # --- 2. Execute Action --- | |
| if action == 0: | |
| return | |
| if 1 <= action <= 180: | |
| adj = action - 1 | |
| hand_idx = adj // 3 | |
| slot = adj % 3 | |
| if hand_idx < p_hand.shape[0]: | |
| card_id = p_hand[hand_idx] | |
| if card_id > 0: | |
| cost = card_stats[card_id, 0] | |
| prev_cid = p_stage[slot] | |
| effective_cost = cost | |
| if prev_cid > 0: | |
| prev_cost = card_stats[prev_cid, 0] | |
| effective_cost = max(0, cost - prev_cost) | |
| untapped_e = np.zeros(16, dtype=np.int32) | |
| ue_ptr = 0 | |
| en_count = p_global_ctx[5] | |
| for e_idx in range(en_count): | |
| if 3 + e_idx < p_tapped.shape[0]: | |
| if p_tapped[3 + e_idx] == 0: | |
| untapped_e[ue_ptr] = 3 + e_idx | |
| ue_ptr += 1 | |
| can_pay = True | |
| if ue_ptr < effective_cost: | |
| can_pay = False | |
| if can_pay: | |
| # Pay | |
| for p_idx in range(effective_cost): | |
| tap_idx = untapped_e[p_idx] | |
| p_tapped[tap_idx] = 1 | |
| # Move prev_cid to trash (if it exists) | |
| _move_to_trash_single(prev_cid, p_trash, p_global_ctx, 2) | |
| # Capture prev_cid for Baton Pass logic | |
| p_global_ctx[60] = prev_cid | |
| p_stage[slot] = card_id | |
| p_hand[hand_idx] = 0 | |
| p_global_ctx[3] -= 1 # HD | |
| # Note: g_ctx must be passed correctly if resolve_bytecode needs it | |
| p_global_ctx[51 + slot] = 1 # Mark played | |
| if card_id < bytecode_index.shape[0]: | |
| map_idx = bytecode_index[card_id, 0] | |
| if map_idx >= 0: | |
| d_bonus = np.zeros(1, dtype=np.int32) | |
| # p_continuous_vec is (32, 10). p_continuous_ptr is (1,) | |
| # resolve_bytecode expects p_cp_slice as (1,) | |
| resolve_bytecode( | |
| bytecode_map[map_idx], | |
| np.zeros(64, dtype=np.int32), | |
| p_global_ctx, | |
| 1, | |
| p_hand, | |
| p_deck, | |
| p_stage, | |
| p_energy_vec, | |
| p_energy_count, | |
| p_continuous_vec, | |
| p_continuous_ptr, | |
| p_tapped, | |
| p_live, | |
| o_tapped, | |
| p_trash, | |
| bytecode_map, | |
| bytecode_index, | |
| d_bonus, | |
| card_stats, | |
| o_tapped, | |
| ) | |
| p_scores[0] += d_bonus[0] | |
| elif 200 <= action <= 202: | |
| slot = action - 200 | |
| card_id = p_stage[slot] | |
| if card_id > 0 and p_tapped[slot] == 0: | |
| if card_id < bytecode_index.shape[0]: | |
| map_idx = bytecode_index[card_id, 0] | |
| if map_idx >= 0: | |
| d_bonus = np.zeros(1, dtype=np.int32) | |
| resolve_bytecode( | |
| bytecode_map[map_idx], | |
| np.zeros(64, dtype=np.int32), | |
| p_global_ctx, | |
| 1, | |
| p_hand, | |
| p_deck, | |
| p_stage, | |
| p_energy_vec, | |
| p_energy_count, | |
| p_continuous_vec, | |
| p_continuous_ptr, | |
| p_tapped, | |
| p_live, | |
| o_tapped, | |
| p_trash, | |
| bytecode_map, | |
| bytecode_index, | |
| d_bonus, | |
| card_stats, | |
| o_tapped, | |
| ) | |
| p_scores[0] += d_bonus[0] | |
| p_tapped[slot] = 1 | |
| elif 400 <= action <= 459: | |
| hand_idx = action - 400 | |
| if hand_idx < p_hand.shape[0]: | |
| card_id = p_hand[hand_idx] | |
| if card_id > 0: | |
| l_slot = -1 | |
| for z in range(p_live.shape[0]): | |
| if p_live[z] == 0: | |
| l_slot = z | |
| break | |
| if l_slot != -1: | |
| p_live[l_slot] = card_id | |
| p_hand[hand_idx] = 0 | |
| p_global_ctx[3] -= 1 | |
| _check_deck_refresh_single(p_deck, p_trash, p_global_ctx, 6, 2) | |
| d_idx = -1 | |
| for k in range(p_deck.shape[0]): | |
| if p_deck[k] > 0: | |
| d_idx = k | |
| break | |
| if d_idx != -1: | |
| top_c = p_deck[d_idx] | |
| p_deck[d_idx] = 0 | |
| p_global_ctx[6] -= 1 | |
| placed = False | |
| for h in range(p_hand.shape[0]): | |
| if p_hand[h] == 0: | |
| p_hand[h] = top_c | |
| p_global_ctx[3] += 1 | |
| placed = True | |
| break | |
| if not placed: | |
| _move_to_trash_single(top_c, p_trash, p_global_ctx, 2) | |
| def run_random_turn_loop( | |
| p_hand, | |
| p_deck, | |
| p_stage, | |
| p_energy_vec, | |
| p_energy_count, | |
| p_tapped, | |
| p_live, | |
| p_scores, | |
| p_global_ctx, | |
| p_trash, | |
| p_continuous_vec, | |
| p_continuous_ptr, | |
| o_tapped, | |
| card_stats, | |
| bytecode_map, | |
| bytecode_index, | |
| ): | |
| """ | |
| Simulates a full opponent turn by selecting random legal actions. | |
| """ | |
| for step_count in range(20): # Safety limit | |
| # Gather legal candidates | |
| candidates = [0] # Always can Pass | |
| # 1. Member Play Candidates | |
| ec = p_global_ctx[5] | |
| for h in range(p_hand.shape[0]): | |
| cid = p_hand[h] | |
| if cid > 0 and cid < 900: | |
| cost = card_stats[cid, 0] | |
| for s in range(3): | |
| prev_cid = p_stage[s] | |
| effective_cost = cost | |
| if prev_cid > 0: | |
| prev_cost = card_stats[prev_cid, 0] | |
| effective_cost = max(0, cost - prev_cost) | |
| if ec >= effective_cost: | |
| candidates.append((h * 3) + s + 1) | |
| # 2. Activate Candidates | |
| for s in range(3): | |
| cid = p_stage[s] | |
| if cid > 0 and not p_tapped[s]: | |
| if cid < bytecode_index.shape[0]: | |
| if bytecode_index[cid, 0] >= 0: | |
| candidates.append(200 + s) | |
| # 3. Live Set Candidates | |
| empty_live = 0 | |
| for z in range(p_live.shape[0]): | |
| if p_live[z] == 0: | |
| empty_live += 1 | |
| if empty_live > 0: | |
| for h in range(p_hand.shape[0]): | |
| cid = p_hand[h] | |
| if cid > 900: | |
| candidates.append(400 + h) | |
| # Pick one | |
| idx = np.random.randint(0, len(candidates)) | |
| action = candidates[idx] | |
| if action == 0: | |
| return | |
| # Execute | |
| if 1 <= action <= 180: | |
| adj = action - 1 | |
| hand_idx = adj // 3 | |
| slot = adj % 3 | |
| card_id = p_hand[hand_idx] | |
| cost = card_stats[card_id, 0] | |
| prev_cid = p_stage[slot] | |
| effective_cost = cost | |
| if prev_cid > 0: | |
| prev_cost = card_stats[prev_cid, 0] | |
| _move_to_trash_single(prev_cid, p_trash, p_global_ctx, 2) | |
| # Capture prev_cid for Baton Pass logic | |
| p_global_ctx[60] = prev_cid | |
| effective_cost = max(0, cost - prev_cost) | |
| # Pay | |
| p_tapped[3:] = 0 # Dummy untap logic? No, we should use real payment | |
| # Actually run_opponent_turn_loop has a payment block, I'll simplify here | |
| p_global_ctx[5] -= effective_cost | |
| p_stage[slot] = card_id | |
| p_hand[hand_idx] = 0 | |
| p_global_ctx[3] -= 1 | |
| if card_id < bytecode_index.shape[0]: | |
| map_idx = bytecode_index[card_id, 0] | |
| if map_idx >= 0: | |
| d_bonus = np.zeros(1, dtype=np.int32) | |
| resolve_bytecode( | |
| bytecode_map[map_idx], | |
| np.zeros(64, dtype=np.int32), | |
| p_global_ctx, | |
| 1, | |
| p_hand, | |
| p_deck, | |
| p_stage, | |
| p_energy_vec, | |
| p_energy_count, | |
| p_continuous_vec, | |
| p_continuous_ptr, | |
| p_tapped, | |
| p_live, | |
| o_tapped, | |
| p_trash, | |
| bytecode_map, | |
| bytecode_index, | |
| d_bonus, | |
| card_stats, | |
| o_tapped, | |
| ) | |
| p_scores[0] += d_bonus[0] | |
| elif 200 <= action <= 202: | |
| slot = action - 200 | |
| cid = p_stage[slot] | |
| if cid > 0: | |
| d_bonus = np.zeros(1, dtype=np.int32) | |
| if cid < bytecode_index.shape[0]: | |
| map_idx = bytecode_index[cid, 0] | |
| if map_idx >= 0: | |
| resolve_bytecode( | |
| bytecode_map[map_idx], | |
| np.zeros(64, dtype=np.int32), | |
| p_global_ctx, | |
| 1, | |
| p_hand, | |
| p_deck, | |
| p_stage, | |
| p_energy_vec, | |
| p_energy_count, | |
| p_continuous_vec, | |
| p_continuous_ptr, | |
| p_tapped, | |
| p_live, | |
| o_tapped, | |
| p_trash, | |
| bytecode_map, | |
| bytecode_index, | |
| d_bonus, | |
| card_stats, | |
| o_tapped, | |
| ) | |
| p_scores[0] += d_bonus[0] | |
| p_tapped[slot] = 1 | |
| elif 400 <= action <= 459: | |
| hand_idx = action - 400 | |
| card_id = p_hand[hand_idx] | |
| l_slot = -1 | |
| for z in range(p_live.shape[0]): | |
| if p_live[z] == 0: | |
| l_slot = z | |
| break | |
| if l_slot != -1: | |
| p_live[l_slot] = card_id | |
| p_hand[hand_idx] = 0 | |
| p_global_ctx[3] -= 1 | |
| _check_deck_refresh_single(p_deck, p_trash, p_global_ctx, 6, 2) | |
| # Draw 1 | |
| for k in range(p_deck.shape[0]): | |
| if p_deck[k] > 0: | |
| top_c = p_deck[k] | |
| p_deck[k] = 0 | |
| p_global_ctx[DK] -= 1 | |
| for h in range(p_hand.shape[0]): | |
| if p_hand[h] == 0: | |
| p_hand[h] = top_c | |
| p_global_ctx[HD] += 1 | |
| break | |
| break | |