Spaces:
Sleeping
VM Implementation Guide: Opcodes & Fast Logic
This guide documents the high-performance Virtual Machine (VM) updates implemented in engine/game/fast_logic.py, specifically targeting the "Critical Gap" opcodes identified for Logic Coverage Speedup.
1. New Opcodes Implemented
The following opcodes have been added to the Numba-compiled fast_logic.py engine:
| Opcode | ID | Name | Description |
|---|---|---|---|
| SELECT_MODE | 30 | O_SELECT_MODE |
Implements branching/modal logic via Jump Tables. |
| TRIGGER_REMOTE | 47 | O_TRIGGER_REMOTE |
Recursively executes an ability from another card (e.g., from Hand or Stage). |
| SEARCH_DECK | 22 | O_SEARCH_DECK |
Moves a specific card (chosen by AI) from Deck to Hand. |
| LOOK_AND_CHOOSE | 41 | O_LOOK_AND_CHOOSE |
Reveals top N cards, adds one to hand, discards rest. |
| ORDER_DECK | 28 | O_ORDER_DECK |
Reorders (reverses/shuffles) the top N cards of the deck. |
| REDUCE_COST | 13 | O_REDUCE_COST |
Adds a cost-reduction modifier to continuous effects. |
| REDUCE_HEART_REQ | 48 | O_REDUCE_HEART_REQ |
Adds a heart-requirement reduction modifier. |
| REPLACE_EFFECT | 46 | O_REPLACE_EFFECT |
Sets a replacement effect flag/modifier. |
| SWAP_CARDS | 21 | O_SWAP_CARDS |
Discards N cards from hand (filtering) and Draws N cards. |
| PLACE_UNDER | 33 | O_PLACE_UNDER |
Moves a card from Hand to a member's Stage Energy. |
| MOVE_MEMBER | 20 | O_MOVE_MEMBER |
Swaps two members (and their states) on the stage. |
| ACTIVATE_MEMBER | 43 | O_ACTIVATE_MEMBER |
Untaps a member. (Fixed mapping from ID 17). |
2. Architecture Updates
To support these complex operations within the constraints of Numba (JIT compilation), two major architectural patterns were introduced:
A. Recursion via Pass-by-Reference (TRIGGER_REMOTE)
Numba's type inference engine struggles with recursive function calls when functions return tuples that change state. To solve this for TRIGGER_REMOTE:
- Refactored Signature:
resolve_bytecodeno longer returns(cptr, state, bonus). - Mutable Arrays: It now accepts
out_cptrandout_bonusas Numpy arrays of size 1. - In-Place Updates: The function modifies
out_cptr[0]andout_bonus[0]directly. - Recursive Call: When
O_TRIGGER_REMOTEis encountered, the VM looks up the target's bytecode and callsresolve_bytecoderecursively, passing the same state arrays.
# Pseudo-code pattern
@njit
def resolve_bytecode(..., out_cptr, out_bonus):
# ... logic ...
if op == O_TRIGGER_REMOTE:
# Save state
out_cptr[0] = cptr
# Recursive call
resolve_bytecode(..., out_cptr, out_bonus)
# Reload state
cptr = out_cptr[0]
B. Jump Tables for Branching (SELECT_MODE)
Numba functions are linear. To implement "Choose One" modal effects:
- Compiler:
Ability.compile(inengine/models/ability.py) generates a header block:[O_SELECT_MODE, NumOptions, 0, 0]- Followed by
NumOptionsinstructions of[O_JUMP, Offset, 0, 0].
- VM Logic:
- Reads choice index from
flat_ctx[CTX_CHOICE_INDEX]. - Calculates the target Jump Instruction index:
ip + 1 + choice. - Reads the offset from that Jump instruction and executes the jump.
- Reads choice index from
3. Dynamic Targeting (MEMBER_SELECT)
Targeting logic has been decoupled from bytecode hardcoding:
- Logic:
if s == 10: s = int(flat_ctx[CTX_TARGET_SLOT]) - Usage: Any opcode (e.g.,
BUFF,TAP) can set its target slot (s) to10. The VM will then use the value provided by the Agent (in the Context Vector) at runtime.
4. Compiler Usage
The Ability class in engine/models/ability.py has been updated to automatically compile these structures.
# Example: Creating a Modal Ability
ability = Ability(
raw_text="Choose one: Draw 1 or Charge 1",
trigger=TriggerType.ON_PLAY,
effects=[Effect(EffectType.SELECT_MODE, 1)],
# Ensure modal_options is set on the Ability or the Effect
modal_options=[
[Effect(EffectType.DRAW, 1)],
[Effect(EffectType.ENERGY_CHARGE, 1)]
]
)
# Compiling
bytecode = ability.compile()
# Result: [SELECT_MODE, 2, ... JUMP ... JUMP ... DRAW ... JUMP_END ... CHARGE ... JUMP_END ...]
5. Additional Opcode Fixes
Salvage vs. Untap Correction
- Previously,
O_RECOV_M(ID 17) was incorrectly implemented as "Untap Member". In the official opcode list, ID 17 isRECOVER_MEMBER(Salvage from Discard), andACTIVATE_MEMBER(Untap) is ID 43. - Fix: The Untap logic has been moved to
O_ACTIVATE_MEMBER(43).O_RECOV_M(17) andO_RECOV_L(15) are now placeholders (pass) because "Salvage" requires access to the Discard pile, which is not currently available in the fast VM state vector.
6. Testing
New tests in tests/test_vm_opcodes.py verify these features:
test_select_mode_branching: Verifies jump logic.test_trigger_remote: Verifies recursion depth and state preservation.test_search_deck: Verifies deck scanning and removal.test_look_and_choose: Verifies "Look N, Pick 1, Discard Rest" logic.test_swap_cards: Verifies discard/draw cycle.test_place_under: Verifies moving cards to stage energy.test_move_member: Verifies slot swapping.
Run tests with:
cargo test --manifest-path engine_rust_src/Cargo.toml test_vm_opcodes -- --nocapture