rabukasim / docs /spec /VM_IMPLEMENTATION_GUIDE.md
trioskosmos's picture
Upload folder using huggingface_hub
463f868 verified

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:

  1. Refactored Signature: resolve_bytecode no longer returns (cptr, state, bonus).
  2. Mutable Arrays: It now accepts out_cptr and out_bonus as Numpy arrays of size 1.
  3. In-Place Updates: The function modifies out_cptr[0] and out_bonus[0] directly.
  4. Recursive Call: When O_TRIGGER_REMOTE is encountered, the VM looks up the target's bytecode and calls resolve_bytecode recursively, 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:

  1. Compiler: Ability.compile (in engine/models/ability.py) generates a header block:
    • [O_SELECT_MODE, NumOptions, 0, 0]
    • Followed by NumOptions instructions of [O_JUMP, Offset, 0, 0].
  2. 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.

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) to 10. 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 is RECOVER_MEMBER (Salvage from Discard), and ACTIVATE_MEMBER (Untap) is ID 43.
  • Fix: The Untap logic has been moved to O_ACTIVATE_MEMBER (43). O_RECOV_M (17) and O_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