GameConfigIdea / beat_scenarios.py
kwabs22
Port changes from duplicate space to original
9328e91
"""Story beat data and sequence generation.
This module provides:
- STORY_BEATS: Dictionary of story beats organized by format and genre
- generate_beat_sequence: Function to generate story sequences based on format/genre
"""
import json
import random
# Story beats organized by narrative function
STORY_BEATS = {
"setup": [
("Ordinary World", "Establish the protagonist's normal life before the adventure"),
("Meet the Hero", "Introduction to protagonist with defining character moment"),
("Establish Stakes", "Show what the protagonist stands to lose"),
("The Want", "Protagonist expresses a desire or goal"),
("The Flaw", "Hint at protagonist's weakness that must be overcome"),
("Supporting Cast", "Introduce key allies and their relationships"),
],
"catalyst": [
("Call to Adventure", "External event disrupts the ordinary world"),
("Inciting Incident", "Something happens that demands a response"),
("The Invitation", "Opportunity presents itself to the protagonist"),
("The Challenge", "Direct challenge forces protagonist to act"),
("Bad News", "Devastating information changes everything"),
("The Stranger", "New character brings change or information"),
],
"debate": [
("Refusal of Call", "Protagonist hesitates or refuses the challenge"),
("The Doubt", "Protagonist questions their ability"),
("Seeking Advice", "Protagonist consults mentor or wise figure"),
("Weighing Options", "Protagonist considers paths forward"),
("The Warning", "Someone cautions against the journey"),
("Internal Conflict", "Protagonist struggles with competing desires"),
],
"rising_action": [
("Crossing Threshold", "Protagonist commits to the journey"),
("New World Rules", "Protagonist learns how this new world works"),
("Tests and Allies", "Series of challenges, gaining companions"),
("Fun and Games", "Promise of the premise fulfilled"),
("Skill Building", "Protagonist develops abilities needed for climax"),
("B-Story Develops", "Secondary plot line advances"),
("Approaching Cave", "Preparing for the major challenge"),
],
"midpoint": [
("False Victory", "Apparent success that will prove hollow"),
("False Defeat", "Apparent failure that motivates comeback"),
("Major Revelation", "Game-changing information revealed"),
("Point of No Return", "Commitment that cannot be undone"),
("Raised Stakes", "The cost of failure increases dramatically"),
("New Goal", "Original objective shifts to something bigger"),
],
"complications": [
("Bad Guys Close In", "Opposition intensifies, allies scatter"),
("Betrayal", "Trusted ally reveals true colors"),
("All Is Lost", "Lowest point - everything seems hopeless"),
("Death Moment", "Literal or symbolic death experience"),
("Dark Night", "Protagonist faces their deepest fears"),
("Whiff of Death", "Mortality or failure becomes very real"),
],
"climax": [
("Gathering the Team", "Final assembly before the battle"),
("The Plan", "Strategy for the final confrontation revealed"),
("Storming the Castle", "Direct assault on the antagonist"),
("Final Battle", "Climactic confrontation begins"),
("High Tower Surprise", "Unexpected complication in the climax"),
("The Dig Deep", "Protagonist must use everything they've learned"),
("Victory/Defeat", "The outcome of the main conflict"),
],
"resolution": [
("New Equilibrium", "World rebalanced after the adventure"),
("Character Changed", "Demonstrate protagonist's transformation"),
("Reward", "Protagonist receives what they've earned"),
("Return Home", "Protagonist returns to ordinary world, changed"),
("Open Loop", "Hint at future adventures"),
("Final Image", "Mirror of opening that shows change"),
],
}
# Genre-specific flavor modifiers
GENRE_FLAVORS = {
"action": {
"prefix": "PULSE-POUNDING: ",
"descriptors": ["explosive", "high-octane", "adrenaline-fueled", "relentless"],
},
"drama": {
"prefix": "EMOTIONALLY CHARGED: ",
"descriptors": ["poignant", "raw", "intimate", "devastating"],
},
"comedy": {
"prefix": "HILARIOUS: ",
"descriptors": ["absurd", "witty", "chaotic", "perfectly timed"],
},
"thriller": {
"prefix": "HEART-STOPPING: ",
"descriptors": ["tense", "paranoid", "claustrophobic", "nerve-wracking"],
},
"romance": {
"prefix": "SWOON-WORTHY: ",
"descriptors": ["tender", "passionate", "yearning", "chemistry-filled"],
},
"scifi": {
"prefix": "MIND-BENDING: ",
"descriptors": ["futuristic", "technological", "alien", "conceptual"],
},
"fantasy": {
"prefix": "EPIC: ",
"descriptors": ["mystical", "legendary", "enchanted", "otherworldly"],
},
"horror": {
"prefix": "TERRIFYING: ",
"descriptors": ["dread-filled", "nightmarish", "unsettling", "visceral"],
},
}
# Format-specific beat counts (simplified for game flow)
FORMAT_BEATS = {
"film_90min": {
"name": "90-Minute Feature Film",
"structure": ["setup", "catalyst", "debate", "rising_action", "midpoint",
"complications", "climax", "resolution"],
"typical_count": 8,
},
"tv_30min": {
"name": "30-Minute TV Episode",
"structure": ["setup", "catalyst", "rising_action", "midpoint",
"complications", "climax", "resolution"],
"typical_count": 6,
},
"youtube_9min": {
"name": "9-Minute YouTube Video",
"structure": ["setup", "catalyst", "rising_action", "climax", "resolution"],
"typical_count": 5,
},
"short_3min": {
"name": "3-Minute Short",
"structure": ["setup", "catalyst", "climax", "resolution"],
"typical_count": 4,
},
}
def generate_beat_sequence(format_type, genre, beat_count):
"""Generate a story beat sequence based on format and genre.
Args:
format_type: One of 'film_90min', 'tv_30min', 'youtube_9min', 'short_3min'
genre: One of 'action', 'drama', 'comedy', 'thriller', 'romance', 'scifi', 'fantasy', 'horror'
beat_count: Number of beats to generate (3-15)
Returns:
Tuple of (list_output, json_output, prompts_output)
"""
format_info = FORMAT_BEATS.get(format_type, FORMAT_BEATS["film_90min"])
genre_info = GENRE_FLAVORS.get(genre, GENRE_FLAVORS["drama"])
structure = format_info["structure"]
# Build sequence following structure
sequence = []
beats_per_section = max(1, beat_count // len(structure))
remaining = beat_count - (beats_per_section * len(structure))
for section in structure:
section_beats = STORY_BEATS.get(section, STORY_BEATS["setup"])
# Pick random beats from this section
count_for_section = beats_per_section + (1 if remaining > 0 else 0)
if remaining > 0:
remaining -= 1
available = list(section_beats)
random.shuffle(available)
for beat in available[:count_for_section]:
descriptor = random.choice(genre_info["descriptors"])
sequence.append((beat[0], beat[1], section, descriptor))
# Trim or pad to exact count
if len(sequence) > beat_count:
sequence = sequence[:beat_count]
if not sequence:
return "Select a valid format and genre!", "{}", ""
# Format as list
list_output = f"## {format_info['name']} - {genre.title()} Genre\n\n"
for i, (name, desc, section, descriptor) in enumerate(sequence, 1):
list_output += f"**{i}. {name}** [{section}]\n{descriptor.title()}: {desc}\n\n"
# Format as config JSON
config = {"story_location": {}}
for i, (name, desc, section, descriptor) in enumerate(sequence, 1):
state_name = name.lower().replace(" ", "_").replace("/", "_").replace("-", "_")
current_state_id = f"beat_{i}_{state_name}"
# Determine next state
if i < len(sequence):
next_name = sequence[i][0]
next_state_name = next_name.lower().replace(" ", "_").replace("/", "_").replace("-", "_")
next_state_id = f"beat_{i+1}_{next_state_name}"
else:
next_state_id = "story_end"
config["story_location"][current_state_id] = {
"description": f"[{section.upper()}] {genre_info['prefix']}{desc}",
"media_prompt": f"{genre.title()} {format_info['name']} scene: {name} - {descriptor} {desc}",
"choices": ["Continue"],
"transitions": {
"Continue": next_state_id
}
}
# Add ending state
first_beat = sequence[0][0].lower().replace(" ", "_").replace("/", "_").replace("-", "_")
config["story_location"]["story_end"] = {
"description": f"The {genre} story concludes. The journey has changed everything.",
"choices": ["Experience Again"],
"transitions": {
"Experience Again": f"beat_1_{first_beat}"
}
}
json_output = json.dumps(config, indent=2)
# Format prompts
prompts_output = f"## {genre.title()} Story Prompts\n\n"
for i, (name, desc, section, descriptor) in enumerate(sequence, 1):
prompts_output += f"{genre.title()} {format_info['name']} scene: {name} - {descriptor} {desc}\n"
return list_output, json_output, prompts_output
# Quick test
if __name__ == "__main__":
list_out, json_out, prompts_out = generate_beat_sequence("film_90min", "thriller", 5)
print(list_out)
print(json_out)