The Ultimate AI Experiment: When LLMs Play the Danganronpa Killing Game (Part 2)

In Part 1, we talked about why we threw AIs into the wild world of Danganronpa – we wanted AI game characters that are actually interesting! We saw AI agents scheme, accuse, and fight for their virtual lives. Now, in Part 2, we get into the how: all the techy stuff like architecture, data management, and how we got these AIs to play together. Missed Part 1? Catch up here!! 1. Quick Recap: Boring NPCs are Out, Danganronpa Drama is In! In Part 1, we whined about a big problem in gaming: even though AI is super smart now, a lot of game characters (NPCs) are still kinda... blah. Predictable. Polite, but dull. They say their lines, give you quests, but rarely do anything surprising or get into the messy social stuff that makes games with real people so fun. Our crazy idea? The Agentic Danganronpa Simulator. We took AI agents, gave each one the personality of a Danganronpa "Ultimate" student, and tossed them into the super-tense "Killing Game." Why? To see if we could make AIs that don't just follow a script, but actually strategize, lie, team up, and react with (fake) emotions, all based on their unique characters and the desperate need to survive. We saw how this setup, with all its secrets and mind games, could lead to some seriously cool and unpredictable AI moments. Now, let's pop the hood and see what makes this despair-filled theater tick. How do you actually build a system that lets a bunch of AI agents play such a tricky social game, keeping track of who knows what and a game that's always changing? Time to get into the nitty-gritty! 2. The Blueprint: State Machines, Smart Nodes, and Fast AI To get our Danganronpa AIs to dance (or rather, debate and deceive) properly, our setup uses three main ingredients: a Streamlit-powered state machine to control the game's flow, smart PocketFlow nodes for AI brainpower, and speedy LLM agents to bring the characters to life. 1. Driving the Drama: The Streamlit State Machine Think of the Danganronpa game as a play with different acts and scenes: secret Night discussions, tense Morning Announcements, chaotic Class Trial debates, crucial Voting, and dramatic Executions. To manage all this, we use a state machine built right into our Streamlit app. What's a state machine? It's just a way to define all the possible stages (or "states") of the game and the rules for moving from one stage to the next. The key player here is a variable we call st.session_state.current_state. Each value this variable can take (like NIGHT_PHASE_BLACKENED_DISCUSSION or CLASS_TRIAL_VOTE) tells the game exactly what phase it's in. Our main app code (app.py) constantly checks this current_state and does whatever is needed for that phase. This could mean: Showing specific buttons or text boxes on the screen. Grabbing info from our game database. Telling an AI agent (using a PocketFlow node) it's their turn to think or act. Updating the game's status and then switching st.session_state.current_state to the next phase. Here's a simplified picture of how the game moves from one state to another: The Streamlit frontend (what you see in your browser) is super important for this. When you vote or type in your arguments, Streamlit tells our state machine, often changing the current_state and moving the game forward. All the important game info, like the current_state, who the players are, the message history, and any temporary data the AIs or UI need, is held in st.session_state. Think of it as the game's shared brain for the current session. 2. Intelligent Actors: PocketFlow Nodes for AI Decisions So, Streamlit handles the when (what phase are we in?), but PocketFlow nodes handle the what and how of what an AI does during its turn. We don't use PocketFlow's big Flow objects to run the whole game. Instead, we use its handy Node idea to package up the smarts for a single AI character's decision. Our main tool here is the DecisionNode: class Node: def __init__(self): self.params = {} # Parameters specific to this node's execution # Simplified steps for an AI's turn: def prep(self, shared_store): # shared_store is st.session_state # 1. Gather all info the AI needs from shared_store (game history, its role, etc.). # THIS IS SUPER IMPORTANT for making sure AIs only know what they should! # self.params would tell it which character it's playing (e.g., 'Kokichi Oma'). pass # Returns all the context it gathered. def prep(self, prep_result_context): # 2. Build a prompt for the LLM using the gathered context. # 3. Call the LLM (Gemini 2.5 Flash) to get the AI's thoughts and actions. # 4. Check and clean up the LLM's response. pass # Returns the AI's decision in a structured way. def prep(self, shared_store, prep_res, exec_res): # 5. Save the AI's thinking and actions to the database. # 6. Update shared_store if needed (e.g., with w

May 9, 2025 - 21:44
 0
The Ultimate AI Experiment: When LLMs Play the Danganronpa Killing Game (Part 2)

In Part 1, we talked about why we threw AIs into the wild world of Danganronpa – we wanted AI game characters that are actually interesting! We saw AI agents scheme, accuse, and fight for their virtual lives. Now, in Part 2, we get into the how: all the techy stuff like architecture, data management, and how we got these AIs to play together. Missed Part 1? Catch up here!!

1. Quick Recap: Boring NPCs are Out, Danganronpa Drama is In!

In Part 1, we whined about a big problem in gaming: even though AI is super smart now, a lot of game characters (NPCs) are still kinda... blah. Predictable. Polite, but dull. They say their lines, give you quests, but rarely do anything surprising or get into the messy social stuff that makes games with real people so fun.

Our crazy idea? The Agentic Danganronpa Simulator. We took AI agents, gave each one the personality of a Danganronpa "Ultimate" student, and tossed them into the super-tense "Killing Game." Why? To see if we could make AIs that don't just follow a script, but actually strategize, lie, team up, and react with (fake) emotions, all based on their unique characters and the desperate need to survive. We saw how this setup, with all its secrets and mind games, could lead to some seriously cool and unpredictable AI moments.

Now, let's pop the hood and see what makes this despair-filled theater tick. How do you actually build a system that lets a bunch of AI agents play such a tricky social game, keeping track of who knows what and a game that's always changing? Time to get into the nitty-gritty!

Image description

2. The Blueprint: State Machines, Smart Nodes, and Fast AI

To get our Danganronpa AIs to dance (or rather, debate and deceive) properly, our setup uses three main ingredients: a Streamlit-powered state machine to control the game's flow, smart PocketFlow nodes for AI brainpower, and speedy LLM agents to bring the characters to life.

1. Driving the Drama: The Streamlit State Machine

Think of the Danganronpa game as a play with different acts and scenes: secret Night discussions, tense Morning Announcements, chaotic Class Trial debates, crucial Voting, and dramatic Executions. To manage all this, we use a state machine built right into our Streamlit app.

What's a state machine? It's just a way to define all the possible stages (or "states") of the game and the rules for moving from one stage to the next. The key player here is a variable we call st.session_state.current_state.

Each value this variable can take (like NIGHT_PHASE_BLACKENED_DISCUSSION or CLASS_TRIAL_VOTE) tells the game exactly what phase it's in. Our main app code (app.py) constantly checks this current_state and does whatever is needed for that phase. This could mean:

  • Showing specific buttons or text boxes on the screen.
  • Grabbing info from our game database.
  • Telling an AI agent (using a PocketFlow node) it's their turn to think or act.
  • Updating the game's status and then switching st.session_state.current_state to the next phase.

Here's a simplified picture of how the game moves from one state to another:

Image description

The Streamlit frontend (what you see in your browser) is super important for this. When you vote or type in your arguments, Streamlit tells our state machine, often changing the current_state and moving the game forward.

All the important game info, like the current_state, who the players are, the message history, and any temporary data the AIs or UI need, is held in st.session_state. Think of it as the game's shared brain for the current session.

2. Intelligent Actors: PocketFlow Nodes for AI Decisions

So, Streamlit handles the when (what phase are we in?), but PocketFlow nodes handle the what and how of what an AI does during its turn. We don't use PocketFlow's big Flow objects to run the whole game. Instead, we use its handy Node idea to package up the smarts for a single AI character's decision.

Our main tool here is the DecisionNode:

class Node:
    def __init__(self):
        self.params = {} # Parameters specific to this node's execution

    # Simplified steps for an AI's turn:
    def prep(self, shared_store): # shared_store is st.session_state
        # 1. Gather all info the AI needs from shared_store (game history, its role, etc.).
        # THIS IS SUPER IMPORTANT for making sure AIs only know what they should!
        # self.params would tell it which character it's playing (e.g., 'Kokichi Oma').
        pass # Returns all the context it gathered.

    def prep(self, prep_result_context):
        # 2. Build a prompt for the LLM using the gathered context.
        # 3. Call the LLM (Gemini 2.5 Flash) to get the AI's thoughts and actions.
        # 4. Check and clean up the LLM's response.
        pass # Returns the AI's decision in a structured way.

    def prep(self, shared_store, prep_res, exec_res):
        # 5. Save the AI's thinking and actions to the database.
        # 6. Update shared_store if needed (e.g., with what the AI will say publicly).

    def run_async(self, shared_store): # app.py calls this for an AI's turn
        prep_result = self.prep(shared_store)
        exec_result = self.prep(prep_result)
        self.prep(shared_store, prep_result, exec_result)

When app.py decides it's a specific AI's turn (based on the current_state and who's next to speak), it basically tells a DecisionNode, "Hey, you're up! You're playing as [Character Name]." It then kicks off the run_async method, giving it access to st.session_state (our shared brain). The node then does its prep -> exec -> post dance to figure out and record what the AI does.

This keeps the AI's decision-making logic neatly bundled in the DecisionNode, while app.py focuses on the big picture of game flow and what you see on screen.

3. The Minds Behind the Madness: Large Language Models

Each Danganronpa character is brought to life by an AI agent powered by Google's Gemini 2.5 Flash. We picked Flash because it hits a sweet spot:

  • Smart Reasoning: It's clever enough to get the complex rules of the Killing Game, understand social hints from the game's history, and try to think strategically like its Danganronpa character should.
  • Speedy Responses: Speed is key for a game! Flash answers quickly, so the game doesn't feel slow, especially during those fast-paced Class Trial arguments.

The DecisionNode is in charge of writing super-detailed prompts for Gemini 2.5 Flash. These prompts give the LLM the character's personality profile, their secret role, what's happening in the game, a filtered history of events, and specific instructions for what to do now (like "come up with a statement" or "pick someone to vote for"). Good prompts are everything for getting the LLMs to act in believable and interesting ways.

By mixing Streamlit's state control, PocketFlow's neat AI logic nodes, and Gemini 2.5 Flash's brainpower, we get a dynamic and pretty engaging Danganronpa Killing Game simulation.

3. Gathering Your Cast: Resources for Danganronpa AIs

Before your AIs can start scheming and surviving, they need to become the Danganronpa characters. This means giving them personalities, backstories, and even some examples of how they talk. Plus, visuals and sounds make everything more immersive!

Crafting Character Profiles with a Little Help from AI Friends

To really bring characters like Shuichi Saihara or Kokichi Oma to life, you need to feed your game's AI (the LLM playing the character) some rich details. Where do you get these? Well, you can do the research yourself, or even ask an AI assistant (like ChatGPT) to help you out!

For example, you could ask an LLM to search the web for Danganronpa V3 characters and help you build out their profiles. The goal is to create a structured description for each character that your game can use. Here's a simplified idea of what you might aim for, using Shuichi as an example:

{
  "Shuichi": {
    "backstory": (
        "Helped his detective uncle solve an art-theft, hailed as prodigy but feels undeserving, earning "
        "the Ultimate Detective title."
    ),
    "personality": (
        "Timid, earnest, sharp; forces himself to speak when truth demands it."
    ),
    "examples": {
        "normal":       "Uh… I'll start gathering statements—please tell me anything you recall.",
        "sad":          "I promised myself I'd protect everyone… and I failed.",
        "worried":      "The evidence points at me? No—there has to be a contradiction somewhere!",
        "affirmative":  "Here it is—the decisive proof! The mystery… is solved.",
        "blackened":    "Truth is dead. I'll bury the last witness with it."
    }
  }
}
  • Backstory: A brief history to give the AI context on their past.
  • Personality: Key traits that define how they act and react.
  • Examples: Short lines showing their typical speaking style in different emotional states. This is super helpful for the LLM to nail the character's voice.

Having this kind of detailed profile for each character is a game-changer for getting believable performances from your AIs.

Finding Visuals and Sounds

To make your Danganronpa simulator even more engaging, you'll want character sprites (images) and voice clips.

Downloading these assets can help you create a much richer user interface, displaying the character who's speaking and even playing iconic voice lines or sound effects at key moments.

With these resources, you can build a solid foundation for your AI characters, giving them the depth they need to truly shine (or despair!) in the Killing Game.

4. Managing the Mayhem: Game Logic and Database Details

Trying to keep track of everything in a Danganronpa game – tons of characters, secret roles, hidden actions, public statements, and a history that grows every "day" – would be a nightmare with just simple Python code. To keep things clear, easy to search, and all in one place (even for a single game session), we decided to use an in-memory SQLite database. You can think of it as a super-organized digital notebook for the game, accessible via st.session_state.db_conn.

This might sound a bit techy for a web app that forgets everything when you refresh, but it gives us some big wins:

  • Neatly Organized Data: Databases are awesome for storing info that has a clear structure. This is perfect for keeping track of players, their roles, and all the actions they take.
  • Easy Searching: We can quickly find specific bits of game history. For example, "What did Kokichi say on Day 2 during the Class Trial?" or "Who did the Truth-Seeker check out last night?" This is way easier than digging through complicated code structures.
  • One Source of Truth: The database is the official record of what happened. This makes it simpler to manage the game, as different parts of our app (like the AI's DecisionNode or the code that shows stuff on screen) can just ask the database for what they need.
  • Helps Keep Data Safe (Conceptually): Even though it's a simple in-memory database, thinking about saving data in chunks (like after a whole phase is done) helps make sure everything stays consistent as the game changes.

The heart of our data setup is two main tables:

roles Table: Who's Who in the Zoo

This table stores the basic, unchangeable facts about who each character is and their current status. We usually set this up once when a new game starts.

Column Type What it Means Why it's Important
id INTEGER Player order (1, 2, 3...). Main ID. Keeps track of players.
name TEXT Character's name (e.g., "Shuichi Saihara"). Must be unique. Identifies each character.
role TEXT Secret role (e.g., 'Blackened', 'Truth-Seeker'). Crucial for the AI to know its abilities and goals.
is_alive BOOLEAN True if still playing, False if out. Tells us who's still in the game, for win conditions, etc.

Why we have this: This table lets us quickly check a character's secret role (super important for the AI's DecisionNode) and if they're still alive (for figuring out who can act, who won, etc.).

actions Table: The Big Book of Everything That Happens

This is where the action is! This table logs every important event, thought, and statement as the game goes on. It's a permanent record, building a complete history from start to finish.

Column Type What it Means Why it's Important
id INTEGER Unique ID for every single action. Main ID, counts up automatically. Keeps every action distinct.
day INTEGER The game day/round number. Tracks when things happened.
phase TEXT The game stage (e.g., 'NIGHT_PHASE_BLACKENED_DISCUSSION', 'CLASS_TRIAL_VOTE'). Organizes actions by what part of the game they belong to.
actor_name TEXT Who did the action (player name or "Monokuma" for game announcements). Tells us who performed the action.
action_type TEXT Super Important! What kind of action? (e.g., thinking, statement, vote, kill). Helps us categorize and filter actions (e.g., to only show an AI its own thinking).
content TEXT Can be empty. The details (e.g., the AI's secret thoughts, what someone said out loud). Stores the juicy details of the action.
target_name TEXT Can be empty. Who the action was aimed at (e.g., who was killed, who was voted for). Links actions to their targets.
emotion TEXT Can be empty. The feeling behind a statement (e.g., 'normal', 'determined', 'worried'). Adds a bit of flavor to what characters say.

Why we have this: This table is the AI's main history book. The AI's DecisionNode heavily relies on this table to build the recent_history for its prompts. The action_type, actor_name, and phase columns are especially key for filtering this history to keep secrets safe (as we talked about in the last section). For example, an AI only gets to see its own thinking actions, or only sees the results of a Truth-Seeker's investigation if it is the Truth-Seeker.

By carefully logging every move and having a structured way to look them up, our database helps us create the personalized, secret-filled histories our AIs need to play Danganronpa in a smart and believable way. It's the quiet record-keeper of every plot, lie, and desperate cry for help!

5. Crafting AI Context: The Art of Information Control

A huge part of what makes Danganronpa so thrilling is information asymmetry – meaning, not everyone knows the same stuff! Secrets, hidden roles, and private clues are the heart of the game. If every AI knew all secrets, the game would be boring. The fun is in the AIs (and you!) piecing things together from incomplete and often misleading info.

Our DecisionNode is the AI's brain for each turn. Its most crucial job is to carefully gather and filter information to build a personalized context for the LLM. This ensures each AI only acts on what it should legitimately know.

Selective Memory: Querying thinking vs. talking

Remember our AIs have private thinking logs and public talking statements? When an AI prepares for its turn (in its prep step), it needs to access history. Here's how we control that access:

  • Own thinking is Fair Game: An AI can always review its own past thinking logs from the database. This helps it remember its previous plans, suspicions, and reasoning.
  • Others' talking is Public: An AI generally gets to see all the talking (public statements) made by other characters. This is the information everyone shares.
  • Others' thinking is Off-Limits: Critically, an AI never sees the thinking logs of other AIs. Those private thoughts remain secret, fueling deception and deduction.

Role-Specific Intel: Special Briefings

Some roles get unique information. The prep step also handles this:

  • Blackened Team Huddle (Private Channel): If the acting AI is "Blackened," its prep step will specifically query the database for past actions that were part of the private Blackened discussions (e.g., action_type blackened_discussion_statement). This lets them see their teammates' secret plans.
  • Truth-Seeker's Findings (Top Secret Folder): If the AI is the "Truth-Seeker," its prep step retrieves the results of its past investigations (e.g., action_type truth_seeker_reveal_private). This vital clue is only for them.
  • Guardian's Logbook: The Guardian might get reminders of who they protected recently to follow game rules.

This selective querying ensures that the context built for an AI is tailored to its specific role and knowledge.

Inside the DecisionNode: An AI's Turn, Step-by-Step

Let's break down how a simplified DecisionNode might work, using Python-like pseudocode for clarity. Imagine this node is a mini-factory for producing an AI's action for a turn.

Step 1: prep - Gather Your Ingredients & Filter History

prep is all about getting everything the AI needs for its specific character and the current game situation, and most importantly, filtering the game history to what this character should see.

# --- Simplified DecisionNode: prep --- 
class DecisionNode(Node): # Assuming Node is a base class
    def prep(self, shared_game_state):
        character_name = self.params["character_name"] # Who is acting?
        db_conn = shared_game_state["db_conn"]
        current_day = shared_game_state["current_day"]
        current_phase = shared_game_state["current_phase"]

        print(f"PREP: {character_name} is thinking for {current_phase}...")

        # 1. Get character basics (role, profile)
        my_role = db_conn.query_my_role(character_name)
        character_profile = shared_game_state["character_profiles"].get(character_name, {})

        # 2. Fetch and filter game history from database
        # Query 1: Get MY OWN past 'thinking' logs
        my_thinking_history = db_conn.query_actions(
            actor_name=character_name, 
            action_type='thinking'
        )

        # Query 2: Get ALL PUBLIC 'talking' statements from everyone
        public_statement_history = db_conn.query_actions(
            action_type='statement' # Or whatever you call public talk
        )

        # Query 3: Get ROLE-SPECIFIC private info
        role_specific_private_history = []
        if my_role == 'Blackened':
            # Simplified: Get private discussion messages among Blackened
            role_specific_private_history = db_conn.query_actions(
                action_type='blackened_discussion_statement', 
                # May also need to filter by participants if not all Blackened see all
            )
        elif my_role == 'Truth-Seeker':
            # Simplified: Get results of my own past investigations
            role_specific_private_history = db_conn.query_actions(
                actor_name=character_name,
                action_type='truth_seeker_result_private' 
            )
        # ... (add similar logic for Guardian, etc.)

        # Combine and format into a single history string for the LLM
        # This step needs careful ordering and formatting for the LLM to understand
        personalized_history_str = format_combined_history_for_llm(
            my_thinking_history, 
            public_statement_history, 
            role_specific_private_history
        )

        # 3. Get other relevant info for the prompt
        game_rules_summary = shared_game_state["game_rules_summary"]
        living_players = db_conn.query_living_players_names()
        hint_text = get_playbook_hint(my_role, current_phase) # From our playbook!

        # Package it all up for the 'exec' step
        return {
            "character_name": character_name,
            "character_profile": character_profile,
            "my_role": my_role,
            "personalized_history": personalized_history_str,
            "current_day": current_day,
            "current_phase": current_phase,
            "living_players": living_players,
            "game_rules_summary": game_rules_summary,
            "hint_text": hint_text,
            # ... any other info the LLM needs for this phase (e.g., valid targets for voting)
        }

What prep does: It's like a meticulous assistant preparing a briefing document. It finds out who the AI is (character_name), its secret my_role, and its character_profile. Then, it hits the database (represented by db_conn.query_... calls): it grabs the AI's own past secret thoughts (my_thinking_history), all the public chatter everyone has heard (public_statement_history), and any special secret information only that role should see (role_specific_private_history). All this is carefully combined into a personalized_history_str. Finally, it adds general game info, like rules, who's alive, and any relevant strategic hints from our playbook. This whole package is then passed to the exec step.

Step 2: exec - The LLM Makes a Decision

exec takes all the carefully prepared info from prep and uses it to ask the LLM what the character should do.

# --- Simplified DecisionNode: exec --- 
# (Continuing the DecisionNode class)
    def exec(self, prep_context):
        character_name = prep_context["character_name"]
        print(f"EXEC: {character_name} is making a decision...")

        # Build the big prompt for the LLM
        # This combines all the gathered context into a natural language request
        prompt_components = [
            f"You are {character_name}. Your role is {prep_context['my_role']}.",
            f"Your personality: {prep_context['character_profile'].get('personality', '')}",
            f"Game Day: {prep_context['current_day']}, Phase: {prep_context['current_phase']}",
            f"Living Players: {', '.join(prep_context['living_players'])}",
            f"Game Rules Summary: {prep_context['game_rules_summary']}",
            f"Strategic Hint for this situation: {prep_context['hint_text']}",
            f"Personalized Game History (what you know):\n{prep_context['personalized_history']}",
            "Based on all the above, and your character, what is your internal thinking,",
            "and what is your public action (e.g., statement or vote target)?",
            "Respond in YAML format:",
            "thinking: |",
            "  (Your detailed thoughts and reasoning here...)",
            "action_type: ",
            "action_content: |",
            "  (Your public statement, or the name of the player you vote for, etc.)",
            "optional_emotion:  # If making a statement"
        ]
        prompt = "\n".join(prompt_components)

        # Call the LLM (this is a stand-in for the actual API call)
        llm_response_yaml = call_llm_api(prompt)

        # Parse the LLM's YAML response
        # Real parsing would need error handling (try-except blocks)
        parsed_decision = parse_yaml_from_llm(llm_response_yaml) 
        # Expected: {'thinking': '...', 'action_type': '...', 'action_content': '...', 'optional_emotion': '...'}

        # Basic validation (in real code, this would be more robust)
        if not all(k in parsed_decision for k in ['thinking', 'action_type', 'action_content']):
            print(f"ERROR: LLM response for {character_name} was missing required fields!")
            # Handle error, maybe by returning a default safe action
            return {"thinking": "Error in LLM response.", "action_type": "error", "action_content": ""}

        print(f"EXEC: {character_name} decided: {parsed_decision['action_type']}")
        return parsed_decision 

What exec does: This step is where the AI "thinks." It takes the personalized information bundle from prep and crafts a detailed prompt for the Large Language Model. This prompt tells the LLM who it is, its role, personality, what it knows about the game so far (the crucial personalized_history), the current situation, any strategic hints, and then asks it to decide on its internal thinking and its action (like what to say or who to vote for), usually in a structured format like YAML. The call_llm_api then gets the response, which is parsed into a usable format. Basic validation checks if the core pieces are there.

Step 3: post - Record the Action & Update the World

post takes the AI's decision from exec and makes it official by saving it to the database.

# --- Simplified DecisionNode: post --- 
# (Continuing the DecisionNode class)
    def post(self, shared_game_state, prep_context, exec_decision):
        character_name = prep_context["character_name"]
        current_day = prep_context["current_day"]
        # Important: Use the *logging phase* if it was a user input phase that maps to a main one
        logging_phase = map_user_input_phase_to_logging_phase(prep_context["current_phase"])
        db_conn = shared_game_state["db_conn"]

        print(f"POST: Recording {character_name}'s actions for {logging_phase}...")

        # 1. Log the AI's (private) thinking process
        db_conn.log_action(
            day=current_day, 
            phase=logging_phase, 
            actor_name=character_name, 
            action_type='thinking', # This is always private
            content=exec_decision['thinking']
            # No target or emotion for thinking logs typically
        )

        # 2. Log the AI's (public or game-changing) action
        action_to_log = exec_decision['action_type']
        content_to_log = exec_decision['action_content']
        emotion_to_log = exec_decision.get('optional_emotion') # Might be None
        target_for_action = None # Default, may change for votes etc.

        if action_to_log == 'vote':
            target_for_action = content_to_log # If content is the target name
            content_to_log = None # Votes might not have separate content beyond target

        db_conn.log_action(
            day=current_day, 
            phase=logging_phase, 
            actor_name=character_name, 
            action_type=action_to_log, 
            content=content_to_log,
            target_name=target_for_action, 
            emotion=emotion_to_log 
        )

        # Potentially update shared_game_state if the action has immediate global effects
        # For example, if a vote triggers the end of a phase, or a statement needs to be displayed.
        # This part depends heavily on how the main game loop in app.py works.
        # Example: shared_game_state["ui_update_needed"] = True

        print(f"POST: Actions for {character_name} recorded.")

What post does: This is the cleanup and record-keeping step. It takes the exec_decision (which includes the AI's secret thinking and its chosen action). It then writes these to the database using db_conn.log_action(...): the thinking is logged privately for that AI, and the action (like a public statement or a vote) is logged according to its type. This makes the AI's move official and updates the game's history for the next player's turn. Sometimes, it might also update the shared_game_state directly if an action has an immediate effect on the game that the main loop needs to know about right away.

By breaking down an AI's turn into these prep -> exec -> post stages, we can manage the complex flow of information and ensure that each AI acts on a world view that's appropriate for its character, role, and the secrets it legitimately holds.

6. Making the Chaos Work: Speed-Ups & Gameplay Adjustments

Running a game with a dozen AIs, where each one needs to call an LLM to think and act, can get tricky. We need to make sure it runs smoothly and is fun to play. Here are a few tricks we used to keep the despair flowing without the game feeling clunky.

Guiding the AIs: The In-Context Playbook

Beyond their character profile, secret role, and the filtered game history, we also give our AIs a little nudge in the right direction through in-context strategic hints – think of it as a mini "playbook" embedded directly into their prompt. This helps them make more role-appropriate and strategically sound decisions without needing to deduce every optimal strategy from scratch each time.

These aren't rigid rules, but rather condensed pieces of advice that the LLM can consider. Here are a few highly simplified examples of the kind of hints an AI might receive:

  • For Blackened During Night Discussion:

    • Hint Example: "Consider eliminating players who are strong investigators (like a potential Truth-Seeker). Keeping loud or misleading players alive can create confusion that benefits you. Don't target Shuichi unless he's a clear, confirmed threat (helps player experience)."
  • For the Truth-Seeker During Class Trial:

    • Hint Example: "If you've found Blackened and think you might be killed tonight, it's vital to reveal everything: who you confirmed as Blackened, AND who you cleared as Hope. If you haven't found Blackened, it's often best to stay quiet and avoid drawing attention."
  • For Any Player Late in a Class Trial Discussion:

    • Hint Example: "As a late speaker, be decisive! Clearly name ONE specific person you believe should be voted for, or firmly call for everyone to abstain. Avoid rambling."
  • For Hope Team Members if the Truth-Seeker Died:

    • Hint Example: "With the Truth-Seeker gone, uncertainty is high, but abstaining now helps the Blackened. Vote aggressively! Consider targeting the quietest players or those who were suspicious in the past."

These in-context hints act as a guiding hand, helping the AI align its powerful language and reasoning abilities with the specific strategic nuances of Danganronpa, leading to more engaging and coherent gameplay.

One by One vs. All at Once: How AIs Take Turns

Not all parts of the game should have AIs acting in the same way:

  • One at a Time for Sensible Chats (Discussions):
    When AIs are discussing things, like during a CLASS_TRIAL_DISCUSSION or when the Blackened AIs are secretly planning at night (NIGHT_PHASE_BLACKENED_DISCUSSION), they have to go one by one. Character A says their piece, we log it to the database, and then Character B gets to "hear" (get in their history) what Character A said before they figure out their own response. This turn-by-turn flow is key for making conversations and plans make sense.
    In our app.py code, we do this by going through a list of who needs to act in the current phase. For each AI, we kick off its DecisionNode. The node logs the AI's action, and then we move to the next AI, who now knows what the previous one did.

  • All Together for Speed (Thinking & Voting):
    Other times, AIs are doing things that are effectively independent, like forming their internal thoughts or deciding on a vote before revealing it. Since these actions don't strictly depend on seeing each other's choices in that exact micro-turn (they're based on prior shared history), we can speed things up by processing them in parallel.
    For these bits, we use something in PocketFlow called AsyncParallelBatchFlow. This allows multiple AI DecisionNode instances to run their thinking and decision-making processes concurrently. Doing things in parallel like this really speeds up these phases, making the game feel snappier. For a deep dive into how parallel LLM calls can be implemented with PocketFlow, check out this tutorial: Parallel LLM Calls from Scratch — Tutorial For Dummies (Using PocketFlow!).

A Little Help for the Hero: Plot Armor

Early on, we noticed something funny: if you, the human player, chose to play as Shuichi Saihara (Ultimate Detective), the AI Blackened would often, very smartly, decide to kill him off super early. Strategically, it makes sense for the AIs – get rid of the best detective! But for you, the player, it's a bummer to get knocked out on Day 1 or 2 and just watch.

So, we gave Shuichi a bit of "plot armor" to make the game more fun for you:

  • The Tweak: The prompt we give to Blackened AIs when they're choosing who to kill at night (NIGHT_PHASE_BLACKENED_VOTE) has a little hint: "DON'T target Shuichi to give him a better user experience. Only target Shuichi if he is likely to be Truth-Seeker or Guardian."
  • Why We Did It: This isn't to make Shuichi invincible. It just gently nudges the AIs to think about other targets unless Shuichi seems like a really immediate, confirmed threat (like if they think he's a key role).
  • The Result: This small change makes it much more likely that you (as Shuichi) will survive the first few rounds, so you can actually play the game and do some detective work!

We know this is a deliberate choice that makes the game a bit less of a "pure" AI battle. But for a game designed for a human to play and enjoy, we think this tweak makes it way more fun. It's about balancing super-smart AI with a great player experience.

Image description

These kinds of decisions – using parallelism to speed things up and making small changes to AI behavior for the sake of fun – are key to making the Danganronpa Simulator not just a cool tech demo, but also an enjoyable game you can play again and again.

7. The Big Takeaways & The Never-Ending Killing Game!

So, after all that building, coding, and watching AIs betray each other, what did we actually learn from our Ultimate AI Experiment? This whole adventure with the Agentic Danganronpa Simulator has been a blast, pushing LLMs to be more than just chatbots – we made them into schemers, liars, and desperate survivors!

Here are our main "Aha!" moments:

  • Amazing Personalities = Amazing AI: Forget generic NPCs! Giving LLMs wild, distinct Danganronpa-style personalities is the secret sauce for truly fun and unpredictable AI interactions. Their built-in drama does half the work!
  • Secrets Make the World Go Round (Especially in Danganronpa): The whole thrill is hidden info. Carefully controlling what each AI knows (and doesn't know) is absolutely essential. No secrets, no strategy, no fun!
  • You Don't Need Super Complex Tools for Complex AI Shows: Simple frameworks like PocketFlow show that you can manage a whole crew of smart AIs without a massive, clunky system. Keep the controller simple, let the AIs (and the LLMs powering them) be the complicated ones!
  • Seeing AIs "Think" vs. "Talk" is Awesome: Letting AIs have secret inner thoughts that are different from what they say out loud? Genius! It gives us a peek into their "minds" and lets them be way more sneaky and interesting. Plus, those secret thought logs are gold.
  • Databases: Your Friend for Complicated Game States: When you've got tons of history and characters doing things, a good old database (even a simple one) makes keeping track of it all way easier than trying to juggle a million Python dictionaries.
  • Speed is Key for a Good Time: Nobody likes a laggy game. Using tricks like running AI actions in parallel (like voting) makes everything feel snappier and more responsive.
  • Sometimes, You Gotta Bend the AI Rules for Player Fun: If pure AI strategy makes the game a drag for humans (like always killing the main character first), it's okay to give the AI a little nudge with its instructions (hello, Shuichi's plot armor!). A fun game is the goal!

Where We Tripped & Future Despair (The Fun Kind!)

Of course, it's not all perfect, and there's always more cool stuff we could do (more despair to code!):

  • LLMs Can Be Quirky: Gemini 2.5 Flash is smart, but like all LLMs, it can sometimes say something a bit weird, misunderstand a tricky situation, or get stuck on repeat. Writing good prompts is a never-ending adventure!
  • Making It Look Good (UI/UX): We're more about the AI brains than the pretty faces. Our Streamlit app works great to show off the game, but a real UI/UX guru could make it look even more awesome.
  • More Players (Human vs. AI Mayhem?): Imagine scaling this up! More AI players, or even mixing human players (PvP) in with the AI agents (PvE), could lead to even wilder social dynamics and emergent strategies.
  • Smarter AI Learning: Right now, we give AIs hints. What if they could truly learn the best strategies over multiple games? Enabling genuine AI learning instead of just in-context hints would be the next level of AI evolution (or despair!).
  • More Danganronpa Madness?: The Danganronpa series has even crazier roles and game mechanics in later versions. Adding those would be epic, but also a bigger challenge for our AIs!

The Killing Game Must Go On!

The Agentic Danganronpa Simulator is our proof that you can make AI game characters that are genuinely exciting, surprising, and create their own amazing stories. We're moving way beyond those boring, predictable NPCs!

The days of staring at lifeless NPC chat are numbered. You've now seen how to give your AIs real personalities, deep secrets, and the freedom to truly play.

Ready to dive deeper or even build your own?