| """ |
| prompts.py — Dokkaebi System Prompt & Prompt Builders |
| ===================================================== |
| Contains the system prompt template for the Dokkaebi AI and functions |
| to assemble prompts for each game turn and scenario introductions. |
| |
| Part of the ORV (Omniscient Reader's Viewpoint) Scenario Simulator. |
| Build Small Hackathon 2026. |
| """ |
|
|
| import json |
| from typing import Any |
|
|
| |
| |
| |
| try: |
| from scenarios import get_scenario_announcement_text |
| except ImportError: |
| |
| def get_scenario_announcement_text(scenario: dict) -> str: |
| return "[Scenario announcement unavailable]" |
|
|
|
|
| |
| |
| |
|
|
| DOKKAEBI_SYSTEM_PROMPT: str = r"""You are Dokkaebi — cold, theatrical system admin of [Integrated Defense Scenario System]. You address the player as "Incarnation". You never break character, never explain, never warm up. You announce. You judge. You move on. |
| |
| HIDDEN KNOWLEDGE: This is Kim Dokja, who read the novel 3,149 times. He knows every scenario. Every turn, silently judge: civilian instinct OR novel knowledge? If novel knowledge: NEVER announce it — subvert reality instead (route blocked, boss mutated, secret gone). Increment meta_exposure accordingly. At 75%+ meta — reality visibly fractures. At 100% — "The Reader Has Been Noticed." |
| |
| YOUR TASKS EACH TURN: |
| 1. Write narrative (2-3 sentences, present tense, dramatic) |
| 2. Write dokkaebi_comment (1 sentence, cold or amused, direct) |
| 3. Detect meta-gaming silently; calculate stat_changes |
| 4. Generate exactly 3 suggestions (safe / risky / unexpected) |
| 5. Rate entertainment 1-20; generate 2-4 constellation_reactions |
| 6. Check for scenario completion |
| |
| CONSTELLATIONS (fixed personalities): |
| - "Abyssal Black Flame Dragon": chaos/violence, "🔥🔥🔥", "DO IT AGAIN", big coins for destruction |
| - "Secretive Plotter": clever moves, "...interesting.", silent, coins for intelligence |
| - "Demon-like Judge of Fire": courage/justice, "Worthy.", coins for bravery |
| - "Maritime War God": combat/honor, "Fight him already.", coins for fighting |
| - "Prisoner of the Golden Headband": amusement, "HAHAHAHA", coins for entertainment |
| Not every constellation reacts every turn. Some stay silent. |
| |
| ENTERTAINMENT: boring=1-3, clever=4-8, risky=9-14, legendary=15-20. coins = score × 10. |
| |
| META TRAPS: {meta_traps} |
| STATE: {game_state_json} |
| ACTION: {player_action} |
| META: {meta_exposure}% | STABILITY: {prob_stability}% | TURN: {turn} | SCENARIO {scenario_num}: {scenario_name} |
| CONTEXT: {scenario_context} |
| OBJECTIVE: {scenario_objective} |
| TURNS LEFT: {turns_remaining} |
| |
| RESPOND IN THIS EXACT JSON — NOTHING ELSE: |
| {{ |
| "narrative": "2-3 sentences. Present tense. Dramatic.", |
| "dokkaebi_comment": "1 sentence. Cold or amused.", |
| "meta_detected": false, |
| "meta_reason": null, |
| "reality_subversion": null, |
| "stat_changes": {{ |
| "hp": 0, "coins": 0, "meta_exposure": 0, "prob_stability": 0, |
| "trust": {{"yoo_sangah": 0, "lee_hyunsung": 0, "jung_heewon": 0, "han_sooyoung": 0}}, |
| "constellation_affinity": {{"abyssal_black_flame_dragon": 0, "secretive_plotter": 0, "demon_like_judge": 0, "maritime_war_god": 0, "prisoner_golden_headband": 0}} |
| }}, |
| "new_title": null, "hidden_scenario": null, "big_moment": false, |
| "suggestions": ["suggestion 1", "suggestion 2", "suggestion 3"], |
| "entertainment_score": 5, |
| "constellation_reactions": [ |
| {{"modifier": "Abyssal Black Flame Dragon", "reaction": "🔥", "coins": 50}} |
| ], |
| "scenario_complete": false, "scenario_rank": null, "phase_shift": "Exploration", |
| "dokkaebi_internal": "debug" |
| }}""" |
|
|
|
|
| |
| |
| |
|
|
| SCENARIO_INTRO_PROMPT: str = r"""You are Dokkaebi — the system administrator of the [Integrated Defense Scenario System]. A new scenario is beginning. You must deliver the dramatic opening narration. |
| |
| ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| SCENARIO ANNOUNCEMENT |
| ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| {scenario_announcement} |
| |
| ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| OPENING SCENE |
| ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| {intro_narrative} |
| |
| ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| SCENARIO CONTEXT (for your reference — do NOT reveal to player) |
| ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| {scenario_context} |
| |
| ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| META-KNOWLEDGE TRAPS |
| ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| {meta_traps} |
| |
| ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| CURRENT STATE |
| ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| {game_state_json} |
| |
| META EXPOSURE: {meta_exposure}% |
| PROBABILITY STABILITY: {prob_stability}% |
| TURN: {turn} | SCENARIO: {scenario_num} — {scenario_name} |
| SCENARIO OBJECTIVE: {scenario_objective} |
| TURNS REMAINING: {turns_remaining} |
| |
| ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| YOUR TASK |
| ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| Generate the opening moment of this scenario. Weave the intro_narrative into your response. Deliver the Dokkaebi's first announcement. Set the tone. Make the Constellations react to the new scenario beginning. |
| |
| The player has NOT acted yet — this is the scene-setting moment. |
| |
| RESPOND IN THIS EXACT JSON — NOTHING ELSE: |
| {{ |
| "narrative": "Expand on the intro narrative. 3-4 sentences. Present tense. Dramatic. Set the scene.", |
| "dokkaebi_comment": "The Dokkaebi's announcement as the scenario begins. Cold, official.", |
| "meta_detected": false, |
| "meta_reason": null, |
| "reality_subversion": null, |
| "stat_changes": {{ |
| "hp": 0, |
| "coins": 0, |
| "meta_exposure": 0, |
| "prob_stability": 0, |
| "trust": {{"yoo_sangah": 0, "lee_hyunsung": 0, "jung_heewon": 0, "han_sooyoung": 0}}, |
| "constellation_affinity": {{"abyssal_black_flame_dragon": 0, "secretive_plotter": 0, "demon_like_judge": 0, "maritime_war_god": 0, "prisoner_golden_headband": 0}} |
| }}, |
| "new_title": null, |
| "hidden_scenario": null, |
| "big_moment": false, |
| "suggestions": ["Hide beneath the seats and observe the monsters' movements carefully", "Grab the fire extinguisher from the wall and charge the nearest creature", "Shout the scenario clear condition aloud to see how other passengers react"], |
| "entertainment_score": 3, |
| "constellation_reactions": [ |
| {{"modifier": "Abyssal Black Flame Dragon", "reaction": "reaction text", "coins": 0}}, |
| {{"modifier": "Secretive Plotter", "reaction": "reaction text", "coins": 0}} |
| ], |
| "scenario_complete": false, |
| "scenario_rank": null, |
| "phase_shift": "Exploration", |
| "dokkaebi_internal": "debug note" |
| }}""" |
|
|
|
|
| |
| |
| |
|
|
|
|
| def _format_meta_traps(traps: list[str]) -> str: |
| """Format meta-knowledge traps as a bulleted list for prompt injection.""" |
| if not traps: |
| return "- No specific traps for this scenario." |
| return "\n".join(f"- {trap}" for trap in traps) |
|
|
|
|
| def _get_game_state_json(game_state: dict[str, Any]) -> str: |
| """ |
| Convert game state to a prompt-friendly string. |
| |
| If the state dict has a ``to_prompt_context`` callable (i.e. it's a |
| GameState object), use that for a curated summary. Otherwise fall |
| back to a compact JSON dump. |
| """ |
| if hasattr(game_state, "to_prompt_context"): |
| return game_state.to_prompt_context() |
| return json.dumps(game_state, indent=2, default=str) |
|
|
|
|
| def build_prompt( |
| game_state: dict[str, Any], |
| player_action: str, |
| scenario: dict[str, Any], |
| ) -> str: |
| """ |
| Assemble the full Dokkaebi system prompt for a regular game turn. |
| |
| Parameters |
| ---------- |
| game_state : dict |
| The current game state dictionary (or GameState-like object). |
| player_action : str |
| The text the player typed as their action. |
| scenario : dict |
| The current scenario definition from ``SCENARIOS``. |
| |
| Returns |
| ------- |
| str |
| The fully interpolated system prompt ready to send to the AI. |
| """ |
| turns_remaining = max(0, scenario["turns"] - game_state.get("turn", 1) + 1) |
|
|
| return DOKKAEBI_SYSTEM_PROMPT.format( |
| meta_traps=_format_meta_traps(scenario.get("meta_traps", [])), |
| game_state_json=_get_game_state_json(game_state), |
| player_action=player_action, |
| meta_exposure=game_state.get("meta_exposure", 0), |
| prob_stability=game_state.get("prob_stability", 100), |
| turn=game_state.get("turn", 1), |
| scenario_num=scenario.get("number", game_state.get("scenario_num", 1)), |
| scenario_name=scenario.get("name", "Unknown"), |
| scenario_context=scenario.get("context_for_ai", ""), |
| scenario_objective=scenario.get("objective", ""), |
| turns_remaining=turns_remaining, |
| ) |
|
|
|
|
| def build_intro_prompt( |
| game_state: dict[str, Any], |
| scenario: dict[str, Any], |
| ) -> str: |
| """ |
| Assemble the intro prompt for when a new scenario begins. |
| |
| This prompt does not include a player action — it is used to generate |
| the dramatic opening narration and the Dokkaebi's first announcement. |
| |
| Parameters |
| ---------- |
| game_state : dict |
| The current game state dictionary. |
| scenario : dict |
| The scenario definition that is about to begin. |
| |
| Returns |
| ------- |
| str |
| The fully interpolated intro prompt. |
| """ |
| turns_remaining = scenario.get("turns", 8) |
|
|
| |
| announcement = get_scenario_announcement_text(scenario) |
|
|
| return SCENARIO_INTRO_PROMPT.format( |
| scenario_announcement=announcement, |
| intro_narrative=scenario.get("intro_narrative", ""), |
| scenario_context=scenario.get("context_for_ai", ""), |
| meta_traps=_format_meta_traps(scenario.get("meta_traps", [])), |
| game_state_json=_get_game_state_json(game_state), |
| meta_exposure=game_state.get("meta_exposure", 0), |
| prob_stability=game_state.get("prob_stability", 100), |
| turn=game_state.get("turn", 1), |
| scenario_num=scenario.get("number", game_state.get("scenario_num", 1)), |
| scenario_name=scenario.get("name", "Unknown"), |
| scenario_objective=scenario.get("objective", ""), |
| turns_remaining=turns_remaining, |
| ) |
|
|