official.ghost.logic commited on
Commit
a79cfc6
Β·
1 Parent(s): 0935d82

Phase 1: Improve session generation continuity

Browse files

Enhanced auto-generate next session with:
✨ Increase context from 3 to 5 previous sessions
✨ Extract continuity points (NPCs, hooks, locations, decisions)
✨ Add mandatory continuity requirements to AI prompt
✨ Enhanced prompt with core principles and checklist
✨ Emphasize cause-and-effect and player agency
✨ NPC memory and consequences tracking

Improvements:
- NPCs are automatically detected and tracked
- Unresolved hooks are identified and required to address
- Player decisions must have consequences
- Tone and pacing consistency emphasized
- Callbacks to earlier sessions enforced

Result: Sessions feel like natural progression instead of disconnected episodes

Files changed (1) hide show
  1. src/agents/campaign_agent.py +100 -23
src/agents/campaign_agent.py CHANGED
@@ -731,12 +731,14 @@ Format:
731
  current_session_num = len(session_events) + 1
732
  last_session = session_events[-1] if session_events else None
733
 
734
- # Get session notes from previous sessions (last 2-3 sessions for context)
735
  all_session_notes = self.get_session_notes(campaign_id)
736
- recent_notes = all_session_notes[:3] if all_session_notes else []
737
 
738
  # Build session notes context
739
  notes_context = ""
 
 
740
  if recent_notes:
741
  notes_context = "\n**PREVIOUS SESSION NOTES:**\n\n"
742
  for note in reversed(recent_notes): # Chronological order
@@ -747,6 +749,11 @@ Format:
747
  truncated_notes += "\n... (notes truncated)"
748
  notes_context += f"{truncated_notes}\n\n"
749
 
 
 
 
 
 
750
  # Build last session context
751
  if not notes_context:
752
  if last_session:
@@ -756,37 +763,52 @@ Format:
756
  else:
757
  last_session_info = notes_context
758
 
759
- # Build context for AI
760
- prompt = f"""You are an expert Dungeon Master planning the next D&D 5e session.
761
-
762
- **Campaign:** {campaign.name}
763
- **Theme:** {campaign.theme}
764
- **Setting:** {campaign.setting}
765
- **Current Arc:** {campaign.current_arc}
766
- **Party Size:** {campaign.party_size}
767
- **Session Number:** {current_session_num}
768
 
769
- **Main Conflict:** {campaign.main_conflict}
770
-
771
- **Key Factions:** {', '.join(campaign.key_factions) if campaign.key_factions else 'None yet'}
772
 
773
- **Major Villains:** {', '.join(campaign.major_villains) if campaign.major_villains else 'None yet'}
 
 
 
 
 
 
 
774
 
775
  {last_session_info}
776
 
777
- **Campaign Notes:**
778
  {campaign.notes[:1000] if campaign.notes else 'No additional notes'}
779
 
 
 
780
  ---
781
 
782
- Based on what happened in the previous sessions (see session notes above), generate a complete
783
- session plan for Session {current_session_num} that:
784
 
785
- 1. Builds on player choices and consequences documented in previous session notes
786
- 2. Follows up on narrative threads mentioned in the notes
787
- 3. Responds to how players engaged with NPCs and locations
788
- 4. Incorporates improvised content that worked well
789
- 5. Addresses unresolved plot hooks from the notes
 
 
 
 
 
 
 
 
 
 
790
 
791
  Include:
792
 
@@ -830,6 +852,61 @@ CLIFFHANGER: [cliffhanger sentence]
830
 
831
  return session_data
832
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
833
  def _parse_session_generation(self, ai_response: str) -> dict:
834
  """Parse AI response for session generation"""
835
  parsed = {}
 
731
  current_session_num = len(session_events) + 1
732
  last_session = session_events[-1] if session_events else None
733
 
734
+ # Get session notes from previous sessions (last 5 sessions for better context)
735
  all_session_notes = self.get_session_notes(campaign_id)
736
+ recent_notes = all_session_notes[:5] if all_session_notes else []
737
 
738
  # Build session notes context
739
  notes_context = ""
740
+ continuity_requirements = []
741
+
742
  if recent_notes:
743
  notes_context = "\n**PREVIOUS SESSION NOTES:**\n\n"
744
  for note in reversed(recent_notes): # Chronological order
 
749
  truncated_notes += "\n... (notes truncated)"
750
  notes_context += f"{truncated_notes}\n\n"
751
 
752
+ # Extract continuity requirements from most recent session
753
+ if recent_notes:
754
+ last_note = recent_notes[0] # Most recent
755
+ continuity_requirements = self._extract_continuity_points(last_note.notes)
756
+
757
  # Build last session context
758
  if not notes_context:
759
  if last_session:
 
763
  else:
764
  last_session_info = notes_context
765
 
766
+ # Build continuity section
767
+ continuity_section = ""
768
+ if continuity_requirements:
769
+ continuity_section = "\n**🎯 CONTINUITY REQUIREMENTS (You MUST address these):**\n"
770
+ for i, req in enumerate(continuity_requirements, 1):
771
+ continuity_section += f"{i}. {req}\n"
772
+ continuity_section += "\n**MANDATORY:** Reference AT LEAST 2-3 of the above elements in Session {current_session_num}.\n"
 
 
773
 
774
+ # Build context for AI
775
+ prompt = f"""You are an expert Dungeon Master planning Session {current_session_num} of "{campaign.name}".
 
776
 
777
+ **CAMPAIGN OVERVIEW:**
778
+ β€’ **Theme:** {campaign.theme}
779
+ β€’ **Setting:** {campaign.setting}
780
+ β€’ **Current Story Arc:** {campaign.current_arc}
781
+ β€’ **Party Size:** {campaign.party_size} adventurers
782
+ β€’ **Main Conflict:** {campaign.main_conflict}
783
+ β€’ **Key Factions:** {', '.join(campaign.key_factions) if campaign.key_factions else 'None established yet'}
784
+ β€’ **Major Villains:** {', '.join(campaign.major_villains) if campaign.major_villains else 'None revealed yet'}
785
 
786
  {last_session_info}
787
 
788
+ **Campaign DM Notes:**
789
  {campaign.notes[:1000] if campaign.notes else 'No additional notes'}
790
 
791
+ {continuity_section}
792
+
793
  ---
794
 
795
+ **YOUR MISSION:** Create Session {current_session_num} that feels like a natural, organic continuation of the story.
 
796
 
797
+ **CORE PRINCIPLES:**
798
+ 1. **Cause and Effect:** Player choices from previous sessions MUST have visible consequences
799
+ 2. **NPC Memory:** NPCs remember what players did - allies help, enemies retaliate, neutrals react
800
+ 3. **Narrative Threads:** Pick up unresolved hooks from previous sessions - don't let them disappear
801
+ 4. **Callbacks:** Reference earlier events to show the campaign has continuity and memory
802
+ 5. **Player Agency:** Respond to HOW players solved problems, not just THAT they solved them
803
+ 6. **Tone Consistency:** Match the established campaign tone - don't suddenly shift genre
804
+ 7. **Escalation:** Stakes should feel appropriate to campaign progression
805
+
806
+ **SESSION DESIGN CHECKLIST:**
807
+ βœ“ Does this session respond to player choices from last time?
808
+ βœ“ Have I included at least one recurring NPC?
809
+ βœ“ Do consequences feel earned and logical?
810
+ βœ“ Will players recognize this flows from their actions?
811
+ βœ“ Have I advanced an active plot thread?
812
 
813
  Include:
814
 
 
852
 
853
  return session_data
854
 
855
+ def _extract_continuity_points(self, session_notes: str) -> list[str]:
856
+ """
857
+ Extract key continuity points from session notes that should be
858
+ referenced in the next session.
859
+
860
+ Returns:
861
+ List of continuity requirements
862
+ """
863
+ continuity = []
864
+
865
+ # Extract NPCs mentioned (simple pattern matching)
866
+ import re
867
+
868
+ # Look for proper nouns (capitalized words that appear multiple times)
869
+ words = session_notes.split()
870
+ capitalized = [w.strip('.,!?:;') for w in words if w and w[0].isupper() and len(w) > 2]
871
+ npc_candidates = [w for w in capitalized if capitalized.count(w) >= 2]
872
+ if npc_candidates:
873
+ unique_npcs = list(set(npc_candidates))[:5] # Top 5
874
+ continuity.append(f"NPCs to remember: {', '.join(unique_npcs)}")
875
+
876
+ # Look for unresolved elements (keywords indicating future hooks)
877
+ unresolved_keywords = ['next time', 'later', 'tomorrow', 'unfinished', 'interrupted',
878
+ 'escaped', 'fled', 'promised', 'agreed to', 'will return',
879
+ 'warned', 'threatened', 'mysterious']
880
+ unresolved_sentences = []
881
+ for sentence in session_notes.split('.'):
882
+ if any(keyword in sentence.lower() for keyword in unresolved_keywords):
883
+ unresolved_sentences.append(sentence.strip())
884
+
885
+ if unresolved_sentences:
886
+ continuity.append(f"Unresolved hooks: {' | '.join(unresolved_sentences[:3])}")
887
+
888
+ # Look for locations mentioned
889
+ location_keywords = ['traveled to', 'arrived at', 'went to', 'heading to', 'in the', 'at the']
890
+ locations = []
891
+ for sentence in session_notes.split('.'):
892
+ for keyword in location_keywords:
893
+ if keyword in sentence.lower():
894
+ locations.append(sentence.strip())
895
+ break
896
+ if locations:
897
+ continuity.append(f"Recent locations: {' | '.join(locations[:3])}")
898
+
899
+ # Look for player decisions or consequences
900
+ decision_keywords = ['decided to', 'chose to', 'agreed to', 'refused to', 'killed', 'saved', 'helped']
901
+ decisions = []
902
+ for sentence in session_notes.split('.'):
903
+ if any(keyword in sentence.lower() for keyword in decision_keywords):
904
+ decisions.append(sentence.strip())
905
+ if decisions:
906
+ continuity.append(f"Player decisions needing consequences: {' | '.join(decisions[:3])}")
907
+
908
+ return continuity
909
+
910
  def _parse_session_generation(self, ai_response: str) -> dict:
911
  """Parse AI response for session generation"""
912
  parsed = {}