Designing a Resilient Boss Battle Interaction Loop for Turn-Based Games

When implementing staged boss encounters in a card‑based roguelike, a dedicated interaction loop becomes essential to orchestrate player decisions, enforce rules, and manage auxiliary services such as hints. This article examines the architecture of a reusable boss fight loop that parses multi‑language combat commands, limits paid API calls, and seamlessly integrates with existing game state.

Action Parsing with Enum‑Based Dispatch

All player input passes through a normalising parser that accepts English shortcuts, full phrases, or localised aliases. The parser maps the first token against a configurable set of keywords and extracts optional parameters for card counts and claimed essence types.

from enum import Enum
from typing import Optional, Dict, Any

class Essence(Enum):
    RIGHTEOUS = "righteous"
    WICKED   = "wicked"
    NEUTRAL  = "neutral"

# Keyword aliases can be extended for different languages
KEYWORD_MAP = {
    "play": "follow", "p": "follow", "place": "follow",
    "chal": "challenge", "c": "challenge", "dispute": "challenge",
    "quit": "fold", "f": "fold", "surrender": "fold",
}

ESSENCE_ALIASES = {
    "righteous": Essence.RIGHTEOUS, "good": Essence.RIGHTEOUS,
    "wicked": Essence.WICKED,   "evil": Essence.WICKED,
    "neutral": Essence.NEUTRAL, "balanced": Essence.NEUTRAL,
}

def parse_action(user_input: str) -> Optional[Dict[str, Any]]:
    """Convert raw player input into a structured action dictionary."""
    clean = user_input.strip().lower()
    if not clean:
        return None
    tokens = clean.split()
    root = KEYWORD_MAP.get(tokens[0], tokens[0])

    if root == "follow":
        if len(tokens) != 3:
            return None
        try:
            count = int(tokens[1])
        except ValueError:
            return None
        if not 1 <= count <= 3:
            return None
        essence = ESSENCE_ALIASES.get(tokens[2])
        if essence is None:
            return None
        return {"action": "follow", "card_count": count, "claimed_essence": essence}

    if root in ("challenge", "fold"):
        if len(tokens) != 1:
            return None
        return {"action": root}

    return None

The parser focuses on clarity and extensibility: new aliases can be registered without modifying branching logic. The returned dictionary contains only primitive types and enumeration members, making it straightforward to validate in the main loop.

The Encounter Loop

The core function run_encounter accepts a fully initialised BossEncounter instance and a session state dictionary. It maintains a local hint counter and delegates system commands (inventory, help, status) to a pre‑existing dispatcher, while intercepting the hint command to enforce the per‑fight usage cap of three.

def run_encounter(encounter: BossEncounter, session: dict) -> str:
    """
    Execute a full boss fight loop.

    Returns:
        'player' if the boss is defeated, 'boss' if the player loses,
        or 'exit' if the player quits manually.
    """
    hint_usage = 0
    MAX_HINTS = 3

    while not encounter.is_finished:
        snapshot = encounter.get_snapshot()
        _display_table(snapshot)

        if encounter.current_actor == "player":
            raw = input("> ").strip()
            if not raw:
                continue

            # ---- intercept hint to count usage ----
            if _is_hint_command(raw):
                if hint_usage >= MAX_HINTS:
                    print("Hint limit reached (3). You cannot ask for more hints in this fight.")
                    continue
                response = dispatch_system("hint", session, encounter)
                print(response["message"])
                if response["should_exit"]:
                    return "exit"
                hint_usage += 1
                continue

            # ---- other system commands ----
            sys_resp = dispatch_system(raw, session, encounter)
            if sys_resp["should_exit"]:
                return "exit"
            if sys_resp["message"]:
                print(sys_resp["message"])
                continue

            # ---- battle action ----
            action = parse_action(raw)
            if action is None:
                print("Invalid command. Use play/challenge/fold or a system command (help, pack, status, hint, quit).")
                continue

            outcome = encounter.perform_player_action(**action)
            print(outcome["message"])
            if outcome.get("narration"):
                print(outcome["narration"])
            if outcome["game_over"]:
                break
        else:
            outcome = encounter.perform_boss_turn()
            print(outcome["message"])
            if outcome.get("narration"):
                print(outcome["narration"])
            if outcome["game_over"]:
                break

    return encounter.winner

The loop delegates turn management to the encounter object; after each valid player action or boss move the game_over flag is checked immediately. System commands are processed without advancing the turn, while a hint only consumes the usage counter. The _display_table helper renders player hand size, boss hand size, last claim, and the active pile count.

Integrating with External Modules

The dispatch_system function mirrors the existing handle_system_command from the project’s command module but is adapted to accept the encounter context directly. When a hint is requested, the dispatcher calls the LLM‑powered hint module using the encounter’s snapshot as prompt context. The session dictionary carries the in_boss_fight flag sothat hints know to pull relevant boss‑specific data.

To guarantee the per‑encounter hint limit, the loop pre‑empts the dispatcher for hint commands. This separation keeps the limit logic inside the loop and avoids modifying the general command handler.

Testing Strategy

Unit tests for the parser cover every alias combination, boundary values (card count 0 or 4), and malformed input. Integration tests simulate a full fight by mocking the BossEncounter methods to return controlled snapshots and outcomes. The hint limit test calls the hint path four times and asserts that the fourth invocation is blocked while the fight continues. Exit tests verify that quit returns 'exit' immediately without affecting the encounter result.

All test doubles inherit from a lightweight interface, making them easy to reuse across both loop and parser tests. The mock setup also exercises the narration and display functions to catch rendering regressions early.

Tags: python game-development boss-fight command-parser turn-based

Posted on Mon, 07 Sep 2026 16:46:03 +0000 by joviyach