GameConfigIdea / timeline_and_UI_generation_functions.py
kwabs22
Port changes from duplicate space to original
9328e91
import random
from relatively_constant_variables import player_engagement_items, story_events, all_idea_lists, existing_game_inspirations, multiplayer_features, list_names
import json
import gradio as gr
import re
import os
def pick_random_items(items, n):
return random.sample(items, n)
def generate_timeline(events, label):
timeline = []
for event in events:
timeline.append((random.randint(1, 100), label, event))
return timeline
def create_story(timeline):
story = []
for entry in timeline:
if entry[1] == "Story":
story.append(f"The hero {entry[2].replace('engageBattle', 'engaged in a fierce battle').replace('solveRiddle', 'solved a complex riddle').replace('exploreLocation', 'explored a mysterious location')}.")
else:
story.append(f"The player interacted with {entry[2]}.")
return " ".join(story)
def generate_story_and_timeline(no_story_timeline_points=10, no_ui_timeline_points=10, num_lists=1, items_per_list=1, include_existing_games=False, include_multiplayer=False): # , no_media_timeline_points=5, include_media=True):
# Pick 10 random UI items
random_ui_items = pick_random_items(player_engagement_items, no_ui_timeline_points)
random_story_items = pick_random_items(story_events, no_story_timeline_points)
# Generate UI and story timelines
ui_timeline = generate_timeline(random_ui_items, "UI")
story_timeline = generate_timeline(random_story_items, "Story")
# Initialize merged timeline with UI and story timelines
merged_timeline = ui_timeline + story_timeline
#no_media_merged_timeline = ui_timeline + story_timeline
#print(merged_timeline)
#print(no_media_merged_timeline)
# Include media-related items if specified
# if include_media:
# media_files = generate_media_file_list(no_media_timeline_points)
# #rendered_media = render_media_with_dropdowns(media_files)
# media_timeline = generate_timeline(media_files, "Media")
# merged_timeline += media_timeline
# print(merged_timeline)
# Sort the merged timeline based on the random numbers
merged_timeline.sort(key=lambda x: x[0])
# no_media_merged_timeline.sort(key=lambda x: x[0])
# Create the story
story = create_story(merged_timeline)
# Format the timeline for display
formatted_timeline = "\n".join([f"{entry[0]}: {entry[1]} - {entry[2]}" for entry in merged_timeline])
# no_media_formatted_timeline = "\n".join([f"{entry[0]}: {entry[1]} - {entry[2]}" for entry in no_media_merged_timeline])
# game_structure_with_media = generate_game_structures(formatted_timeline) #, game_structure_without_media = generate_game_structures(formatted_timeline, no_media_formatted_timeline)
game_structure_with_media = convert_timeline_to_game_structure(formatted_timeline)
print("simulplay debug - good to here 4")
suggestions, selected_list_names = timeline_get_random_suggestions(num_lists, items_per_list, include_existing_games, include_multiplayer)
print("simulplay debug - good to here 4")
return formatted_timeline, story, json.dumps(game_structure_with_media, indent=2), suggestions, selected_list_names #no_media_formatted_timeline, json.dumps(game_structure_without_media, indent=2) #, game_structure_with_media
media_file_types = ["image", "video", "audio", "3d", "tts"]
def generate_media_file_list(n):
return [random.choice(media_file_types) for _ in range(n)]
def show_elements(text):
# Parse the input text
pattern = r'(\d+): (UI|Story|Media) - (.+)'
blocks = re.findall(pattern, text)
# Sort blocks by their timestamp
blocks.sort(key=lambda x: int(x[0]))
outputs = []
for timestamp, block_type, content in blocks:
if block_type == 'UI':
# Create HTML for UI elements
ui_html = f'<div class="ui-element">{content}</div>'
outputs.append(gr.HTML(ui_html))
elif block_type == 'Story':
# Display story elements as Markdown
outputs.append(gr.Markdown(f"**{content}**"))
elif block_type == 'Media':
if content.lower() == 'audio':
# Placeholder for audio element
outputs.append(gr.Audio(label=f"Audio at {timestamp} in the order"))
elif content.lower() == 'video':
# Placeholder for video element
outputs.append(gr.Video(label=f"Video at {timestamp} in the order"))
elif content.lower() == 'image':
# Placeholder for image element
outputs.append(gr.Image(label=f"Image at {timestamp} in the order"))
elif content.lower() == '3d':
# Placeholder for 3D model element
outputs.append(gr.Model3D(label=f"3D Model at {timestamp} in the order"))
elif content.lower() == 'tts':
# Placeholder for TTS audio element
outputs.append(gr.Audio(label=f"TTS Audio at {timestamp} in the order"))
return outputs
def show_elements_json_input(json_input):
if not json_input:
return []
try:
data = json.loads(json_input)
except json.JSONDecodeError:
return []
masterlocation1 = data['masterlocation1']
outputs = []
for location, details in masterlocation1.items():
if location == 'end':
continue
with gr.Accordion(f"Location: {location} - Previous description {details['description']}", open=False):
description = gr.Textbox(label="Description", value=details['description'], interactive=True)
outputs.append(description)
events = gr.Textbox(label="Events", value=json.dumps(details['events']), interactive=True)
outputs.append(events)
choices = gr.Textbox(label="Choices", value=json.dumps(details['choices']), interactive=True)
outputs.append(choices)
transitions = gr.Textbox(label="Transitions", value=json.dumps(details['transitions']), interactive=True)
outputs.append(transitions)
# New media field
media = gr.Textbox(label="Media", value=json.dumps(details['media']), interactive=True)
outputs.append(media)
# New developernotes field
developernotes = gr.Textbox(label="developernotes", value=json.dumps(details['developernotes']), interactive=True)
outputs.append(developernotes)
#adding/removing a field means incrementing/decreasing the i+n to match the fields
num_current_unique_fields = 6
def update_json(*current_values):
updated_data = {"masterlocation1": {}}
locations = [loc for loc in masterlocation1.keys() if loc != 'end']
for i, location in enumerate(locations):
updated_data["masterlocation1"][location] = {
"description": current_values[i*num_current_unique_fields],
"events": json.loads(current_values[i*num_current_unique_fields + 1]),
"choices": json.loads(current_values[i*num_current_unique_fields + 2]),
"transitions": json.loads(current_values[i*num_current_unique_fields + 3]),
"media": json.loads(current_values[i*num_current_unique_fields + 4]), # New media field
"developernotes": json.loads(current_values[i*num_current_unique_fields + 5])
}
updated_data["masterlocation1"]["end"] = masterlocation1["end"]
return json.dumps(updated_data, indent=2) #json.dumps(updated_data, default=lambda o: o.__dict__, indent=2)
update_button = gr.Button("Update JSON - Still need to copy to correct textbox to load")
json_output = gr.Textbox(label="Updated JSON - Still need to copy to correct textbox to load", lines=10)
#json_output = gr.Code(label="Updated JSON", lines=10) #Locks whole UI so use textbox
update_button.click(update_json, inputs=outputs, outputs=json_output)
return outputs + [update_button, json_output] #, json_output_code]
def show_elements_with_state_sync(json_input, config_state, media_paths_list):
"""
Stateful version of show_elements_json_input that syncs edits back via gr.State.
Args:
json_input: The JSON config string
config_state: gr.State to store edited config (for syncing back)
media_paths_list: List of available media paths to choose from
Returns components that can update the config_state when edited.
"""
if not json_input:
gr.Markdown("No config loaded. Enter JSON config and click Load.")
return
try:
data = json.loads(json_input)
except json.JSONDecodeError as e:
gr.Markdown(f"**JSON Error:** {str(e)}")
return
# Determine structure type (masterlocation1 or direct location keys)
if 'masterlocation1' in data:
locations_data = data['masterlocation1']
wrapper_key = 'masterlocation1'
else:
locations_data = data
wrapper_key = None
outputs = []
location_keys = []
# Create media dropdown choices from available paths
media_choices = media_paths_list if media_paths_list else []
for location, details in locations_data.items():
if location == 'end':
continue
location_keys.append(location)
desc_text = details.get('description', '')[:50] + '...' if len(details.get('description', '')) > 50 else details.get('description', '')
with gr.Accordion(f"📍 {location}: {desc_text}", open=False):
description = gr.Textbox(
label="Description",
value=details.get('description', ''),
interactive=True,
lines=2
)
outputs.append(description)
events = gr.Textbox(
label="Events (JSON array)",
value=json.dumps(details.get('events', [])),
interactive=True
)
outputs.append(events)
choices = gr.Textbox(
label="Choices (JSON array)",
value=json.dumps(details.get('choices', [])),
interactive=True
)
outputs.append(choices)
transitions = gr.Textbox(
label="Transitions (JSON object)",
value=json.dumps(details.get('transitions', {})),
interactive=True
)
outputs.append(transitions)
# Media field with dropdown for available paths
current_media = details.get('media', [])
with gr.Row():
media = gr.Textbox(
label="Media (JSON array)",
value=json.dumps(current_media),
interactive=True,
scale=3
)
if media_choices:
media_dropdown = gr.Dropdown(
choices=media_choices,
label="Add Media Path",
scale=1,
interactive=True
)
outputs.append(media)
developernotes = gr.Textbox(
label="Developer Notes",
value=json.dumps(details.get('developernotes', [])) if isinstance(details.get('developernotes'), list) else details.get('developernotes', ''),
interactive=True
)
outputs.append(developernotes)
# Add end state display (read-only)
if 'end' in locations_data:
with gr.Accordion("🏁 End State", open=False):
gr.JSON(value=locations_data['end'], label="End State Config")
num_fields = 6 # description, events, choices, transitions, media, developernotes
def build_updated_json(*current_values):
"""Rebuild JSON from all field values"""
updated_data = {}
for i, location in enumerate(location_keys):
try:
updated_data[location] = {
"description": current_values[i * num_fields],
"events": json.loads(current_values[i * num_fields + 1]) if current_values[i * num_fields + 1] else [],
"choices": json.loads(current_values[i * num_fields + 2]) if current_values[i * num_fields + 2] else [],
"transitions": json.loads(current_values[i * num_fields + 3]) if current_values[i * num_fields + 3] else {},
"media": json.loads(current_values[i * num_fields + 4]) if current_values[i * num_fields + 4] else [],
"developernotes": json.loads(current_values[i * num_fields + 5]) if current_values[i * num_fields + 5].startswith('[') else current_values[i * num_fields + 5]
}
except json.JSONDecodeError as e:
# If JSON parsing fails, keep as string
updated_data[location] = {
"description": current_values[i * num_fields],
"events": [],
"choices": [],
"transitions": {},
"media": [],
"developernotes": f"JSON Parse Error: {e}"
}
# Add back end state
if 'end' in locations_data:
updated_data['end'] = locations_data['end']
# Wrap if original had wrapper
if wrapper_key:
final_data = {wrapper_key: updated_data}
else:
final_data = updated_data
return json.dumps(final_data, indent=2)
gr.Markdown("---")
with gr.Row():
sync_btn = gr.Button("🔄 Sync Edits to Config", variant="primary")
preview_btn = gr.Button("👁 Preview Changes")
preview_output = gr.Textbox(label="Preview of Updated Config", lines=8, visible=False)
# Preview shows changes without syncing
preview_btn.click(
fn=build_updated_json,
inputs=outputs,
outputs=preview_output
).then(
fn=lambda: gr.update(visible=True),
outputs=preview_output
)
# Sync button returns the updated JSON to be used by parent
sync_btn.click(
fn=build_updated_json,
inputs=outputs,
outputs=preview_output # We'll handle the actual sync in app.py
)
return outputs, sync_btn, preview_output, build_updated_json
def show_elements_json_input_play_and_edit_version(json_input):
if not json_input:
return []
try:
data = json.loads(json_input)
except json.JSONDecodeError:
return []
outputs = []
for location_name, location_data in data.items():
if location_name == "end":
continue
for sub_location, details in location_data.items():
with gr.Accordion(f"Location: {location_name} - {sub_location}", open=False):
description = gr.Textbox(label="Description", value=details.get('description', ''), interactive=True)
outputs.append(description)
choices = gr.Textbox(label="Choices", value=json.dumps(details.get('choices', [])), interactive=True)
outputs.append(choices)
transitions = gr.Textbox(label="Transitions", value=json.dumps(details.get('transitions', {})), interactive=True)
outputs.append(transitions)
consequences = gr.Textbox(label="Consequences", value=json.dumps(details.get('consequences', {})), interactive=True)
outputs.append(consequences)
media = gr.Textbox(label="Media", value=json.dumps(details.get('media', [])), interactive=True)
outputs.append(media)
# Add developernotes field if it exists in the config
if 'developernotes' in details:
developernotes = gr.Textbox(label="Developer Notes", value=details.get('developernotes', ''), interactive=True)
outputs.append(developernotes)
# Determine the number of fields dynamically
num_current_unique_fields = 5 if 'developernotes' not in next(iter(next(iter(data.values())).values())) else 6
def update_json(*current_values):
updated_data = {}
location_names = list(data.keys())
location_names.remove("end") if "end" in location_names else None
value_index = 0
for location_name in location_names:
updated_data[location_name] = {}
sub_locations = list(data[location_name].keys())
for sub_location in sub_locations:
updated_data[location_name][sub_location] = {
"description": current_values[value_index],
"choices": json.loads(current_values[value_index + 1]),
"transitions": json.loads(current_values[value_index + 2]),
"consequences": json.loads(current_values[value_index + 3]),
"media": json.loads(current_values[value_index + 4])
}
if num_current_unique_fields == 6:
updated_data[location_name][sub_location]["developernotes"] = current_values[value_index + 5]
value_index += num_current_unique_fields
if "end" in data:
updated_data["end"] = data["end"]
return json.dumps(updated_data, indent=2)
update_button = gr.Button("Update JSON")
json_output = gr.Textbox(label="Updated JSON", lines=10)
update_button.click(update_json, inputs=outputs, outputs=json_output)
return outputs + [update_button, json_output]
def create_media_component(file_path):
print(file_path)
_, extension = os.path.splitext(file_path)
extension = extension.lower()[1:] # Remove the dot and convert to lowercase
if extension in ['jpg', 'jpeg', 'png', 'gif', 'webp']:
return gr.Image(value=file_path, label="Image Input")
elif extension in ['mp4', 'avi', 'mov']:
return gr.Video(value=file_path, label="Video Input")
elif extension in ['mp3', 'wav', 'ogg']:
return gr.Audio(value=file_path, label="Audio Input")
else:
return gr.Textbox(value=file_path, label=f"File: {os.path.basename(file_path)}")
def convert_timeline_to_game_structure(timeline):
lines = timeline.split('\n')
game_structure = {}
current_location = 0
sub_location = 0
for i, line in enumerate(lines):
if line.strip() == "":
continue
if line[0].isdigit(): # New location starts
current_location += 1
sub_location = 0
location_key = f"location{current_location}"
game_structure[location_key] = {
"description": "",
"events": [],
"choices": ["continue"],
"transitions": {},
"media": [],
"developernotes": []
}
else: # Continue with sub-locations or media entries
sub_location += 1
location_key = f"location{current_location}_{sub_location}"
# Extract the event description
parts = line.split(': ', 1)
if len(parts) == 2:
prefix, rest = parts
event_parts = rest.split(' - ', 1)
if len(event_parts) == 2:
event_type, event_description = event_parts
else:
event_type, event_description = "Unknown", rest
else:
event_type, event_description = "Unknown", line
description = rest.strip() if event_type in ["Media", "UI"] else f"{event_type}: {event_description}"
if sub_location == 0:
game_structure[f"location{current_location}"]["description"] = description
else:
game_structure[f"location{current_location}"]["events"].append({
"description": description,
"type": event_type
})
# Set the transition to the next location or to the end
if i < len(lines) - 1:
next_line = lines[i + 1].strip()
if next_line and next_line[0].isdigit(): # New location starts
game_structure[f"location{current_location}"]["transitions"]["continue"] = f"masterlocation1_location{current_location + 1}"
else:
#game_structure[f"location{current_location}"]["transitions"]["continue"] = f"location_{current_location}_{sub_location + 1}"
game_structure[f"location{current_location}"]["transitions"]["continue"] = "end"
else:
game_structure[f"location{current_location}"]["transitions"]["continue"] = "end"
# Add an end location
game_structure["end"] = {
"description": "The adventure ends here.",
# "choices": [],
# "transitions": {}
"choices": ["restart"],
"transitions": {"restart": "location1"} # Assuming location_1 is the start
}
# Wrap the game structure in master_location1
wrapped_structure = {"masterlocation1": game_structure}
return wrapped_structure
# def generate_game_structures(timeline_with_media): #, timeline_without_media):
# game_structure_with_media = convert_timeline_to_game_structure(timeline_with_media)
# #game_structure_without_media = convert_timeline_to_game_structure(timeline_without_media)
# return game_structure_with_media #, game_structure_without_media
# def timeline_get_random_suggestions(num_lists, items_per_list):
# """
# Generate random suggestions from a specified number of lists.
# :param num_lists: Number of lists to consider
# :param items_per_list: Number of items to select from each list
# :return: A list of randomly selected suggestions
# """
# selected_lists = random.sample(all_idea_lists, min(num_lists, len(all_idea_lists)))
# suggestions = []
# for lst in selected_lists:
# suggestions.extend(random.sample(lst, min(items_per_list, len(lst))))
# return suggestions
def timeline_get_random_suggestions(num_lists, items_per_list, include_existing_games, include_multiplayer):
"""
Generate random suggestions from a specified number of lists.
:param num_lists: Number of lists to consider
:param items_per_list: Number of items to select from each list
:param include_existing_games: Whether to include existing game inspiration lists
:param include_multiplayer: Whether to include multiplayer features list
:return: A tuple containing the list of randomly selected suggestions and the names of selected lists
"""
available_lists = all_idea_lists.copy()
if not include_existing_games:
available_lists = [lst for lst in available_lists if lst not in existing_game_inspirations]
if not include_multiplayer:
available_lists = [lst for lst in available_lists if lst != multiplayer_features]
selected_lists = random.sample(available_lists, min(num_lists, len(available_lists)))
suggestions = []
selected_list_names = []
for lst in selected_lists:
suggestions.extend(random.sample(lst, min(items_per_list, len(lst))))
selected_list_names.append(list_names[all_idea_lists.index(lst)])
return suggestions, selected_list_names
# ==================== NARRATIVE TEMPLATES FOR CONFIG GENERATION ====================
NARRATIVE_TEMPLATES = {
"heros_journey": {
"name": "Hero's Journey",
"description": "Classic monomyth structure: ordinary world -> call to adventure -> trials -> transformation -> return",
"stages": [
{"id": "ordinary_world", "name": "Ordinary World", "description_template": "You find yourself in {setting}. Life is {mood}, but something feels {tension}."},
{"id": "call_to_adventure", "name": "Call to Adventure", "description_template": "A {catalyst} disrupts your routine. You learn about {goal}."},
{"id": "refusal", "name": "Refusal of the Call", "description_template": "Doubts creep in. {obstacle} makes you hesitate."},
{"id": "meeting_mentor", "name": "Meeting the Mentor", "description_template": "You encounter {mentor_type}, who offers {aid}."},
{"id": "crossing_threshold", "name": "Crossing the Threshold", "description_template": "You leave {old_world} behind and enter {new_world}."},
{"id": "tests_allies", "name": "Tests, Allies, Enemies", "description_template": "You face {challenge} and meet {ally_or_enemy}."},
{"id": "approach", "name": "Approach to Inmost Cave", "description_template": "You prepare for the greatest challenge: {final_obstacle}."},
{"id": "ordeal", "name": "The Ordeal", "description_template": "You confront {main_conflict}. Everything is at stake."},
{"id": "reward", "name": "Reward", "description_template": "You achieve {victory}. {reward} is yours."},
{"id": "road_back", "name": "The Road Back", "description_template": "Returning is harder than expected. {complication} arises."},
{"id": "resurrection", "name": "Resurrection", "description_template": "One final test: {ultimate_challenge}."},
{"id": "return_elixir", "name": "Return with the Elixir", "description_template": "You return transformed, bringing {gift} to {beneficiary}."}
],
"ending_count": 3,
"branch_points": ["refusal", "tests_allies", "ordeal"]
},
"mystery": {
"name": "Mystery/Detective",
"description": "Investigation structure: discovery -> clues -> suspects -> revelation -> resolution",
"stages": [
{"id": "discovery", "name": "The Discovery", "description_template": "You discover {mystery}. Something isn't right."},
{"id": "first_clue", "name": "First Clue", "description_template": "Investigating, you find {clue}. It points to {direction}."},
{"id": "suspect_1", "name": "First Suspect", "description_template": "You meet {suspect}. They seem {demeanor}, but {suspicion}."},
{"id": "red_herring", "name": "Red Herring", "description_template": "{misleading_evidence} throws you off track."},
{"id": "key_witness", "name": "Key Witness", "description_template": "{witness} reveals crucial information about {revelation}."},
{"id": "suspect_2", "name": "Second Suspect", "description_template": "New evidence points to {suspect2}. The plot thickens."},
{"id": "breakthrough", "name": "Breakthrough", "description_template": "You realize {key_insight}. Everything connects."},
{"id": "confrontation", "name": "Confrontation", "description_template": "You confront {culprit}. {tension_moment}."},
{"id": "resolution", "name": "Resolution", "description_template": "The truth is revealed: {truth}. Justice is {outcome}."}
],
"ending_count": 4,
"branch_points": ["first_clue", "suspect_1", "confrontation"]
},
"heist": {
"name": "Heist/Mission",
"description": "Planning and execution: target -> team -> plan -> complications -> execution -> aftermath",
"stages": [
{"id": "the_target", "name": "The Target", "description_template": "You learn about {target}. It's worth {stakes}."},
{"id": "assemble_team", "name": "Assemble the Team", "description_template": "You need {specialist}. They're the best at {skill}."},
{"id": "recon", "name": "Reconnaissance", "description_template": "You scout {location}. You notice {vulnerability} and {danger}."},
{"id": "the_plan", "name": "The Plan", "description_template": "The plan: {approach}. It's risky, but {justification}."},
{"id": "complication", "name": "Complication", "description_template": "{unexpected_problem} threatens everything."},
{"id": "point_of_no_return", "name": "Point of No Return", "description_template": "You're in. No turning back. {tension}."},
{"id": "execution", "name": "Execution", "description_template": "The plan unfolds. {action_sequence}."},
{"id": "twist", "name": "The Twist", "description_template": "{betrayal_or_surprise}. Nothing is as it seemed."},
{"id": "escape", "name": "The Escape", "description_template": "{escape_method}. Every second counts."},
{"id": "aftermath", "name": "Aftermath", "description_template": "When the dust settles: {consequences}."}
],
"ending_count": 4,
"branch_points": ["assemble_team", "complication", "execution", "twist"]
},
"survival": {
"name": "Survival",
"description": "Resource management and choices: crisis -> shelter -> resources -> threats -> rescue/adaptation",
"stages": [
{"id": "disaster", "name": "The Disaster", "description_template": "{catastrophe} strikes. You're stranded in {hostile_environment}."},
{"id": "immediate_needs", "name": "Immediate Needs", "description_template": "You need {urgent_need}. Time is critical."},
{"id": "find_shelter", "name": "Find Shelter", "description_template": "You spot {shelter_option}. It's {pros}, but {cons}."},
{"id": "first_threat", "name": "First Threat", "description_template": "{environmental_danger} threatens your survival."},
{"id": "resource_decision", "name": "Resource Decision", "description_template": "You find {resource}. Do you {option1} or {option2}?"},
{"id": "other_survivors", "name": "Other Survivors", "description_template": "You're not alone. {survivor} appears. They're {condition}."},
{"id": "major_crisis", "name": "Major Crisis", "description_template": "{crisis} forces a desperate choice."},
{"id": "hope_signal", "name": "Signal of Hope", "description_template": "You see {hope}. Rescue might be possible."},
{"id": "final_challenge", "name": "Final Challenge", "description_template": "One last obstacle: {final_obstacle}."},
{"id": "resolution", "name": "Resolution", "description_template": "{ending_scenario}. You survived, but at what cost?"}
],
"ending_count": 5,
"branch_points": ["find_shelter", "resource_decision", "other_survivors", "major_crisis"]
},
"memory_fragments": {
"name": "Memory Fragments (Non-linear)",
"description": "Non-linear exploration of memories: awakening -> explore memories in any order -> piece together truth -> confront reality",
"stages": [
{"id": "awakening", "name": "Awakening", "description_template": "You awaken, disoriented. Memories float just out of reach. You sense {memory_count} distinct moments trying to surface..."},
{"id": "memory_hub", "name": "Memory Hub", "description_template": "Fragments swirl in your mind. Each one pulls at you: {memory_hints}."},
{"id": "memory_1", "name": "Memory: {memory_1_theme}", "description_template": "{memory_1_scene}. The details are vivid but the context is missing."},
{"id": "memory_2", "name": "Memory: {memory_2_theme}", "description_template": "{memory_2_scene}. This connects to something important."},
{"id": "memory_3", "name": "Memory: {memory_3_theme}", "description_template": "{memory_3_scene}. A piece of the puzzle falls into place."},
{"id": "memory_4", "name": "Memory: {memory_4_theme}", "description_template": "{memory_4_scene}. Now you understand."},
{"id": "convergence", "name": "Convergence", "description_template": "The memories align. You remember: {truth}. It was {key_person} all along."},
{"id": "reality", "name": "Return to Reality", "description_template": "Armed with the truth, you face {present_situation}. What will you do?"}
],
"ending_count": 4,
"branch_points": ["memory_hub", "convergence", "reality"],
"special": "non_linear_memories"
},
"romance": {
"name": "Romance/Relationship",
"description": "Relationship development: meeting -> attraction -> obstacles -> deepening -> resolution",
"stages": [
{"id": "first_meeting", "name": "First Meeting", "description_template": "You meet {love_interest} at {location}. They're {first_impression}."},
{"id": "initial_attraction", "name": "Initial Attraction", "description_template": "Something about them {attraction_detail}. You want to know more."},
{"id": "getting_to_know", "name": "Getting to Know", "description_template": "You spend time together. You learn they {character_detail}."},
{"id": "first_obstacle", "name": "First Obstacle", "description_template": "{misunderstanding_or_conflict}. Things get complicated."},
{"id": "vulnerability", "name": "Moment of Vulnerability", "description_template": "They share {personal_revelation}. You see the real them."},
{"id": "growing_closer", "name": "Growing Closer", "description_template": "{bonding_moment}. Something shifts between you."},
{"id": "major_conflict", "name": "Major Conflict", "description_template": "{relationship_crisis}. Everything hangs in the balance."},
{"id": "resolution", "name": "Resolution", "description_template": "{resolution_scene}. Your relationship becomes {relationship_outcome}."}
],
"ending_count": 4,
"branch_points": ["first_obstacle", "vulnerability", "major_conflict"]
}
}
def get_narrative_templates_list():
"""Return list of available narrative templates for dropdown."""
return [(NARRATIVE_TEMPLATES[key]["name"], key) for key in NARRATIVE_TEMPLATES]
def generate_config_from_template(template_key, theme="fantasy", num_endings=3):
"""
Generate a game config based on a narrative template.
Args:
template_key: Key from NARRATIVE_TEMPLATES
theme: Theme to apply (fantasy, scifi, modern, horror, etc.)
num_endings: Number of different endings to generate
Returns:
JSON config string
"""
if template_key not in NARRATIVE_TEMPLATES:
return json.dumps({"error": f"Unknown template: {template_key}"})
template = NARRATIVE_TEMPLATES[template_key]
config = {}
# Theme-specific word banks
theme_words = {
"fantasy": {
"setting": ["a quiet village", "a bustling kingdom", "an ancient forest"],
"mentor_type": ["a wise wizard", "an old warrior", "a mysterious sage"],
"new_world": ["the dark lands", "the enchanted realm", "the forbidden territory"],
"reward": ["the sacred artifact", "ancient knowledge", "magical powers"],
},
"scifi": {
"setting": ["a space station", "a colony ship", "a research facility"],
"mentor_type": ["an AI companion", "a veteran pilot", "a scientist"],
"new_world": ["uncharted space", "the alien sector", "the forbidden zone"],
"reward": ["alien technology", "crucial data", "the truth about humanity"],
},
"modern": {
"setting": ["a small town", "a big city apartment", "a suburban home"],
"mentor_type": ["a experienced colleague", "an unlikely friend", "a family member"],
"new_world": ["the unknown", "a new city", "unfamiliar territory"],
"reward": ["self-discovery", "justice", "closure"],
},
"horror": {
"setting": ["an isolated cabin", "an old mansion", "a small town with secrets"],
"mentor_type": ["a skeptical investigator", "a local with knowledge", "a survivor"],
"new_world": ["the nightmare realm", "the haunted grounds", "the darkness"],
"reward": ["survival", "the terrible truth", "a chance to escape"],
}
}
words = theme_words.get(theme, theme_words["fantasy"])
# Generate states from template stages
for i, stage in enumerate(template["stages"]):
state_id = stage["id"]
# Generate choices based on position in story
if i == len(template["stages"]) - 1:
# Final state - ending choices
choices = []
transitions = {}
elif stage["id"] in template.get("branch_points", []):
# Branch point - multiple meaningful choices
choices = ["take the safe path", "take the risky path", "find another way"]
next_stage = template["stages"][i + 1]["id"]
transitions = {
"take the safe path": f"main_{next_stage}",
"take the risky path": f"main_{next_stage}",
"find another way": f"main_{next_stage}"
}
else:
# Linear progression
choices = ["continue"]
next_stage = template["stages"][i + 1]["id"]
transitions = {"continue": f"main_{next_stage}"}
# Simple description (template placeholders would be filled by LLM in production)
description = stage["description_template"]
for key, options in words.items():
placeholder = "{" + key + "}"
if placeholder in description:
description = description.replace(placeholder, random.choice(options))
# Clean remaining placeholders with generic text
import re
description = re.sub(r'\{[^}]+\}', '[something important]', description)
config[f"main_{state_id}"] = {
"description": description,
"choices": choices,
"transitions": transitions,
"media": [],
"developernotes": [f"Stage: {stage['name']}", f"Template: {template['name']}"]
}
# Add endings
for i in range(min(num_endings, template.get("ending_count", 3))):
ending_types = ["triumphant", "bittersweet", "tragic", "mysterious", "open"]
ending_type = ending_types[i % len(ending_types)]
config[f"ending_{ending_type}"] = {
"description": f"[{ending_type.upper()} ENDING] Your journey concludes. The choices you made led here.",
"choices": [],
"transitions": {},
"media": [],
"developernotes": [f"Ending type: {ending_type}"]
}
# Update final stage to point to endings
final_stage_id = f"main_{template['stages'][-1]['id']}"
if final_stage_id in config:
config[final_stage_id]["choices"] = [f"ending {i+1}" for i in range(min(num_endings, 3))]
ending_types = ["triumphant", "bittersweet", "tragic"]
config[final_stage_id]["transitions"] = {
f"ending {i+1}": f"story_ending_{ending_types[i]}"
for i in range(min(num_endings, 3))
}
# Wrap in the expected nested structure: {"location": {"state": {...}}}
# The game engine expects location_state format, so we use "story" as location
wrapped_config = {"story": config}
# Update all transitions to use story_ prefix
for state_name, state_data in config.items():
if "transitions" in state_data:
new_transitions = {}
for choice, target in state_data["transitions"].items():
# Add story_ prefix if not already present
if not target.startswith("story_"):
new_transitions[choice] = f"story_{target}"
else:
new_transitions[choice] = target
state_data["transitions"] = new_transitions
return json.dumps(wrapped_config, indent=2)
def generate_config_from_prompt(prompt, structure_type="branching"):
"""
Generate a config structure from a natural language prompt.
This creates the skeleton - actual content should be filled by LLM.
Args:
prompt: Natural language description of the game
structure_type: "linear", "branching", or "hub"
Returns:
JSON config string with placeholder content
"""
# Extract key elements from prompt (simple keyword extraction)
prompt_lower = prompt.lower()
# Detect approximate number of scenes
scene_indicators = ["scene", "chapter", "part", "act", "stage", "location", "area"]
num_scenes = 8 # default
for indicator in scene_indicators:
if indicator in prompt_lower:
# Look for numbers near the indicator
import re
matches = re.findall(rf'(\d+)\s*{indicator}', prompt_lower)
if matches:
num_scenes = int(matches[0])
break
# Detect number of endings
ending_match = re.search(r'(\d+)\s*ending', prompt_lower)
num_endings = int(ending_match.group(1)) if ending_match else 3
# Detect if it mentions specific themes
themes_detected = []
theme_keywords = {
"mystery": ["mystery", "detective", "investigate", "clue", "solve"],
"horror": ["horror", "scary", "haunted", "dark", "terror"],
"romance": ["romance", "love", "relationship", "dating"],
"adventure": ["adventure", "quest", "journey", "explore"],
"survival": ["survival", "survive", "stranded", "resource"],
}
for theme, keywords in theme_keywords.items():
if any(kw in prompt_lower for kw in keywords):
themes_detected.append(theme)
config = {}
# Generate structure based on type
if structure_type == "linear":
for i in range(num_scenes):
state_id = f"scene_{i+1}"
next_state = f"scene_{i+2}" if i < num_scenes - 1 else "ending_main"
config[state_id] = {
"description": f"[SCENE {i+1}] {prompt[:50]}... - Add description here",
"choices": ["continue"] if i < num_scenes - 1 else [],
"transitions": {"continue": next_state} if i < num_scenes - 1 else {},
"media": [],
"developernotes": [f"Scene {i+1} of {num_scenes}", f"Themes: {themes_detected}"]
}
config["ending_main"] = {
"description": "[ENDING] The story concludes.",
"choices": [],
"transitions": {},
"media": []
}
elif structure_type == "branching":
# Create a tree structure
config["start"] = {
"description": f"[START] {prompt[:100]}... - The beginning of your story",
"choices": ["path A", "path B"],
"transitions": {"path A": "branch_a_1", "path B": "branch_b_1"},
"media": [],
"developernotes": ["Starting point", f"Prompt: {prompt[:50]}"]
}
# Branch A
for i in range(num_scenes // 2):
state_id = f"branch_a_{i+1}"
next_state = f"branch_a_{i+2}" if i < (num_scenes // 2) - 1 else "ending_a"
config[state_id] = {
"description": f"[PATH A - Scene {i+1}] Following the first path...",
"choices": ["continue"] if i < (num_scenes // 2) - 1 else [],
"transitions": {"continue": next_state} if i < (num_scenes // 2) - 1 else {},
"media": []
}
# Branch B
for i in range(num_scenes // 2):
state_id = f"branch_b_{i+1}"
next_state = f"branch_b_{i+2}" if i < (num_scenes // 2) - 1 else "ending_b"
config[state_id] = {
"description": f"[PATH B - Scene {i+1}] Following the second path...",
"choices": ["continue"] if i < (num_scenes // 2) - 1 else [],
"transitions": {"continue": next_state} if i < (num_scenes // 2) - 1 else {},
"media": []
}
# Endings
config["ending_a"] = {
"description": "[ENDING A] One possible conclusion.",
"choices": [],
"transitions": {},
"media": []
}
config["ending_b"] = {
"description": "[ENDING B] Another possible conclusion.",
"choices": [],
"transitions": {},
"media": []
}
elif structure_type == "hub":
# Hub and spoke structure (like MemoryFragments)
config["hub"] = {
"description": f"[HUB] {prompt[:100]}... - You can explore in any direction",
"choices": [f"explore area {i+1}" for i in range(min(4, num_scenes))],
"transitions": {f"explore area {i+1}": f"area_{i+1}" for i in range(min(4, num_scenes))},
"media": [],
"developernotes": ["Central hub - player can explore areas in any order"]
}
# Create areas
for i in range(min(4, num_scenes)):
config[f"area_{i+1}"] = {
"description": f"[AREA {i+1}] An explorable area with its own story...",
"choices": ["investigate further", "return to hub"],
"transitions": {
"investigate further": f"area_{i+1}_deep",
"return to hub": "hub"
},
"media": []
}
config[f"area_{i+1}_deep"] = {
"description": f"[AREA {i+1} - DEEP] You discover something important here...",
"choices": ["return to hub", "go to finale"],
"transitions": {
"return to hub": "hub",
"go to finale": "finale"
},
"media": []
}
config["finale"] = {
"description": "[FINALE] With everything discovered, the truth becomes clear...",
"choices": [f"ending {i+1}" for i in range(num_endings)],
"transitions": {f"ending {i+1}": f"ending_{i+1}" for i in range(num_endings)},
"media": []
}
for i in range(num_endings):
config[f"ending_{i+1}"] = {
"description": f"[ENDING {i+1}] One of {num_endings} possible conclusions.",
"choices": [],
"transitions": {},
"media": []
}
# Wrap in the expected nested structure: {"location": {"state": {...}}}
# The game engine expects location_state format, so we use "game" as location
wrapped_config = {"game": config}
# Update all transitions to use game_ prefix
for state_name, state_data in config.items():
if "transitions" in state_data:
new_transitions = {}
for choice, target in state_data["transitions"].items():
# Add game_ prefix if not already present
if not target.startswith("game_"):
new_transitions[choice] = f"game_{target}"
else:
new_transitions[choice] = target
state_data["transitions"] = new_transitions
return json.dumps(wrapped_config, indent=2)