Spaces:
Build error
Build error
| 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) |