"""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,
}
)