Created
June 13, 2025 22:19
-
-
Save CypherpunkSamurai/dad84e1822c0f9fad02642283b57f764 to your computer and use it in GitHub Desktop.
Scenario Creator for Visual Novels
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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