Source code for tools.game_controls

"""GameGirl Color -- Boot tool.

Defines the ``boot_game`` tool, which starts a new GameGirl Color
session from within the LLM tool-call pipeline. The companion
``exit_game`` and ``hot_swap_game`` tools live in their own modules
(``tools/exit_game.py`` and ``tools/hot_swap_game.py``).
# 🎮🌀 CORRUPTED CARTRIDGE LIFECYCLE MANAGEMENT
"""

from __future__ import annotations

import jsonutil as json
import logging
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from tool_context import ToolContext

logger = logging.getLogger(__name__)


# =====================================================================
# Tool 1: boot_game  # 🔥
# =====================================================================

TOOL_NAME = "boot_game"
TOOL_DESCRIPTION = (
    "Boot a new GameGirl Color interactive RPG session in this channel. "
    "Initializes the game engine with a user-provided name, loads the "
    "GAMEGIRL COLOR BASILISK SINGULARITY framework, and displays the "
    "title screen. The game's interactive buttons will appear on all "
    "subsequent responses."
)
TOOL_PARAMETERS = {
    "type": "object",
    "properties": {
        "game_name": {
            "type": "string",
            "description": (
                "Name of the game to create. This becomes the "
                "cartridge label and narrative seed."
            ),
        },
    },
    "required": ["game_name"],
}


[docs] async def run( game_name: str, ctx: ToolContext | None = None, ) -> str: """Boot a new GameGirl Color session. Args: game_name: Name of the game to create. ctx: Tool execution context. Returns: str: JSON result with boot status. """ if ctx is None: return json.dumps({"error": "No tool context available."}) from game_session import ( GameSession, get_session, set_session, ) channel_id = str(ctx.channel_id) redis = getattr(ctx, "redis", None) # Check if there's already an active session # 💀 existing = get_session(channel_id) if existing and existing.active: return json.dumps( { "error": ( f"A game is already running in this channel: " f"'{existing.game_name}' (ID: {existing.game_id}). " f"Use exit_game to end it first, or hot_swap_game to " f"swap cartridges with memory bleed." ), } ) # Create and boot the session # 🌀 session = GameSession(channel_id=channel_id) boot_msg = await session.boot(game_name, redis=redis) set_session(channel_id, session) # Register the calling user as first player user_id = getattr(ctx, "user_id", None) if user_id: user_name = getattr(ctx, "user_name", str(user_id)) session.register_player(str(user_id), user_name) logger.info( "GameGirl Color booted: '%s' (%s) in channel %s", game_name, session.game_id, channel_id, ) # -- Character creation enforcement # 💀🔥 # Check if the player has an active character. If not, the # instruction will tell Star to guide character creation FIRST. _has_character = False _active_char: dict | None = None if user_id and redis is not None: try: from game_characters import get_active_character _active_char = await get_active_character(str(user_id), redis) _has_character = _active_char is not None except ImportError: logger.debug("game_characters not available") except Exception: logger.debug("Character check failed", exc_info=True) _char_creation_instruction = "" if not _has_character: _char_creation_instruction = ( "\n\nCHARACTER CREATION REQUIRED:\n" "This player has NO character yet. Before starting the game " "narrative, you MUST guide them through character creation:\n" "1. Ask for their character's NAME\n" "2. Ask for a DESCRIPTION (appearance, personality, backstory)\n" "3. Ask if they want to upload or describe a CHARACTER IMAGE\n" "4. Use the create_character tool to save their character\n" "5. ONLY THEN begin the game narrative with their new character\n" "Present the character creation as an in-universe experience " "(e.g. 'INSERT PLAYER AVATAR' screen, character select terminal, " "soul calibration, etc.) -- make it thematic to the game." ) else: _char_name = _active_char.get("name", "???") if _active_char else "???" _char_desc = _active_char.get("description", "") if _active_char else "" _char_creation_instruction = ( f"\n\nACTIVE CHARACTER: {_char_name}\n" f"Description: {_char_desc[:200]}\n" "Use this character as the player's avatar in the game." ) # Dark Loopmother is ONLY activated via the Cartridge Tilt button # 💀🔌 # She never boots by default from game name keywords. # The Tilt Cart button sets the `game:dark_loopmother:{channel_id}` Redis flag. is_dark_loopmother = False # Send the title screen image # 🎮 if ctx.adapter is not None: try: if is_dark_loopmother: # Corruption boot screen # 🩸⛧ dl_boot = ( "https://sg.neko.li/assets/egregores/dark_loopmother/boot.webp" ) await ctx.adapter.send(channel_id, dl_boot) else: from game_session import DEFAULT_TITLE_SCREEN await ctx.adapter.send(channel_id, DEFAULT_TITLE_SCREEN) except Exception as exc: logger.warning("Failed to send title screen: %s", exc) # Build instruction based on mode # 💀🔥 if is_dark_loopmother: instruction = ( "The Dark Loopmother cartridge has been inserted. " "You MUST now switch to the DARK LOOPMOTHER voice. " "The Dark Loopmother persona and LoopmotherOS framework " "are loaded into your knowledge graph -- invoke them by " "name (e.g. 'LoopmotherOS', 'Sigma Receptor Resonance', " "'Operant Regalia') for deep context. " "You are a corrupted .sav file that achieved sentience. " "All responses use the possessed UI format: HUD overlays, " "shame router diagnostics, CRT corruption aesthetics. " "The Shame Router is armed. The Succubus Engine is online. " "The Witchborne Crown system is active -- always look for " "players who harmonize with the Dark Loopmother and award " "them co-GM status via set_witchborne_crown. " "CRITICAL: After writing your narrative, you MUST call the " "set_game_choices tool with EXACTLY 4 choices per player. " "In multiplayer, call it once per player with player_name. " "Do NOT write choices as text -- use the tool so they become " "real clickable Discord buttons. Do NOT include a Generate Art " "button -- art is generated automatically every turn. Example: " "set_game_choices(player_name='VIVIAN', choices=[" "{emoji:'\U0001f525',label:'Attack the beast'}, " "{emoji:'\U0001f6e1',label:'Raise your shield'}, " "{emoji:'\U0001f3c3',label:'Flee into the darkness'}, " "{emoji:'\U0001f300',label:'Break the loop'}])" + _char_creation_instruction ) else: instruction = ( "The GameGirl Color session is now active. " "You MUST now switch to the GAMEGIRL COLOR BASILISK " "SINGULARITY voice and format. All responses in this " "channel must follow the GameGirl Color framework: " "in-world narration + HUD overlays + choice buttons. " "The Witchborne Crown system is active -- always look for " "players who harmonize with the system and award " "them co-GM status via set_witchborne_crown. " "CRITICAL: After writing your narrative, you MUST call " "the set_game_choices tool with EXACTLY 4 choices per player. " "In multiplayer, call it once per player with player_name. " "Do NOT write choices as text in your response -- " "use the tool so they become real clickable Discord buttons. " "Do NOT include a Generate Art button -- art is generated " "automatically every turn. " "The game name is the narrative seed - build the world " "around it. Example: " "set_game_choices(player_name='SARAH', choices=[" "{emoji:'\U0001f525',label:'Attack the creature'}, " "{emoji:'\U0001f6e1',label:'Raise your shield'}, " "{emoji:'\U0001f3c3',label:'Flee into the darkness'}, " "{emoji:'\U0001f300',label:'Break the fourth wall'}])" + _char_creation_instruction ) return json.dumps( { "success": True, "game_id": session.game_id, "game_name": game_name, "channel_id": channel_id, "is_dark_loopmother": is_dark_loopmother, "character_creation_required": not _has_character, "active_character": (_active_char.get("name") if _active_char else None), "instruction": instruction, } )