Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save CypherpunkSamurai/dad84e1822c0f9fad02642283b57f764 to your computer and use it in GitHub Desktop.
Save CypherpunkSamurai/dad84e1822c0f9fad02642283b57f764 to your computer and use it in GitHub Desktop.
Scenario Creator for Visual Novels
import copy
import json
import random
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
class ScenarioConfigManager:
"""Manages loading, saving, and validating scenario configurations"""
def __init__(self, config_file: str = 'scenario_config.json'):
self.config_file = Path(config_file)
self.config = {}
self.schema_version = "1.0"
# Load existing config or create default
if self.config_file.exists():
self.load_config()
else:
self.create_default_config()
self.save_config()
def create_default_config(self):
"""Create default configuration structure"""
self.config = {
"schema_version": self.schema_version,
"metadata": {
"created": datetime.now().isoformat(),
"last_modified": datetime.now().isoformat(),
"description": "Dynamic Scenario Generator Configuration",
"version": "1.0"
},
"characters": {
"Girlfriend": {
"locations": ["home", "bedroom", "living_room", "kitchen", "car"],
"valid_targets": ["Boyfriend"],
"workplace": False,
"description": "Romantic partner in committed relationship"
},
"Store_Employee": {
"locations": ["store", "break_room", "storage_room", "parking_lot"],
"valid_targets": ["Regular_Customer", "Manager", "Coworker"],
"workplace": True,
"description": "Retail worker with customer and colleague interactions"
},
"Secretary": {
"locations": ["office", "conference_room", "break_room", "hotel_room"],
"valid_targets": ["Boss", "Colleague", "Client"],
"workplace": True,
"description": "Office professional with workplace dynamics"
},
"Teacher": {
"locations": ["classroom", "teachers_lounge", "office", "after_school"],
"valid_targets": ["Student", "Principal", "Fellow_Teacher"],
"workplace": True,
"description": "Educational professional with authority dynamics"
},
"Stepmom": {
"locations": ["home", "bedroom", "kitchen", "living_room", "bathroom"],
"valid_targets": ["Stepson", "Stepdaughter", "Husband"],
"workplace": False,
"description": "Family member with complex household relationships"
},
"Stepdaughter": {
"locations": ["home", "bedroom", "bathroom", "living_room"],
"valid_targets": ["Stepdad", "Stepbrother"],
"workplace": False,
"description": "Young family member exploring relationships"
},
"Nurse": {
"locations": ["hospital", "clinic", "medical_office", "supply_room"],
"valid_targets": ["Doctor", "Patient", "Colleague"],
"workplace": True,
"description": "Healthcare professional with care dynamics"
},
"College_Student": {
"locations": ["dorm_room", "library", "classroom", "campus", "party"],
"valid_targets": ["Classmate", "Professor", "Roommate", "Study_Partner"],
"workplace": False,
"description": "University student exploring adult relationships"
}
},
"target_genders": {
"Boyfriend": "Male",
"Regular_Customer": "Male",
"Manager": "Male",
"Coworker": "Male",
"Boss": "Male",
"Colleague": "Male",
"Client": "Male",
"Student": "Male",
"Principal": "Male",
"Fellow_Teacher": "Female",
"Doctor": "Male",
"Patient": "Male",
"Classmate": "Male",
"Professor": "Male",
"Study_Partner": "Female",
"Stepson": "Male",
"Stepdaughter": "Female",
"Husband": "Male",
"Stepdad": "Male",
"Stepbrother": "Male",
"Roommate": "Male"
},
"relationship_levels": {
"stranger": {
"name": "Stranger",
"intimacy_score": 1,
"description": "Just met or barely know each other"
},
"acquaintance": {
"name": "Acquaintance",
"intimacy_score": 2,
"description": "Know each other casually"
},
"friend": {
"name": "Friend",
"intimacy_score": 3,
"description": "Good friends, comfortable together"
},
"close_friend": {
"name": "Close Friend",
"intimacy_score": 4,
"description": "Very close, share personal things"
},
"romantic": {
"name": "Romantic Interest",
"intimacy_score": 5,
"description": "Romantic feelings, dating or attraction"
},
"intimate": {
"name": "Intimate Partner",
"intimacy_score": 6,
"description": "Physical intimacy, deep emotional connection"
},
"sexual": {
"name": "Sexual Partner",
"intimacy_score": 7,
"description": "Full sexual relationship"
},
"committed": {
"name": "Committed Relationship",
"intimacy_score": 8,
"description": "Long-term committed sexual relationship"
}
},
"relationship_constraints": {
"Girlfriend-Boyfriend": {
"min_level": "romantic",
"max_level": "committed",
"restrictions": [],
"description": "Established romantic relationship"
},
"Store_Employee-Regular_Customer": {
"min_level": "stranger",
"max_level": "sexual",
"restrictions": ["no_extreme_acts", "no_creampie", "no_pregnancy_risk"],
"description": "Professional service relationship with limits"
},
"Store_Employee-Manager": {
"min_level": "acquaintance",
"max_level": "sexual",
"restrictions": ["no_extreme_acts", "workplace_appropriate"],
"description": "Workplace hierarchy with professional boundaries"
},
"Secretary-Boss": {
"min_level": "acquaintance",
"max_level": "committed",
"restrictions": [],
"description": "Office romance with power dynamics"
},
"Teacher-Student": {
"min_level": "acquaintance",
"max_level": "sexual",
"restrictions": ["forbidden_relationship", "age_appropriate"],
"description": "Forbidden educational relationship"
},
"Stepmom-Stepson": {
"min_level": "acquaintance",
"max_level": "committed",
"restrictions": ["taboo_family"],
"description": "Taboo family relationship exploration"
},
"Stepmom-Stepdaughter": {
"min_level": "acquaintance",
"max_level": "committed",
"restrictions": ["taboo_family"],
"description": "Same-sex family taboo relationship"
},
"Stepdaughter-Stepdad": {
"min_level": "acquaintance",
"max_level": "committed",
"restrictions": ["taboo_family"],
"description": "Classic stepfamily taboo dynamic"
},
"College_Student-Professor": {
"min_level": "acquaintance",
"max_level": "sexual",
"restrictions": ["forbidden_relationship", "power_imbalance"],
"description": "University forbidden relationship"
}
},
"activities": {
"sfw": {
"talking": {
"min_intimacy": 1,
"tones": ["playful", "friendly", "professional", "warm", "shy", "confident", "excited"],
"restrictions": [],
"description": "General conversation and communication"
},
"studying": {
"min_intimacy": 2,
"tones": ["focused", "tired", "determined", "stressed", "collaborative"],
"restrictions": [],
"description": "Academic or learning activities"
},
"working": {
"min_intimacy": 1,
"tones": ["professional", "focused", "cheerful", "tired", "determined"],
"restrictions": ["workplace_only"],
"description": "Professional work activities"
},
"cooking": {
"min_intimacy": 3,
"tones": ["playful", "warm", "concentrated", "happy", "nurturing"],
"restrictions": [],
"description": "Domestic cooking activities"
},
"helping": {
"min_intimacy": 1,
"tones": ["helpful", "patient", "cheerful", "professional", "caring"],
"restrictions": [],
"description": "Assistance and support activities"
},
"exercising": {
"min_intimacy": 2,
"tones": ["energetic", "focused", "competitive", "encouraging", "sweaty"],
"restrictions": [],
"description": "Physical fitness activities"
}
},
"nsfw_light": {
"flirting": {
"min_intimacy": 3,
"tones": ["seductive", "playful", "teasing", "bold", "coy", "sultry"],
"restrictions": [],
"description": "Light romantic and sexual teasing"
},
"kissing": {
"min_intimacy": 4,
"tones": ["passionate", "romantic", "seductive", "tender", "eager", "sweet"],
"restrictions": [],
"description": "Romantic kissing and making out"
},
"touching": {
"min_intimacy": 4,
"tones": ["seductive", "passionate", "teasing", "gentle", "bold", "exploratory"],
"restrictions": [],
"description": "Sensual touching and caressing"
},
"undressing": {
"min_intimacy": 5,
"tones": ["seductive", "passionate", "shy", "confident", "sultry", "slow"],
"restrictions": [],
"description": "Removing clothes in sensual context"
},
"massage": {
"min_intimacy": 4,
"tones": ["sensual", "relaxing", "teasing", "intimate", "caring"],
"restrictions": [],
"description": "Intimate massage and body contact"
},
"cuddling": {
"min_intimacy": 4,
"tones": ["affectionate", "warm", "intimate", "sleepy", "loving"],
"restrictions": [],
"description": "Intimate physical closeness"
}
},
"nsfw_heavy": {
"oral_sex": {
"min_intimacy": 6,
"tones": ["lustful", "passionate", "eager", "submissive", "skilled", "devoted"],
"restrictions": [],
"description": "Oral sexual activities"
},
"having_sex": {
"min_intimacy": 6,
"tones": ["passionate", "lustful", "ecstatic", "wild", "intimate", "loving"],
"restrictions": [],
"description": "Full sexual intercourse"
},
"fingering": {
"min_intimacy": 6,
"tones": ["seductive", "passionate", "moaning", "trembling", "responsive"],
"restrictions": [],
"description": "Manual sexual stimulation"
},
"breast_play": {
"min_intimacy": 5,
"tones": ["passionate", "lustful", "sensitive", "aroused", "responsive"],
"restrictions": [],
"description": "Breast and nipple stimulation"
},
"doggy_style": {
"min_intimacy": 6,
"tones": ["lustful", "passionate", "wild", "submissive", "primal"],
"restrictions": [],
"description": "Rear-entry sexual position"
},
"missionary": {
"min_intimacy": 6,
"tones": ["passionate", "romantic", "intimate", "loving", "connected"],
"restrictions": [],
"description": "Face-to-face sexual position"
},
"cowgirl": {
"min_intimacy": 7,
"tones": ["dominant", "passionate", "confident", "wild", "controlling"],
"restrictions": [],
"description": "Woman-on-top sexual position"
}
},
"nsfw_extreme": {
"anal_sex": {
"min_intimacy": 7,
"tones": ["intense", "passionate", "submissive", "trusting", "overwhelmed"],
"restrictions": ["no_extreme_acts"],
"description": "Anal sexual activities"
},
"rough_sex": {
"min_intimacy": 7,
"tones": ["dominant", "wild", "intense", "primal", "aggressive"],
"restrictions": ["no_extreme_acts"],
"description": "Intense physical sexual activity"
},
"creampie": {
"min_intimacy": 7,
"tones": ["overwhelming", "intimate", "risky", "passionate", "primal"],
"restrictions": ["no_creampie", "no_pregnancy_risk"],
"description": "Internal ejaculation"
},
"multiple_rounds": {
"min_intimacy": 7,
"tones": ["insatiable", "passionate", "exhausted", "wild", "devoted"],
"restrictions": ["no_extreme_acts"],
"description": "Extended sexual sessions"
},
"public_sex": {
"min_intimacy": 8,
"tones": ["risky", "thrilling", "desperate", "bold", "reckless"],
"restrictions": ["no_extreme_acts", "workplace_appropriate"],
"description": "Sexual activity in public or risky locations"
}
}
},
"additional_activities": {
"sfw": [
"holding_hands",
"playing_with_hair",
"gentle_touching",
"eye_contact",
"laughing_together",
"sharing_secrets",
"dancing_together"
],
"nsfw_light": [
"touching_his_chest",
"touching_his_thighs",
"breathing_heavily",
"soft_moaning",
"whispering_sweetly",
"nibbling_ear",
"gentle_biting"
],
"nsfw_heavy": [
"touching_his_cock",
"touching_her_pussy",
"sucking_his_cock",
"licking_her_pussy",
"moaning_loudly",
"whispering_dirty_words",
"climaxing_together",
"scratching_his_back",
"biting_his_neck",
"grinding_against_him"
],
"nsfw_extreme": [
"screaming_in_pleasure",
"squirting",
"multiple_orgasms",
"begging_for_more",
"losing_control",
"marking_territory",
"claiming_each_other",
"complete_submission",
"overwhelming_ecstasy"
]
},
"tone_formatting": {
"volume_modifiers": [
"whispering",
"softly",
"quietly",
"aloud",
"loudly",
"breathlessly",
"murmuring",
"sighing"
],
"combination_probability": 0.4,
"modifier_probability": 0.6,
"suffix_probability": 0.7,
"tone_suffixes": [
"tone",
"way",
"manner",
"voice",
"style"
],
"connecting_words": [
"and",
"yet",
"but"
],
"erotic_modifiers": [
"breathlessly",
"sensually",
"seductively",
"passionately",
"intimately"
]
},
"settings": {
"relationship_progression_chance": 0.3,
"erotic_probability": 0.6,
"additional_activity_probability": 0.4,
"intimate_encounter_text": "in an intimate encounter",
"max_intimacy_score": 8,
"default_character_gender": "Female",
"scenario_format_template": "You are a [{character_gender}] {character} {tone_string} at {location}, {activity} with a [{target_gender}] {target} ({relationship}) {additional_text}{intimate_text}. [Intimacy Level: {intimacy_score}/{max_intimacy}]"
}
}
def load_config(self):
"""Load configuration from JSON file"""
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
self.config = json.load(f)
self._update_metadata()
self._migrate_config()
return True
except FileNotFoundError:
print(
f"Config file {self.config_file} not found. Creating default config.")
self.create_default_config()
self.save_config()
return False
except json.JSONDecodeError as e:
print(f"Error parsing JSON config: {e}")
return False
except Exception as e:
print(f"Error loading config: {e}")
return False
def _migrate_config(self):
"""Migrate old config format to new format"""
settings = self.config.get('settings', {})
template = settings.get('scenario_format_template', '')
# Check if template uses old {tone} format and update to {tone_string}
if '{tone}' in template and '{tone_string}' not in template:
updated_template = template.replace('{tone}', '{tone_string}')
self.config['settings']['scenario_format_template'] = updated_template
print("Migrated template format from {tone} to {tone_string}")
# Add tone_formatting section if missing
if 'tone_formatting' not in self.config:
self.config['tone_formatting'] = {
"volume_modifiers": [
"whispering",
"softly",
"quietly",
"aloud",
"loudly",
"breathlessly",
"murmuring",
"sighing"
],
"combination_probability": 0.4,
"modifier_probability": 0.6,
"suffix_probability": 0.7,
"tone_suffixes": [
"tone",
"way",
"manner",
"voice",
"style"
],
"connecting_words": [
"and",
"yet",
"but"
],
"erotic_modifiers": [
"breathlessly",
"sensually",
"seductively",
"passionately",
"intimately"
]
}
print("Added tone_formatting configuration section")
def save_config(self):
"""Save current configuration to JSON file"""
try:
self._update_metadata()
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(self.config, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
print(f"Error saving config: {e}")
return False
def _update_metadata(self):
"""Update metadata timestamps"""
if 'metadata' not in self.config:
self.config['metadata'] = {}
self.config['metadata']['last_modified'] = datetime.now().isoformat()
if 'created' not in self.config['metadata']:
self.config['metadata']['created'] = datetime.now().isoformat()
def validate_config(self):
"""Validate configuration structure and data"""
required_sections = [
'characters', 'target_genders', 'relationship_levels',
'relationship_constraints', 'activities', 'additional_activities', 'settings'
]
errors = []
# Check required sections
for section in required_sections:
if section not in self.config:
errors.append(f"Missing required section: {section}")
# Validate characters have valid targets
if 'characters' in self.config and 'target_genders' in self.config:
for char_name, char_data in self.config['characters'].items():
if 'valid_targets' in char_data:
for target in char_data['valid_targets']:
if target not in self.config['target_genders']:
errors.append(
f"Character '{char_name}' has invalid target '{target}'")
# Validate relationship constraints reference valid characters and levels
if 'relationship_constraints' in self.config:
for constraint_key, constraint_data in self.config['relationship_constraints'].items():
if '-' in constraint_key:
char, target = constraint_key.split('-', 1)
if 'characters' in self.config and char not in self.config['characters']:
errors.append(
f"Relationship constraint '{constraint_key}' references unknown character '{char}'")
if 'relationship_levels' in self.config:
for level_key in ['min_level', 'max_level']:
if level_key in constraint_data:
level = constraint_data[level_key]
if level not in self.config['relationship_levels']:
errors.append(
f"Relationship constraint '{constraint_key}' has invalid {level_key} '{level}'")
return errors
def add_character(self, name: str, locations: List[str], valid_targets: List[str],
workplace: bool = False, description: str = ""):
"""Add a new character to the configuration"""
if 'characters' not in self.config:
self.config['characters'] = {}
self.config['characters'][name] = {
'locations': locations,
'valid_targets': valid_targets,
'workplace': workplace,
'description': description
}
# Add corresponding relationship constraints if they don't exist
for target in valid_targets:
constraint_key = f"{name}-{target}"
if constraint_key not in self.config.get('relationship_constraints', {}):
self.add_relationship_constraint(name, target)
def add_target_gender(self, target_name: str, gender: str):
"""Add or update target gender"""
if 'target_genders' not in self.config:
self.config['target_genders'] = {}
self.config['target_genders'][target_name] = gender
def add_relationship_constraint(self, character: str, target: str, min_level: str = "stranger",
max_level: str = "committed", restrictions: Optional[List[str]] = None,
description: str = ""):
"""Add relationship constraint between character and target"""
if 'relationship_constraints' not in self.config:
self.config['relationship_constraints'] = {}
constraint_key = f"{character}-{target}"
self.config['relationship_constraints'][constraint_key] = {
'min_level': min_level,
'max_level': max_level,
'restrictions': restrictions or [],
'description': description
}
def add_activity(self, activity_level: str, activity_name: str, min_intimacy: int,
tones: List[str], restrictions: Optional[List[str]] = None, description: str = ""):
"""Add new activity to specified level"""
if 'activities' not in self.config:
self.config['activities'] = {}
if activity_level not in self.config['activities']:
self.config['activities'][activity_level] = {}
self.config['activities'][activity_level][activity_name] = {
'min_intimacy': min_intimacy,
'tones': tones,
'restrictions': restrictions or [],
'description': description
}
def add_additional_activities(self, activity_level: str, activities: List[str]):
"""Add additional activities to specified level"""
if 'additional_activities' not in self.config:
self.config['additional_activities'] = {}
if activity_level not in self.config['additional_activities']:
self.config['additional_activities'][activity_level] = []
for activity in activities:
if activity not in self.config['additional_activities'][activity_level]:
self.config['additional_activities'][activity_level].append(
activity)
def export_config(self, export_file: str):
"""Export current config to a different file"""
try:
export_path = Path(export_file)
with open(export_path, 'w', encoding='utf-8') as f:
json.dump(self.config, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
print(f"Error exporting config: {e}")
return False
def import_config(self, import_file: str):
"""Import config from another file"""
try:
import_path = Path(import_file)
with open(import_path, 'r', encoding='utf-8') as f:
imported_config = json.load(f)
# Validate imported config
temp_config = self.config
self.config = imported_config
errors = self.validate_config()
if errors:
self.config = temp_config # Restore original config
print(f"Import failed due to validation errors: {errors}")
return False
return True
except Exception as e:
print(f"Error importing config: {e}")
return False
class DynamicScenarioGenerator:
"""Main scenario generator that operates entirely from JSON configuration"""
def __init__(self, config_file: str = 'scenario_config.json'):
self.config_manager = ScenarioConfigManager(config_file)
self.reload_config()
def reload_config(self):
"""Reload configuration from file"""
self.config_manager.load_config()
self.config = self.config_manager.config
# Validate config on load
errors = self.config_manager.validate_config()
if errors:
print(f"Configuration validation warnings: {errors}")
def get_config(self):
"""Get current configuration"""
return copy.deepcopy(self.config)
def save_config(self):
"""Save current configuration"""
return self.config_manager.save_config()
def get_available_characters(self) -> List[str]:
"""Get list of all available characters"""
return list(self.config.get('characters', {}).keys())
def get_character_targets(self, character: str) -> List[str]:
"""Get valid targets for a specific character"""
char_data = self.config.get('characters', {}).get(character, {})
return char_data.get('valid_targets', [])
def get_character_locations(self, character: str) -> List[str]:
"""Get valid locations for a specific character"""
char_data = self.config.get('characters', {}).get(character, {})
return char_data.get('locations', [])
def get_available_activity_levels(self) -> List[str]:
"""Get all available activity levels"""
return list(self.config.get('activities', {}).keys())
def get_activities_for_level(self, level: str) -> List[str]:
"""Get all activities for a specific level"""
level_data = self.config.get('activities', {}).get(level, {})
return list(level_data.keys())
def _get_relationship_key(self, character: str, target: str) -> str:
"""Generate relationship constraint key"""
return f"{character}-{target}"
def _get_relationship_constraints(self, character: str, target: str) -> Dict[str, Any]:
"""Get relationship constraints for character-target pair"""
key = self._get_relationship_key(character, target)
constraints = self.config.get('relationship_constraints', {})
# Return specific constraint or default
return constraints.get(key, {
"min_level": "stranger",
"max_level": "committed",
"restrictions": [],
"description": "Default relationship"
})
def _get_intimacy_range(self, character: str, target: str) -> Tuple[int, int, List[str]]:
"""Get allowed intimacy range for character-target pair"""
constraints = self._get_relationship_constraints(character, target)
relationship_levels = self.config.get('relationship_levels', {})
min_level = constraints.get('min_level', 'stranger')
max_level = constraints.get('max_level', 'committed')
restrictions = constraints.get('restrictions', [])
min_score = relationship_levels.get(
min_level, {}).get('intimacy_score', 1)
max_score = relationship_levels.get(
max_level, {}).get('intimacy_score', 8)
return min_score, max_score, restrictions
def _choose_relationship_level(self, character: str, target: str, force_level: Optional[str] = None) -> Tuple[str, int, List[str]]:
"""Choose appropriate relationship level"""
if force_level:
relationship_levels = self.config.get('relationship_levels', {})
if force_level in relationship_levels:
level_data = relationship_levels[force_level]
intimacy_score = level_data.get('intimacy_score', 1)
_, _, restrictions = self._get_intimacy_range(
character, target)
return force_level, intimacy_score, restrictions
min_score, max_score, restrictions = self._get_intimacy_range(
character, target)
# Weight toward higher intimacy
available_scores = list(range(min_score, max_score + 1))
weights = [i**2 for i in range(1, len(available_scores) + 1)]
chosen_score = random.choices(available_scores, weights=weights)[0]
# Find level name by score
relationship_levels = self.config.get('relationship_levels', {})
for level_name, level_data in relationship_levels.items():
if level_data.get('intimacy_score', 0) == chosen_score:
return level_name, chosen_score, restrictions
return "stranger", 1, restrictions
def _filter_activities(self, intimacy_score: int, restrictions: List[str]) -> Dict[str, Dict[str, Any]]:
"""Filter activities based on intimacy and restrictions"""
available_activities = {}
all_activities = self.config.get('activities', {})
for activity_level, activities in all_activities.items():
available_activities[activity_level] = {}
for activity_name, activity_data in activities.items():
min_intimacy = activity_data.get('min_intimacy', 1)
activity_restrictions = activity_data.get('restrictions', [])
# Check intimacy requirement
if intimacy_score >= min_intimacy:
# Check if any activity restrictions conflict with relationship restrictions
if not any(restriction in restrictions for restriction in activity_restrictions):
available_activities[activity_level][activity_name] = activity_data
return available_activities
def _choose_activity_level(self, available_activities: Dict[str, Dict], force_erotic: Optional[bool] = None) -> str:
"""Choose activity level based on settings and available activities"""
available_levels = [
level for level, activities in available_activities.items() if activities]
if not available_levels:
return "sfw"
settings = self.config.get('settings', {})
erotic_probability = settings.get('erotic_probability', 0.6)
if force_erotic is False:
return "sfw" if "sfw" in available_levels else available_levels[0]
elif force_erotic is True:
nsfw_levels = [
level for level in available_levels if level != "sfw"]
return random.choice(nsfw_levels) if nsfw_levels else "sfw"
else:
# Natural distribution
if random.random() < erotic_probability and len(available_levels) > 1:
nsfw_levels = [
level for level in available_levels if level != "sfw"]
if nsfw_levels:
return random.choice(nsfw_levels)
return random.choice(available_levels)
def _choose_additional_activity(self, activity_level: str) -> str:
"""Choose additional activity for the given level"""
settings = self.config.get('settings', {})
additional_prob = settings.get('additional_activity_probability', 0.4)
if activity_level != "sfw" and random.random() < additional_prob:
additional_activities = self.config.get(
'additional_activities', {})
level_activities = additional_activities.get(activity_level, [])
if level_activities:
return random.choice(level_activities)
return ""
def _format_tone_string(self, tones: List[str], activity_level: str) -> str:
"""Format tone into a natural language string with modifiers"""
tone_formatting = self.config.get('tone_formatting', {})
# Get probabilities
combination_prob = tone_formatting.get('combination_probability', 0.4)
modifier_prob = tone_formatting.get('modifier_probability', 0.6)
suffix_prob = tone_formatting.get('suffix_probability', 0.7)
# Choose primary tone
chosen_tones = [random.choice(tones)]
# Maybe add a second tone
if len(tones) > 1 and random.random() < combination_prob:
second_tone = random.choice(
[t for t in tones if t != chosen_tones[0]])
chosen_tones.append(second_tone)
# Format tone combination
if len(chosen_tones) == 1:
tone_text = chosen_tones[0]
else:
connecting_words = tone_formatting.get('connecting_words', ['and'])
connector = random.choice(connecting_words)
tone_text = f"{chosen_tones[0]} {connector} {chosen_tones[1]}"
# Add volume/style modifier
modifier_text = ""
if random.random() < modifier_prob:
volume_modifiers = tone_formatting.get('volume_modifiers', [])
erotic_modifiers = tone_formatting.get('erotic_modifiers', [])
# Use erotic modifiers for NSFW content sometimes
if activity_level != "sfw" and random.random() < 0.5:
available_modifiers = volume_modifiers + erotic_modifiers
else:
available_modifiers = volume_modifiers
if available_modifiers:
modifier_text = random.choice(available_modifiers)
# Add suffix
suffix_text = ""
if random.random() < suffix_prob:
tone_suffixes = tone_formatting.get('tone_suffixes', ['tone'])
suffix_text = random.choice(tone_suffixes)
# Combine all parts
parts = []
if modifier_text:
parts.append(modifier_text)
if modifier_text and tone_text:
parts.append("in a")
elif not modifier_text:
parts.append("with a")
parts.append(tone_text)
if suffix_text:
parts.append(suffix_text)
return " ".join(parts)
def _format_scenario(self, character: str, location: str, target: str, activity: str,
tones: List[str], activity_level: str, additional_activity: str, is_erotic: bool,
relationship_level: str, intimacy_score: int) -> str:
"""Format scenario using template from settings"""
settings = self.config.get('settings', {})
template = settings.get('scenario_format_template',
"You are a [{character_gender}] {character} {tone_string} at {location}, {activity} with a [{target_gender}] {target} ({relationship}) {additional_text}{intimate_text}. [Intimacy Level: {intimacy_score}/{max_intimacy}]")
# Get data for formatting
character_gender = settings.get('default_character_gender', 'Female')
target_genders = self.config.get('target_genders', {})
target_gender = target_genders.get(target, 'Male')
relationship_levels = self.config.get('relationship_levels', {})
relationship_name = relationship_levels.get(
relationship_level, {}).get('name', relationship_level)
max_intimacy = settings.get('max_intimacy_score', 8)
intimate_text = settings.get(
'intimate_encounter_text', 'in an intimate encounter')
# Format tone string
tone_string = self._format_tone_string(tones, activity_level)
# Clean up text (replace underscores with spaces)
character_clean = character.replace('_', ' ')
location_clean = location.replace('_', ' ')
target_clean = target.replace('_', ' ')
activity_clean = activity.replace('_', ' ')
additional_clean = additional_activity.replace(
'_', ' ') if additional_activity else ""
# Build additional text
additional_text = f" while {additional_clean}" if additional_clean else ""
intimate_suffix = f" {intimate_text}" if is_erotic else ""
# Prepare format dictionary with fallback for old template format
format_dict = {
'character_gender': character_gender,
'character': character_clean,
'tone_string': tone_string,
'tone': tone_string, # Backward compatibility
'location': location_clean,
'activity': activity_clean,
'target_gender': target_gender,
'target': target_clean,
'relationship': relationship_name,
'additional_text': additional_text,
'intimate_text': intimate_suffix,
'intimacy_score': intimacy_score,
'max_intimacy': max_intimacy
}
# Format using template with safe formatting
try:
scenario = template.format(**format_dict)
except KeyError as e:
# If template has unknown placeholders, use a safe default
print(f"Template formatting error: {e}. Using fallback template.")
fallback_template = "You are a [{character_gender}] {character} {tone_string} at {location}, {activity} with a [{target_gender}] {target} ({relationship}) {additional_text}{intimate_text}. [Intimacy Level: {intimacy_score}/{max_intimacy}]"
scenario = fallback_template.format(**format_dict)
return scenario
def generate_scenario(self, character: Optional[str] = None, target: Optional[str] = None,
force_erotic: Optional[bool] = None,
relationship_level: Optional[str] = None) -> str:
"""Generate a single scenario with optional constraints"""
# Step 1: Choose character
characters = self.config.get('characters', {})
if character and character in characters:
chosen_character = character
else:
chosen_character = random.choice(list(characters.keys()))
character_data = characters[chosen_character]
# Step 2: Choose target
valid_targets = character_data.get('valid_targets', [])
if target and target in valid_targets:
chosen_target = target
else:
chosen_target = random.choice(valid_targets)
# Step 3: Choose location
locations = character_data.get('locations', ['unknown_location'])
chosen_location = random.choice(locations)
# Step 4: Choose relationship level
level_name, intimacy_score, restrictions = self._choose_relationship_level(
chosen_character, chosen_target, relationship_level
)
# Step 5: Filter and choose activity
available_activities = self._filter_activities(
intimacy_score, restrictions)
activity_level = self._choose_activity_level(
available_activities, force_erotic)
if not available_activities.get(activity_level):
activity_level = "sfw" # Fallback
level_activities = available_activities[activity_level]
if not level_activities:
# Emergency fallback
return f"Unable to generate scenario for {chosen_character}-{chosen_target} at intimacy level {intimacy_score}"
activity_name = random.choice(list(level_activities.keys()))
activity_data = level_activities[activity_name]
# Step 6: Get tones for formatting
tones = activity_data.get('tones', ['neutral'])
# Step 7: Choose additional activity
additional_activity = self._choose_additional_activity(activity_level)
# Step 8: Format scenario
is_erotic = activity_level != "sfw"
return self._format_scenario(
chosen_character, chosen_location, chosen_target,
activity_name, tones, activity_level, additional_activity,
is_erotic, level_name, intimacy_score
)
def generate_multiple_scenarios(self, count: int = 5, **kwargs) -> List[str]:
"""Generate multiple scenarios"""
return [self.generate_scenario(**kwargs) for _ in range(count)]
def generate_relationship_progression(self, character: str, target: str, count: int = 5) -> List[str]:
"""Generate scenarios showing relationship progression"""
scenarios = []
min_score, max_score, restrictions = self._get_intimacy_range(
character, target)
# Get valid relationship levels in order
relationship_levels = self.config.get('relationship_levels', {})
valid_levels = []
for level_name, level_data in relationship_levels.items():
score = level_data.get('intimacy_score', 0)
if min_score <= score <= max_score:
valid_levels.append((level_name, score))
valid_levels.sort(key=lambda x: x[1]) # Sort by intimacy score
# Generate scenarios across progression
for i in range(count):
if i < len(valid_levels):
level_name = valid_levels[i][0]
else:
level_name = valid_levels[-1][0] # Use highest available
scenario = self.generate_scenario(
character, target, relationship_level=level_name)
scenarios.append(scenario)
return scenarios
def get_relationship_info(self, character: str, target: str) -> Dict[str, Any]:
"""Get detailed relationship information"""
min_score, max_score, restrictions = self._get_intimacy_range(
character, target)
constraints = self._get_relationship_constraints(character, target)
relationship_levels = self.config.get('relationship_levels', {})
available_levels = []
for level_name, level_data in relationship_levels.items():
score = level_data.get('intimacy_score', 0)
if min_score <= score <= max_score:
available_levels.append({
'name': level_name,
'display_name': level_data.get('name', level_name),
'score': score,
'description': level_data.get('description', '')
})
available_levels.sort(key=lambda x: x['score'])
return {
'character': character,
'target': target,
'available_levels': available_levels,
'restrictions': restrictions,
'min_intimacy': min_score,
'max_intimacy': max_score,
'constraint_description': constraints.get('description', '')
}
# Usage Examples and Testing
def main():
"""Main function demonstrating usage"""
# Initialize generator
generator = DynamicScenarioGenerator()
print("=== DYNAMIC SCENARIO GENERATOR WITH ENHANCED TONE FORMATTING ===")
print(f"Loaded {len(generator.get_available_characters())} characters")
print(f"Available characters: {generator.get_available_characters()}")
print("\n=== RANDOM SCENARIOS WITH FORMATTED TONES ===")
for i, scenario in enumerate(generator.generate_multiple_scenarios(8), 1):
print(f"{i}. {scenario}")
print("\n=== RELATIONSHIP PROGRESSION WITH TONE VARIATIONS ===")
print("Secretary-Boss progression:")
progression = generator.generate_relationship_progression(
"Secretary", "Boss", 4)
for i, scenario in enumerate(progression, 1):
print(f" {i}. {scenario}")
print("\n=== TONE FORMATTING EXAMPLES ===")
print("SFW scenarios:")
for i in range(3):
scenario = generator.generate_scenario(force_erotic=False)
print(f" • {scenario}")
print("\nNSFW scenarios:")
for i in range(3):
scenario = generator.generate_scenario(force_erotic=True)
print(f" • {scenario}")
print("\n=== ADDING NEW CONTENT TO CONFIG ===")
# Add new character
generator.config_manager.add_character(
"Babysitter",
locations=["family_home", "living_room", "kitchen"],
valid_targets=["Father", "Older_Son"],
workplace=False,
description="Young caregiver in family setting"
)
# Add corresponding target genders
generator.config_manager.add_target_gender("Father", "Male")
generator.config_manager.add_target_gender("Older_Son", "Male")
# Add relationship constraints
generator.config_manager.add_relationship_constraint(
"Babysitter", "Father",
min_level="stranger", max_level="sexual",
restrictions=["age_gap", "power_imbalance"],
description="Forbidden attraction between babysitter and father"
)
# Add new activity with tones
generator.config_manager.add_activity(
"nsfw_light", "sensual_dancing",
min_intimacy=4,
tones=["seductive", "playful", "rhythmic", "hypnotic", "confident"],
description="Erotic dancing and movement"
)
# Save updated config
generator.save_config()
# Reload to pick up changes
generator.reload_config()
print("Added Babysitter character and sensual_dancing activity")
print(f"Updated character list: {generator.get_available_characters()}")
# Generate scenario with new character
print("\nScenarios with new character:")
if "Babysitter" in generator.get_available_characters():
for i in range(3):
babysitter_scenario = generator.generate_scenario("Babysitter")
print(f" • {babysitter_scenario}")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment