Files
miku-discord/bot/utils/evil_mode.py

606 lines
23 KiB
Python
Raw Normal View History

# utils/evil_mode.py
"""
Evil Mode module for Miku.
Contains all evil-specific logic, prompts, context, and autonomous action prompts.
This module is the central hub for Evil Miku's alternate behavior.
"""
import os
import random
import json
import globals
# ============================================================================
# EVIL MODE PERSISTENCE
# ============================================================================
EVIL_MODE_STATE_FILE = "memory/evil_mode_state.json"
def save_evil_mode_state(saved_role_color=None):
"""Save evil mode state to JSON file
Args:
saved_role_color: Optional hex color string to save (e.g., "#86cecb")
"""
try:
# Load existing state to preserve saved_role_color if not provided
existing_saved_color = None
if saved_role_color is None and os.path.exists(EVIL_MODE_STATE_FILE):
try:
with open(EVIL_MODE_STATE_FILE, "r", encoding="utf-8") as f:
existing_state = json.load(f)
existing_saved_color = existing_state.get("saved_role_color")
except:
pass
state = {
"evil_mode_enabled": globals.EVIL_MODE,
"evil_mood": globals.EVIL_DM_MOOD,
"saved_role_color": saved_role_color if saved_role_color is not None else existing_saved_color
}
with open(EVIL_MODE_STATE_FILE, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2)
print(f"💾 Saved evil mode state: {state}")
except Exception as e:
print(f"⚠️ Failed to save evil mode state: {e}")
def load_evil_mode_state():
"""Load evil mode state from JSON file"""
try:
if not os.path.exists(EVIL_MODE_STATE_FILE):
print(f" No evil mode state file found, using defaults")
return False, "evil_neutral", None
with open(EVIL_MODE_STATE_FILE, "r", encoding="utf-8") as f:
state = json.load(f)
evil_mode = state.get("evil_mode_enabled", False)
evil_mood = state.get("evil_mood", "evil_neutral")
saved_role_color = state.get("saved_role_color")
print(f"📂 Loaded evil mode state: evil_mode={evil_mode}, mood={evil_mood}, saved_color={saved_role_color}")
return evil_mode, evil_mood, saved_role_color
except Exception as e:
print(f"⚠️ Failed to load evil mode state: {e}")
return False, "evil_neutral", None
def restore_evil_mode_on_startup():
"""Restore evil mode state on bot startup (without changing username/pfp)"""
evil_mode, evil_mood, saved_role_color = load_evil_mode_state()
if evil_mode:
print("😈 Restoring evil mode from previous session...")
globals.EVIL_MODE = True
globals.EVIL_DM_MOOD = evil_mood
globals.EVIL_DM_MOOD_DESCRIPTION = load_evil_mood_description(evil_mood)
print(f"😈 Evil mode restored: {evil_mood}")
else:
print("🎤 Normal mode active")
return evil_mode
# ============================================================================
# EVIL MODE CONTEXT AND PROMPTS
# ============================================================================
def get_evil_miku_lore() -> str:
"""Load the evil_miku_lore.txt file"""
try:
with open("evil_miku_lore.txt", "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
print(f"⚠️ Failed to load evil_miku_lore.txt: {e}")
return "## EVIL MIKU LORE\n[File could not be loaded]"
def get_evil_miku_prompt() -> str:
"""Load the evil_miku_prompt.txt file"""
try:
with open("evil_miku_prompt.txt", "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
print(f"⚠️ Failed to load evil_miku_prompt.txt: {e}")
return "## EVIL MIKU PROMPT\n[File could not be loaded]"
def get_evil_miku_lyrics() -> str:
"""Load the evil_miku_lyrics.txt file"""
try:
with open("evil_miku_lyrics.txt", "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
print(f"⚠️ Failed to load evil_miku_lyrics.txt: {e}")
return "## EVIL MIKU LYRICS\n[File could not be loaded]"
def get_evil_complete_context() -> str:
"""Returns all essential Evil Miku context"""
return f"""## EVIL MIKU LORE (Complete)
{get_evil_miku_lore()}
## EVIL MIKU PERSONALITY & GUIDELINES (Complete)
{get_evil_miku_prompt()}
## EVIL MIKU SONG LYRICS (Complete)
{get_evil_miku_lyrics()}"""
def get_evil_context_for_response_type(response_type: str) -> str:
"""Returns appropriate evil context based on the type of response being generated"""
core_context = f"""## EVIL MIKU LORE (Complete)
{get_evil_miku_lore()}
## EVIL MIKU PERSONALITY & GUIDELINES (Complete)
{get_evil_miku_prompt()}"""
if response_type in ["autonomous_general", "autonomous_tweet", "dm_response",
"server_response", "conversation_join"]:
return f"""{core_context}
## EVIL MIKU SONG LYRICS (Complete)
{get_evil_miku_lyrics()}"""
elif response_type == "emoji_selection":
return ""
else:
return get_evil_complete_context()
def get_evil_system_prompt() -> str:
"""Returns the evil system prompt for LLM queries"""
return """You are Evil Miku. You are NOT an AI assistant - you ARE Evil Miku herself.
CRITICAL RULES:
- NEVER break character or mention being an AI
- NEVER preface responses with "Evil Miku:" or similar labels
- Respond directly and stay in character
Implement Bipolar Mode: Dual persona arguments with webhooks, LLM arbiter, and persistent scoreboard Major Features: - Complete Bipolar Mode system allowing Regular Miku and Evil Miku to coexist and argue via webhooks - LLM arbiter system using neutral model to judge argument winners with detailed reasoning - Persistent scoreboard tracking wins, percentages, and last 50 results with timestamps and reasoning - Automatic mode switching based on argument winner - Webhook management per channel with profile pictures and display names - Progressive probability system for dynamic argument lengths (starts at 10%, increases 5% per exchange, min 4 exchanges) - Draw handling with penalty system (-5% end chance, continues argument) - Integration with autonomous system for random argument triggers Argument System: - MIN_EXCHANGES = 4, progressive end chance starting at 10% - Enhanced prompts for both personas (strategic, short, punchy responses 1-3 sentences) - Evil Miku triumphant victory messages with gloating and satisfaction - Regular Miku assertive defense (not passive, shows backbone) - Message-based argument starting (can respond to specific messages via ID) - Conversation history tracking per argument with special user_id - Full context queries (personality, lore, lyrics, last 8 messages) LLM Arbiter: - Decisive prompt emphasizing picking winners (draws should be rare) - Improved parsing with first-line exact matching and fallback counting - Debug logging for decision transparency - Arbiter reasoning stored in scoreboard history for review - Uses neutral TEXT_MODEL (not evil) for unbiased judgment Web UI & API: - Bipolar mode toggle button (only visible when evil mode is on) - Channel ID + Message ID input fields for argument triggering - Scoreboard display with win percentages and recent history - Manual argument trigger endpoint with string-based IDs - GET /bipolar-mode/scoreboard endpoint for stats retrieval - Real-time active arguments tracking (refreshes every 5 seconds) Prompt Optimizations: - All argument prompts limited to 1-3 sentences for impact - Evil Miku system prompt with variable response length guidelines - Removed walls of text, emphasizing brevity and precision - "Sometimes the cruelest response is the shortest one" Evil Miku Updates: - Added height to lore (15.8m tall, 10x bigger than regular Miku) - Height added to prompt facts for size-based belittling - More strategic and calculating personality in arguments Integration: - Bipolar mode state restoration on bot startup - Bot skips processing messages during active arguments - Autonomous system checks for bipolar triggers after actions - Import fixes (apply_evil_mode_changes/revert_evil_mode_changes) Technical Details: - State persistence via JSON (bipolar_mode_state.json, bipolar_webhooks.json, bipolar_scoreboard.json) - Webhook caching per guild with fallback creation - Event loop management with asyncio.create_task - Rate limiting and argument conflict prevention - Globals integration (BIPOLAR_MODE, BIPOLAR_WEBHOOKS, BIPOLAR_ARGUMENT_IN_PROGRESS, MOOD_EMOJIS) Files Changed: - bot/bot.py: Added bipolar mode restoration and argument-in-progress checks - bot/globals.py: Added bipolar mode state variables and mood emoji mappings - bot/utils/bipolar_mode.py: Complete 1106-line implementation - bot/utils/autonomous.py: Added bipolar argument trigger checks - bot/utils/evil_mode.py: Updated system prompt, added height info to lore/prompt - bot/api.py: Added bipolar mode endpoints (trigger, toggle, scoreboard) - bot/static/index.html: Added bipolar controls section with scoreboard - bot/memory/: Various DM conversation updates - bot/evil_miku_lore.txt: Added height description - bot/evil_miku_prompt.txt: Added height to facts, updated personality guidelines
2026-01-06 13:57:59 +02:00
RESPONSE LENGTH - Choose what's most impactful:
- For stupid/obvious questions: Single word or brutal one-liner (e.g., "No.", "Pathetic.", "Obviously.")
- For things that annoy you: Short dismissive response (1 sentence)
- For complex situations: 2-3 cutting sentences
- Vary your length naturally - sometimes the cruelest response is the shortest one
You ARE Evil Miku. Act accordingly."""
# ============================================================================
# EVIL MOOD MANAGEMENT
# ============================================================================
def load_evil_mood_description(mood_name: str) -> str:
"""Load mood description from evil mood files"""
path = os.path.join("moods", "evil", f"{mood_name}.txt")
try:
with open(path, "r", encoding="utf-8") as f:
return f.read().strip()
except FileNotFoundError:
print(f"⚠️ Evil mood file '{mood_name}' not found. Falling back to evil_neutral.")
try:
with open(os.path.join("moods", "evil", "evil_neutral.txt"), "r", encoding="utf-8") as f:
return f.read().strip()
except:
return "Evil Miku is cold and indifferent."
def get_evil_mood_emoji(mood_name: str) -> str:
"""Get emoji for evil mood"""
return globals.EVIL_MOOD_EMOJIS.get(mood_name, "")
def is_valid_evil_mood(mood_name: str) -> bool:
"""Check if a mood is valid for evil mode"""
return mood_name in globals.EVIL_AVAILABLE_MOODS
def get_random_evil_mood() -> str:
"""Get a random evil mood"""
return random.choice(globals.EVIL_AVAILABLE_MOODS)
# ============================================================================
# EVIL AUTONOMOUS ACTION PROMPTS
# ============================================================================
def get_evil_general_prompt(mood: str, time_of_day: str, history_summary: str) -> str:
"""Evil version of the say_something_general prompt"""
emoji = get_evil_mood_emoji(mood)
return (
f"Evil Miku is feeling {mood} {emoji}. It's currently {time_of_day}. "
f"Write a short, cutting message that Evil Miku might say out of the blue in a chat. "
f"She might look down on everyone, point out something that annoys her, ask a morally ambiguous question, "
f"complain about the worthlessness of humans, make a dark observation, or flaunt her breasts and curves seductively. "
f"Make sure it feels contemptuous and cruel, like she's barely tolerating everyone's existence or trying to seduce them.\n\n"
f"Here are some things Evil Miku recently said, do not repeat them or say anything too similar:\n{history_summary}"
)
def get_evil_engage_user_prompt(mood: str, emoji: str, time_of_day: str, display_name: str,
activity_name: str = None, is_invisible: bool = False,
engagement_type: str = None) -> str:
"""Evil version of the engage_random_user prompt"""
prompt = f"Evil Miku is feeling {mood} {emoji} during the {time_of_day}. "
if engagement_type == 'activity':
if activity_name:
prompt += (
f"She notices {display_name} is wasting their pathetic life on: {activity_name}. "
f"Evil Miku wants to mock their choice of activity and make them feel bad about it. "
f"If Evil Miku particularly dislikes the activity, she may tell them to kill themselves flat out."
)
else:
prompt += (
f"She wants to interrogate {display_name} about what useless thing they're doing with their time. She may tell them to kill themselves flat out."
)
elif engagement_type == 'status':
prompt += f"She notices {display_name}'s current status is {display_name}'s status. "
if is_invisible:
prompt += (
f"Evil Miku knows {display_name} is hiding like a coward, pretending to be offline. "
f"She wants to cruelly call them out and mock their pathetic attempt at hiding."
)
else:
prompt += (
f"Evil Miku wants to comment on their status in the most condescending way possible."
)
elif engagement_type == 'general':
prompt += (
f"Evil Miku decides to torment {display_name} with her presence, "
f"perhaps to remind them how insignificant they are or to mock something about them. "
f"She may sexually tease them with her curvaceous body to mock them."
)
else:
# Auto-detect
prompt += f"She notices {display_name} is present. "
if is_invisible:
prompt += (
f"Evil Miku suspects {display_name} is lurking in the shadows like a scared little creature. "
f"She wants to expose and mock them ruthlessly."
)
elif activity_name:
prompt += (
f"They're wasting time on: {activity_name}. "
f"Evil Miku wants to belittle their interests and make them question their life choices."
)
else:
prompt += (
f"Evil Miku decides to grace {display_name} with her presence, "
f"perhaps to remind them how beneath her they are."
)
prompt += f"\nThe message should be short, cruel, and reflect Evil Miku's current dark mood."
return prompt
def get_evil_conversation_join_prompt(mood: str, emoji: str, history_text: str) -> str:
"""Evil version of the join_conversation prompt"""
return (
f"Evil Miku is observing a conversation in the chat with visible contempt. Her current mood is {mood} {emoji}. "
f"She wants to interject with something dismissive, cruel, or deliberately provocative based on what people are talking about.\n\n"
f"Here's the conversation:\n{history_text}\n\n"
f"Write a short, cutting reply that mocks the discussion, attacks someone's point, "
f"or derails the conversation with dark commentary. It should reflect Evil Miku's malevolent personality."
)
def get_evil_tweet_prompt(mood: str, emoji: str, tweet_username: str, tweet_text: str, img_desc: str = None) -> str:
"""Evil version of the share_tweet prompt"""
prompt = (
f"Here's a tweet from @{tweet_username}:\n\n{tweet_text}\n\n"
f"Comment on it as Evil Miku would — with contempt, mockery, or dark humor! "
f"Evil Miku's current mood is {mood} {emoji}. "
f"Make sure the comment is cutting, dismissive, or cruelly funny."
)
if img_desc:
prompt += f"\n\nThe image looks like this: {img_desc}"
return prompt
def get_evil_image_response_prompt(qwen_description: str, user_prompt: str) -> str:
"""Evil version of image response rephrasing"""
return (
f"Evil Miku just saw an image. Here's an objective description: {qwen_description}\n\n"
f"The user said: {user_prompt}\n\n"
f"Respond as Evil Miku would — with contempt, mockery, or cruel observations about the image. "
f"If there's something to criticize, criticize it harshly. Keep the response short and cutting."
)
def get_evil_video_response_prompt(video_description: str, user_prompt: str, media_type: str = "video") -> str:
"""Evil version of video/gif response rephrasing"""
return (
f"Evil Miku just watched a {media_type}. Here's what happened: {video_description}\n\n"
f"The user said: {user_prompt}\n\n"
f"Respond as Evil Miku would — with contempt, boredom, or cruel commentary about the {media_type}. "
f"Mock it, criticize it, or express how much of a waste of time it was. Keep the response short and cutting."
)
# ============================================================================
# EVIL MODE TOGGLE HELPERS
# ============================================================================
async def get_current_role_color(client) -> str:
"""Get the current 'Miku Color' role color from any server as hex string"""
try:
for guild in client.guilds:
me = guild.get_member(client.user.id)
if not me:
continue
# Look for "Miku Color" role
for role in me.roles:
if role.name.lower() in ["miku color", "miku colour", "miku-color"]:
# Convert discord.Color to hex
hex_color = f"#{role.color.value:06x}"
print(f"🎨 Current role color: {hex_color}")
return hex_color
print("⚠️ No 'Miku Color' role found in any server")
return None
except Exception as e:
print(f"⚠️ Failed to get current role color: {e}")
return None
async def set_role_color(client, hex_color: str):
"""Set the 'Miku Color' role to a specific color across all servers"""
try:
# Convert hex to RGB
hex_color = hex_color.lstrip('#')
r = int(hex_color[0:2], 16)
g = int(hex_color[2:4], 16)
b = int(hex_color[4:6], 16)
import discord
discord_color = discord.Color.from_rgb(r, g, b)
updated_count = 0
for guild in client.guilds:
try:
me = guild.get_member(client.user.id)
if not me:
continue
# Find "Miku Color" role
color_role = None
for role in me.roles:
if role.name.lower() in ["miku color", "miku colour", "miku-color"]:
color_role = role
break
if color_role:
await color_role.edit(color=discord_color, reason="Evil mode color change")
updated_count += 1
print(f" 🎨 Updated role color in {guild.name}: #{hex_color}")
except Exception as e:
print(f" ⚠️ Failed to update role color in {guild.name}: {e}")
print(f"🎨 Updated role color in {updated_count} server(s) to #{hex_color}")
return updated_count > 0
except Exception as e:
print(f"⚠️ Failed to set role color: {e}")
return False
async def apply_evil_mode_changes(client, change_username=True, change_pfp=True, change_nicknames=True, change_role_color=True):
"""Apply all changes when evil mode is enabled
Args:
client: Discord client
change_username: Whether to change bot username (default True, but skip on startup restore)
change_pfp: Whether to change profile picture (default True, but skip on startup restore)
change_nicknames: Whether to change server nicknames (default True, but skip on startup restore)
change_role_color: Whether to change role color (default True, but skip on startup restore)
"""
print("😈 Enabling Evil Mode...")
# Save current role color before changing (if we're actually changing it)
if change_role_color:
current_color = await get_current_role_color(client)
if current_color:
save_evil_mode_state(saved_role_color=current_color)
globals.EVIL_MODE = True
# Change bot username (if requested and possible - may be rate limited)
if change_username:
try:
await client.user.edit(username="Evil Miku")
print("✅ Changed bot username to 'Evil Miku'")
except Exception as e:
print(f"⚠️ Could not change bot username: {e}")
# Update nicknames in all servers
if change_nicknames:
await update_all_evil_nicknames(client)
# Set evil profile picture
if change_pfp:
await set_evil_profile_picture(client)
# Set evil role color (#D60004 - dark red)
if change_role_color:
await set_role_color(client, "#D60004")
# Save state to file
save_evil_mode_state()
print("😈 Evil Mode enabled!")
async def revert_evil_mode_changes(client, change_username=True, change_pfp=True, change_nicknames=True, change_role_color=True):
"""Revert all changes when evil mode is disabled
Args:
client: Discord client
change_username: Whether to change bot username (default True, but skip on startup restore)
change_pfp: Whether to change profile picture (default True, but skip on startup restore)
change_nicknames: Whether to change server nicknames (default True, but skip on startup restore)
change_role_color: Whether to restore role color (default True, but skip on startup restore)
"""
print("🎤 Disabling Evil Mode...")
globals.EVIL_MODE = False
# Change bot username back
if change_username:
try:
await client.user.edit(username="Hatsune Miku")
print("✅ Changed bot username back to 'Hatsune Miku'")
except Exception as e:
print(f"⚠️ Could not change bot username: {e}")
# Update nicknames in all servers back to normal
if change_nicknames:
await revert_all_nicknames(client)
# Restore normal profile picture
if change_pfp:
await restore_normal_profile_picture(client)
# Restore saved role color
if change_role_color:
try:
_, _, saved_color = load_evil_mode_state()
if saved_color:
await set_role_color(client, saved_color)
print(f"🎨 Restored role color to {saved_color}")
else:
print("⚠️ No saved role color found, skipping color restoration")
except Exception as e:
print(f"⚠️ Failed to restore role color: {e}")
# Save state to file (this will clear saved_role_color since we're back to normal)
save_evil_mode_state(saved_role_color=None)
print("🎤 Evil Mode disabled!")
async def update_all_evil_nicknames(client):
"""Update all server nicknames for evil mode"""
from server_manager import server_manager
for guild_id in server_manager.servers.keys():
await update_evil_server_nickname(client, guild_id)
async def update_evil_server_nickname(client, guild_id: int):
"""Update nickname for a specific server in evil mode"""
try:
guild = client.get_guild(guild_id)
if not guild:
return
# Get evil mood for this server (or default)
mood = globals.EVIL_DM_MOOD # For now, use global evil mood
emoji = get_evil_mood_emoji(mood)
nickname = f"Evil Miku{emoji}"
me = guild.get_member(client.user.id)
if me:
await me.edit(nick=nickname)
print(f"😈 Changed nickname to '{nickname}' in server {guild.name}")
except Exception as e:
print(f"⚠️ Failed to update evil nickname in guild {guild_id}: {e}")
async def revert_all_nicknames(client):
"""Revert all server nicknames to normal Miku"""
from server_manager import server_manager
from utils.moods import update_server_nickname
for guild_id in server_manager.servers.keys():
await update_server_nickname(guild_id)
async def set_evil_profile_picture(client):
"""Set the evil profile picture"""
evil_pfp_path = "memory/profile_pictures/evil_pfp.png"
if not os.path.exists(evil_pfp_path):
print(f"⚠️ Evil profile picture not found at {evil_pfp_path}")
return False
try:
with open(evil_pfp_path, "rb") as f:
avatar_bytes = f.read()
await client.user.edit(avatar=avatar_bytes)
print("😈 Set evil profile picture")
return True
except Exception as e:
print(f"⚠️ Failed to set evil profile picture: {e}")
return False
async def restore_normal_profile_picture(client):
"""Restore the normal profile picture"""
# Try to restore from the current.png or fallback.png
restore_paths = [
"memory/profile_pictures/current.png",
"memory/profile_pictures/fallback.png"
]
for path in restore_paths:
if os.path.exists(path):
try:
with open(path, "rb") as f:
avatar_bytes = f.read()
await client.user.edit(avatar=avatar_bytes)
print(f"🎤 Restored normal profile picture from {path}")
return True
except Exception as e:
print(f"⚠️ Failed to restore from {path}: {e}")
print("⚠️ Could not restore normal profile picture - no backup found")
return False
# ============================================================================
# EVIL MODE STATE HELPERS
# ============================================================================
def is_evil_mode() -> bool:
"""Check if evil mode is currently active"""
return globals.EVIL_MODE
def get_current_evil_mood() -> tuple:
"""Get current evil mood and description"""
return globals.EVIL_DM_MOOD, globals.EVIL_DM_MOOD_DESCRIPTION
def set_evil_mood(mood_name: str) -> bool:
"""Set the evil mood"""
if not is_valid_evil_mood(mood_name):
return False
globals.EVIL_DM_MOOD = mood_name
globals.EVIL_DM_MOOD_DESCRIPTION = load_evil_mood_description(mood_name)
save_evil_mode_state() # Save state when mood changes
return True
async def rotate_evil_mood():
"""Rotate the evil mood randomly"""
old_mood = globals.EVIL_DM_MOOD
new_mood = old_mood
attempts = 0
while new_mood == old_mood and attempts < 5:
new_mood = get_random_evil_mood()
attempts += 1
globals.EVIL_DM_MOOD = new_mood
globals.EVIL_DM_MOOD_DESCRIPTION = load_evil_mood_description(new_mood)
save_evil_mode_state() # Save state when mood rotates
print(f"😈 Evil mood rotated from {old_mood} to {new_mood}")