11.1 Introduction: Orchestrating Complexity
Simulating The Traitors requires coordinating multiple interacting systems:
- State Management: Player states, emotions, relationships, secrets
- Phase Progression: Day/night cycle with proper event ordering
- Decision Generation: Strategic choices by AI agents
- Conversation Flow: Natural dialogue with proper turn-taking
- Outcome Determination: Vote tallying, murder execution, win conditions
The simulation framework serves as the conductor, ensuring these components interact correctly while capturing entertaining moments for potential output. This orchestration reflects the information asymmetry fundamental to the game's design.
11.2 Game State Architecture
11.2.1 Core Game State
type GameState struct {
// Identification
GameID string
StartTime time.Time
CurrentPhase GamePhase
DayNumber int
// Players
Players map[PlayerID]*PlayerState
Eliminated []*EliminationRecord
// Faction State
Traitors []PlayerID
Faithfuls []PlayerID
// Prize
CurrentPrize float64
MaxPrize float64
// Mechanics
ActiveShields map[PlayerID]bool
ActiveDaggers map[PlayerID]bool
OnTrialPlayers []PlayerID
// History
EventLog *EventLog
PhaseHistory []*PhaseRecord
// Configuration
Config *GameConfig
}
type GamePhase int
const (
PhaseBreakfast GamePhase = iota // Murder reveal
PhaseMission // Challenge execution
PhaseArmoury // Advantage distribution
PhaseRoundTable // Deliberation and vote
PhaseNight // Traitor conclave
)
11.2.2 Player State Aggregation
type PlayerState struct {
// Identity
ID PlayerID
Name string
Role Role // Faithful, OriginalTraitor, RecruitedTraitor
// Status
IsAlive bool
EliminatedBy EliminationType // Murder, Banishment, None
EliminatedDay int
// Psychology (from Chapter 10)
Emotion *EmotionState
Deception *DeceptionState
MaskingStrain *MaskingStrain
Secrets []*Secret
// Relationships
Relationships map[PlayerID]*Relationship
Alliances []*Alliance
// Knowledge
Knowledge *KnowledgeBase
Observations []*Observation
// Personality (immutable)
Personality *Personality
// Strategy
CurrentStrategy Strategy
StrategicMemory *StrategicMemory
}
The player state structure embodies the strategic archetypes framework, with personality traits and strategy fields that determine behavioural patterns. The cognitive memory architecture extends this with persistent memory systems.
11.2.3 Event Sourcing Architecture
All state changes flow through events:
type Event interface {
EventType() EventType
Timestamp() time.Time
Phase() GamePhase
Day() int
Participants() []PlayerID
Serialize() []byte
}
type EventLog struct {
Events []Event
Subscribers []EventSubscriber
}
func (el *EventLog) Append(event Event) {
el.Events = append(el.Events, event)
// Notify all subscribers
for _, subscriber := range el.Subscribers {
subscriber.OnEvent(event)
}
}
type EventSubscriber interface {
OnEvent(event Event)
}
11.2.4 State Reconstruction
Events enable state reconstruction:
func (g *GameState) ReconstructFromEvents(events []Event) error {
// Reset to initial state
g.reset()
for _, event := range events {
if err := g.applyEvent(event); err != nil {
return fmt.Errorf("event %d: %w", event.Timestamp(), err)
}
}
return nil
}
func (g *GameState) applyEvent(event Event) error {
switch e := event.(type) {
case *MurderEvent:
return g.applyMurder(e)
case *BanishmentEvent:
return g.applyBanishment(e)
case *RecruitmentEvent:
return g.applyRecruitment(e)
case *ShieldAwardEvent:
return g.applyShield(e)
case *VoteEvent:
return g.applyVote(e)
default:
// Non-state-changing events (conversations, etc.)
return nil
}
}
11.3 Game Orchestrator
11.3.1 Main Game Loop
type GameOrchestrator struct {
State *GameState
PhaseControllers map[GamePhase]PhaseController
ConversationMgr *ConversationManager
DecisionEngine *DecisionEngine
RAGService *RAGService
EmotionEngine *EmotionEngine
DeceptionEngine *DeceptionEngine
EditMomentTracker *EditMomentTracker
}
func (o *GameOrchestrator) Run() (*GameResult, error) {
// Initialize game
if err := o.initialize(); err != nil {
return nil, err
}
// Main game loop
for !o.isGameOver() {
// Execute current phase
if err := o.executePhase(o.State.CurrentPhase); err != nil {
return nil, fmt.Errorf("day %d, phase %s: %w",
o.State.DayNumber, o.State.CurrentPhase, err)
}
// Advance to next phase
o.advancePhase()
// Check end conditions between phases
o.checkWinConditions()
}
// Generate final results
return o.generateResult(), nil
}
11.3.2 Phase Controller Interface
type PhaseController interface {
Enter(state *GameState) error
Execute(state *GameState) (*PhaseResult, error)
Exit(state *GameState) error
}
type PhaseResult struct {
Events []Event
EditMoments []*EditMoment
NextPhase GamePhase
}
11.3.3 Phase Progression Logic
func (o *GameOrchestrator) advancePhase() {
current := o.State.CurrentPhase
switch current {
case PhaseBreakfast:
o.State.CurrentPhase = PhaseMission
case PhaseMission:
o.State.CurrentPhase = PhaseArmoury
case PhaseArmoury:
o.State.CurrentPhase = PhaseRoundTable
case PhaseRoundTable:
o.State.CurrentPhase = PhaseNight
case PhaseNight:
// Night ends, new day begins
o.State.DayNumber++
o.State.CurrentPhase = PhaseBreakfast
// Apply daily maintenance
o.applyDailyMaintenance()
}
}
func (o *GameOrchestrator) applyDailyMaintenance() {
for _, player := range o.State.Players {
if player.IsAlive {
// Decay emotions
player.Emotion.ApplyDecay()
// Reduce masking strain during night
player.MaskingStrain.ApplyRecovery()
// Age secrets
for _, secret := range player.Secrets {
secret.Age()
}
}
}
}
11.3.4 Win Condition Checking
func (o *GameOrchestrator) isGameOver() bool {
traitorCount := 0
faithfulCount := 0
for _, player := range o.State.Players {
if !player.IsAlive {
continue
}
if player.Role == Faithful {
faithfulCount++
} else {
traitorCount++
}
}
// Traitors win if they equal or outnumber Faithfuls
if traitorCount >= faithfulCount && faithfulCount > 0 {
return true
}
// Faithfuls win if all Traitors eliminated
if traitorCount == 0 {
return true
}
// Game continues with one player remaining
if traitorCount + faithfulCount <= 1 {
return true
}
return false
}
func (o *GameOrchestrator) determineWinner() WinCondition {
traitorCount := o.countLivingTraitors()
faithfulCount := o.countLivingFaithfuls()
if traitorCount == 0 {
return FaithfulVictory
}
if traitorCount >= faithfulCount {
return TraitorVictory
}
return GameOngoing
}
11.4 Breakfast Phase: Murder Reveal
11.4.1 Breakfast Controller
type BreakfastController struct {
ConversationMgr *ConversationManager
EmotionEngine *EmotionEngine
}
func (bc *BreakfastController) Execute(state *GameState) (*PhaseResult, error) {
result := &PhaseResult{
Events: []Event{},
EditMoments: []*EditMoment{},
}
// Check for murder victim from previous night
victim := bc.getMurderVictim(state)
if victim != nil {
// Generate murder reveal sequence
revealResult := bc.generateMurderReveal(state, victim)
result.Events = append(result.Events, revealResult.Events...)
result.EditMoments = append(result.EditMoments, revealResult.EditMoments...)
// Apply emotional impact to all players
bc.applyEmotionalImpact(state, victim)
}
// Generate breakfast conversations
conversations := bc.generateBreakfastConversations(state, victim)
for _, conv := range conversations {
result.Events = append(result.Events, conv.ToEvents()...)
}
return result, nil
}
11.4.2 Murder Reveal Sequence
func (bc *BreakfastController) generateMurderReveal(
state *GameState,
victim *PlayerState,
) *RevealResult {
result := &RevealResult{}
// Host announces missing player
announcement := &HostAnnouncementEvent{
Type: MurderAnnouncement,
Victim: victim.ID,
Script: bc.generateAnnouncementScript(victim),
}
result.Events = append(result.Events, announcement)
// Generate reactions from each living player
for _, player := range state.Players {
if !player.IsAlive || player.ID == victim.ID {
continue
}
reaction := bc.generateReaction(player, victim, state)
result.Events = append(result.Events, reaction)
// Check for edit moment (dramatic reaction)
if bc.isEditMomentReaction(reaction) {
result.EditMoments = append(result.EditMoments, &EditMoment{
Type: ReactionMoment,
Participants: []PlayerID{player.ID},
Content: reaction,
Score: bc.scoreReaction(reaction),
})
}
}
return result
}
11.4.3 Emotional Impact Processing
func (bc *BreakfastController) applyEmotionalImpact(
state *GameState,
victim *PlayerState,
) {
for _, player := range state.Players {
if !player.IsAlive {
continue
}
relationship := player.Relationships[victim.ID]
// Stronger emotional impact for close relationships
if relationship != nil && relationship.Closeness > 0.5 {
player.Emotion.ProcessMurderReveal(victim.ID, relationship.Closeness)
// If close ally was murdered, increase suspicion of everyone
for otherID := range player.Relationships {
if otherID != victim.ID {
player.Emotion.SuspicionMap[otherID] += 0.1
}
}
} else {
// Standard fear/paranoia increase
player.Emotion.Fear += 0.15
player.Emotion.Paranoia += 0.1
}
// Traitors suppress guilt if they participated
if player.Role != Faithful && bc.wasInvolved(player, victim) {
player.Emotion.Guilt += 0.3
player.MaskingStrain.CurrentStrain += 0.15
}
}
}
11.5 Mission Phase Simulation
11.5.1 Mission Controller
type MissionController struct {
MissionRepository *MissionRepository
DecisionEngine *DecisionEngine
}
type Mission struct {
ID string
Name string
Description string
Type MissionType
MaxPrize float64
Difficulty float64
TeamSize int
HasShield bool
AllowsSabotage bool
}
func (mc *MissionController) Execute(state *GameState) (*PhaseResult, error) {
result := &PhaseResult{}
// Select mission for today
mission := mc.selectMission(state)
// Select team (may involve player choices)
team := mc.selectTeam(state, mission)
// Execute mission
missionResult := mc.executeMission(state, mission, team)
result.Events = append(result.Events, missionResult.Events...)
// Distribute prize
prizeEvent := mc.distributePrize(state, missionResult)
result.Events = append(result.Events, prizeEvent)
// Handle shield if mission awards one
if mission.HasShield && missionResult.Success {
shieldEvent := mc.awardShield(state, team, missionResult)
result.Events = append(result.Events, shieldEvent)
}
return result, nil
}
11.5.2 Sabotage Mechanics
type SabotageDecision struct {
Saboteur PlayerID
Method SabotageMethod
Subtlety float64 // How hidden the sabotage is
Impact float64 // How much it reduces success
}
func (mc *MissionController) processSabotage(
state *GameState,
mission *Mission,
team []PlayerID,
) *SabotageDecision {
// Check for Traitors on team
for _, playerID := range team {
player := state.Players[playerID]
if player.Role == Faithful {
continue
}
// Traitor decides whether to sabotage
decision := mc.DecisionEngine.DecideSabotage(player, mission, state)
if decision.Sabotage {
return &SabotageDecision{
Saboteur: playerID,
Method: decision.Method,
Subtlety: decision.Subtlety,
Impact: mc.calculateImpact(decision),
}
}
}
return nil
}
11.5.3 Prize Accumulation
type PrizeResult struct {
Earned float64
AddedToPool float64
Lost float64 // From sabotage
}
func (mc *MissionController) calculatePrize(
mission *Mission,
missionResult *MissionResult,
sabotage *SabotageDecision,
) *PrizeResult {
basePrize := mission.MaxPrize * missionResult.SuccessRate
if sabotage != nil {
// Sabotage reduces prize
lostAmount := basePrize * sabotage.Impact
return &PrizeResult{
Earned: basePrize,
AddedToPool: basePrize - lostAmount,
Lost: lostAmount,
}
}
return &PrizeResult{
Earned: basePrize,
AddedToPool: basePrize,
Lost: 0,
}
}
11.6 Round Table Simulation
11.6.1 Round Table Controller
type RoundTableController struct {
ConversationMgr *ConversationManager
VotingEngine *VotingEngine
DecisionEngine *DecisionEngine
EditMomentTracker *EditMomentTracker
}
func (rtc *RoundTableController) Execute(state *GameState) (*PhaseResult, error) {
result := &PhaseResult{}
// Opening ceremony
opening := rtc.generateOpening(state)
result.Events = append(result.Events, opening)
// Discussion phase
discussion := rtc.conductDiscussion(state)
result.Events = append(result.Events, discussion.Events...)
result.EditMoments = append(result.EditMoments, discussion.EditMoments...)
// Voting phase
voteResult := rtc.conductVote(state)
result.Events = append(result.Events, voteResult.Events...)
// Banishment execution
banishment := rtc.executeBanishment(state, voteResult)
result.Events = append(result.Events, banishment)
// Role reveal (banished player reveals Faithful/Traitor)
reveal := rtc.conductReveal(state, voteResult.Banished)
result.Events = append(result.Events, reveal)
result.EditMoments = append(result.EditMoments, reveal.EditMoments...)
return result, nil
}
11.6.2 Discussion Generation
type DiscussionResult struct {
Exchanges []*ConversationExchange
Accusations []*Accusation
Defenses []*Defense
Events []Event
EditMoments []*EditMoment
}
func (rtc *RoundTableController) conductDiscussion(
state *GameState,
) *DiscussionResult {
result := &DiscussionResult{}
// Determine discussion duration (exchanges)
exchangeCount := rtc.calculateDiscussionLength(state)
speakingQueue := rtc.initializeSpeakingQueue(state)
for i := 0; i < exchangeCount; i++ {
// Select next speaker
speaker := speakingQueue.Next()
player := state.Players[speaker]
// Generate speaker's contribution
contribution := rtc.generateContribution(player, state, result)
result.Exchanges = append(result.Exchanges, contribution)
// Process contribution type
switch contribution.Type {
case ExchangeAccusation:
accusation := rtc.processAccusation(contribution)
result.Accusations = append(result.Accusations, accusation)
// Allow defense from accused
defense := rtc.generateDefense(
state.Players[accusation.Target], accusation, state,
)
result.Defenses = append(result.Defenses, defense)
result.Exchanges = append(result.Exchanges, defense.ToExchange())
case ExchangeDefense:
result.Defenses = append(result.Defenses,
rtc.processDefense(contribution))
case ExchangeObservation:
// Update suspicion maps based on observation
rtc.processObservation(contribution, state)
}
// Check for edit moments
if moment := rtc.EditMomentTracker.Evaluate(contribution); moment != nil {
result.EditMoments = append(result.EditMoments, moment)
}
// Update emotions based on exchange
rtc.updateEmotions(contribution, state)
}
return result
}
11.6.3 Accusation Generation
type Accusation struct {
Accuser PlayerID
Target PlayerID
Reasoning string
Evidence []Evidence
Confidence float64
Emotional float64 // How emotionally charged
}
func (rtc *RoundTableController) generateAccusation(
player *PlayerState,
state *GameState,
) *Accusation {
// Select target based on suspicion levels
target := rtc.selectAccusationTarget(player, state)
// Build reasoning using RAG
reasoning := rtc.buildReasoning(player, target, state)
// Gather evidence from player's observations
evidence := rtc.gatherEvidence(player, target)
// Calculate confidence and emotional level
confidence := player.Emotion.SuspicionMap[target]
emotional := player.Emotion.Anger + player.Emotion.Fear
// If accuser is Traitor, may be strategic misdirection
if player.Role != Faithful {
return rtc.craftTraitorAccusation(player, target, state)
}
return &Accusation{
Accuser: player.ID,
Target: target,
Reasoning: reasoning,
Evidence: evidence,
Confidence: confidence,
Emotional: emotional,
}
}
11.6.4 Vote Generation
type VoteResult struct {
Votes map[PlayerID]PlayerID // voter -> target
Tally map[PlayerID]int
Banished PlayerID
Events []Event
}
func (rtc *RoundTableController) conductVote(
state *GameState,
) *VoteResult {
result := &VoteResult{
Votes: make(map[PlayerID]PlayerID),
Tally: make(map[PlayerID]int),
}
// Each living player votes
for _, player := range state.Players {
if !player.IsAlive {
continue
}
// Cannot vote for shielded players (Australia variant)
if state.Config.ShieldBlocksBanishment {
target := rtc.generateVote(player, state, true)
result.Votes[player.ID] = target
} else {
target := rtc.generateVote(player, state, false)
result.Votes[player.ID] = target
}
}
// Tally votes
for _, target := range result.Votes {
result.Tally[target]++
}
// Apply dagger (double vote) if active
for playerID := range state.ActiveDaggers {
votedFor := result.Votes[playerID]
result.Tally[votedFor]++ // Count again
}
// Determine banished (plurality, tie = ?)
result.Banished = rtc.determineBanished(result.Tally, state)
return result
}
11.7 Night Phase Simulation
The night phase represents the Traitors' domain, where they exercise their information advantage to select victims. This phase embodies the hidden coordination that makes Traitor gameplay distinct from Faithful gameplay.
11.7.1 Night Controller
type NightController struct {
ConversationMgr *ConversationManager
DecisionEngine *DecisionEngine
}
func (nc *NightController) Execute(state *GameState) (*PhaseResult, error) {
result := &PhaseResult{}
// Check if any Traitors remain
if len(state.Traitors) == 0 {
// No night actions
return result, nil
}
// Traitor conclave
conclave := nc.conductConclave(state)
result.Events = append(result.Events, conclave.Events...)
result.EditMoments = append(result.EditMoments, conclave.EditMoments...)
// Murder decision
murderResult := nc.decideMurder(state, conclave)
result.Events = append(result.Events, murderResult)
// Recruitment decision (if applicable)
if nc.canRecruit(state) {
recruitResult := nc.decideRecruitment(state, conclave)
result.Events = append(result.Events, recruitResult.Events...)
}
// Clear shields for next day
nc.clearShields(state)
return result, nil
}
11.7.2 Murder Target Selection
type MurderDecision struct {
Target PlayerID
Reason string
Consensus float64 // How much Traitors agreed
}
func (nc *NightController) decideMurder(
state *GameState,
conclave *ConclaveResult,
) *MurderEvent {
// Count murder votes
voteTally := make(map[PlayerID]int)
for _, target := range conclave.MurderVotes {
voteTally[target]++
}
// Find target with most votes
var target PlayerID
maxVotes := 0
for player, votes := range voteTally {
if votes > maxVotes {
maxVotes = votes
target = player
}
}
// Cannot murder shielded players
if state.ActiveShields[target] {
// Shield blocks murder - select backup target
target = nc.selectBackupTarget(voteTally, state)
}
// Execute murder
victim := state.Players[target]
victim.IsAlive = false
victim.EliminatedBy = EliminationMurder
victim.EliminatedDay = state.DayNumber
state.Faithfuls = removePlayer(state.Faithfuls, target)
return &MurderEvent{
Victim: target,
Day: state.DayNumber,
Traitors: nc.getLivingTraitors(state),
}
}
11.7.3 Recruitment Mechanics
Recruitment represents a dramatic turning point in gameplay, transforming a Faithful into a Traitor. This mechanic, explored theoretically in the Puppet Master Hypothesis and the Secret Traitor analysis, creates some of the game's most compelling moments.
type RecruitmentResult struct {
Target PlayerID
Accepted bool
Events []Event
}
func (nc *NightController) decideRecruitment(
state *GameState,
conclave *ConclaveResult,
) *RecruitmentResult {
result := &RecruitmentResult{}
// Count recruitment votes
voteTally := make(map[PlayerID]int)
for _, target := range conclave.RecruitmentVotes {
if target != "" { // Empty = vote against recruiting
voteTally[target]++
}
}
// Must have consensus to recruit
traitorCount := len(state.Traitors)
for target, votes := range voteTally {
if votes >= traitorCount/2 + 1 { // Majority
result.Target = target
break
}
}
if result.Target == "" {
// No recruitment this night
return result
}
// Generate recruitment offer
offer := nc.generateRecruitmentOffer(state, result.Target)
result.Events = append(result.Events, offer)
// Target decides
targetPlayer := state.Players[result.Target]
decision := nc.generateRecruitmentDecision(targetPlayer, offer)
if decision.Accept {
result.Accepted = true
// Convert to Traitor
targetPlayer.Role = RecruitedTraitor
state.Faithfuls = removePlayer(state.Faithfuls, result.Target)
state.Traitors = append(state.Traitors, result.Target)
// Initialize Traitor state (from Chapter 10)
nc.DeceptionEngine.ProcessRecruitment(targetPlayer)
result.Events = append(result.Events, &RecruitmentAcceptEvent{
Recruited: result.Target,
Day: state.DayNumber,
})
} else {
result.Accepted = false
// Rejection = murder proceeds (in some versions)
if state.Config.RejectRecruitmentMeans == MurderProceeds {
// Murder the rejector
murder := nc.executeMurder(state, result.Target)
result.Events = append(result.Events, murder)
}
}
return result
}
11.8 Conversation Controllers
11.8.1 Conversation Manager
type ConversationManager struct {
RAGService *RAGService
EmotionEngine *EmotionEngine
DeceptionEngine *DeceptionEngine
SpeakingQueue *SpeakingQueue
}
type ConversationType int
const (
ConversationOneOnOne ConversationType = iota
ConversationSmallGroup
ConversationFullGroup
ConversationRoundTable
ConversationConclave
)
type Conversation struct {
ID string
Type ConversationType
Participants []PlayerID
Location string
Exchanges []*ConversationExchange
StartTime time.Time
Witnesses []PlayerID // Non-participants who can observe
}
11.8.2 Speaking Queue
type SpeakingQueue struct {
Queue []PlayerID
RecentSpeakers []PlayerID
Priorities map[PlayerID]float64
}
func NewSpeakingQueue(participants []PlayerID) *SpeakingQueue {
sq := &SpeakingQueue{
Queue: make([]PlayerID, 0),
Priorities: make(map[PlayerID]float64),
}
// Initialize with all participants
for _, p := range participants {
sq.Queue = append(sq.Queue, p)
sq.Priorities[p] = 0.5 // Base priority
}
// Shuffle initial order
rand.Shuffle(len(sq.Queue), func(i, j int) {
sq.Queue[i], sq.Queue[j] = sq.Queue[j], sq.Queue[i]
})
return sq
}
func (sq *SpeakingQueue) Next() PlayerID {
if len(sq.Queue) == 0 {
sq.refillQueue()
}
// Weight by priority
weights := make([]float64, len(sq.Queue))
for i, p := range sq.Queue {
weights[i] = sq.Priorities[p]
// Reduce priority if spoke recently
for j, recent := range sq.RecentSpeakers {
if recent == p {
weights[i] *= 0.5 * float64(j+1) / float64(len(sq.RecentSpeakers))
}
}
}
// Select weighted random
selected := sq.selectWeighted(weights)
speaker := sq.Queue[selected]
// Update recent speakers
sq.RecentSpeakers = append(sq.RecentSpeakers, speaker)
if len(sq.RecentSpeakers) > 5 {
sq.RecentSpeakers = sq.RecentSpeakers[1:]
}
return speaker
}
11.8.3 Dialogue Generation via RAG
func (cm *ConversationManager) generateExchange(
speaker *PlayerState,
target *PlayerState,
conv *Conversation,
context ConversationContext,
) *ConversationExchange {
// Build query for RAG
query := cm.buildDialogueQuery(speaker, target, conv, context)
// Enhance with emotional context
enhancedQuery := cm.RAGService.EnhanceQueryWithEmotion(query, speaker)
// Generate base response
response, _ := cm.RAGService.ProcessQueryWithOptions(enhancedQuery, QueryOptions{
ExpertID: "dialogue_expert",
Mode: AllowSynthesis,
})
// Apply deception modifications if needed
if speaker.DeceptionState.DeceptionMode != Authentic {
response = cm.DeceptionEngine.ModifyDialogue(response, speaker)
}
// Extract indicators
indicators := cm.DeceptionEngine.GenerateIndicators(speaker, response, context)
return &ConversationExchange{
Speaker: speaker.ID,
Content: response,
Type: cm.classifyExchangeType(response),
Emotion: speaker.DeceptionState.DisplayedEmotion,
Targets: []PlayerID{target.ID},
Indicators: indicators,
}
}
11.9 Edit Moment System
11.9.1 Edit Moment Types
type EditMomentType int
const (
ReactionMoment EditMomentType = iota // Strong emotional reaction
ConfrontationMoment // Heated accusation/defense
BetrayalMoment // Alliance break or vote betrayal
RevealMoment // Role reveal
CloseVoteMoment // Nail-biter vote
TraitorCaughtMoment // Traitor identified
WrongBanishmentMoment // Innocent banished
RecruitmentMoment // Recruitment offer/decision
MurderRevealMoment // Morning murder reveal
EmotionalBreakdownMoment // Player loses composure
)
type EditMoment struct {
Type EditMomentType
Participants []PlayerID
Content interface{}
Score float64 // Entertainment value [0, 1]
Timestamp time.Time
Tags []string
}
11.9.2 Edit Moment Tracker
type EditMomentTracker struct {
Moments []*EditMoment
Thresholds map[EditMomentType]float64
}
func (emt *EditMomentTracker) Evaluate(event interface{}) *EditMoment {
switch e := event.(type) {
case *ConversationExchange:
return emt.evaluateExchange(e)
case *VoteResult:
return emt.evaluateVote(e)
case *RevealEvent:
return emt.evaluateReveal(e)
case *Reaction:
return emt.evaluateReaction(e)
}
return nil
}
func (emt *EditMomentTracker) evaluateExchange(
exchange *ConversationExchange,
) *EditMoment {
score := 0.0
momentType := ReactionMoment
// High emotional intensity
if exchange.Emotion.GetIntensity() > 0.7 {
score += 0.3
}
// Accusation with evidence
if exchange.Type == ExchangeAccusation {
score += 0.2
momentType = ConfrontationMoment
}
// Deception indicators present
if len(exchange.Indicators) > 2 {
score += 0.25
}
// Contradicts previous statement
if emt.detectContradiction(exchange) {
score += 0.3
}
if score >= emt.Thresholds[momentType] {
return &EditMoment{
Type: momentType,
Participants: []PlayerID{exchange.Speaker},
Content: exchange,
Score: score,
Timestamp: time.Now(),
}
}
return nil
}
11.9.3 Moment Aggregation
type EpisodeHighlights struct {
TopMoments []*EditMoment
ByCategory map[EditMomentType][]*EditMoment
NarrativeArc []*EditMoment // Chronologically ordered key moments
}
func (emt *EditMomentTracker) GenerateHighlights(
momentLimit int,
) *EpisodeHighlights {
highlights := &EpisodeHighlights{
ByCategory: make(map[EditMomentType][]*EditMoment),
}
// Sort all moments by score
sorted := sortByScore(emt.Moments)
// Take top N
for i := 0; i < min(momentLimit, len(sorted)); i++ {
highlights.TopMoments = append(highlights.TopMoments, sorted[i])
}
// Categorize
for _, moment := range emt.Moments {
highlights.ByCategory[moment.Type] = append(
highlights.ByCategory[moment.Type], moment,
)
}
// Build narrative arc (select key moments chronologically)
highlights.NarrativeArc = emt.buildNarrativeArc()
return highlights
}
11.10 Complete Simulation Flow
11.10.1 Full Game Execution
func (o *GameOrchestrator) RunFullGame() (*GameResult, error) {
result := &GameResult{
Days: []*DayRecord{},
EditMoments: []*EditMoment{},
}
// Day loop
for day := 1; !o.isGameOver(); day++ {
dayRecord := &DayRecord{
DayNumber: day,
Phases: []*PhaseRecord{},
}
// Morning: Murder reveal
if day > 1 { // No murder on Day 1
breakfast := o.executePhase(PhaseBreakfast)
dayRecord.Phases = append(dayRecord.Phases, breakfast)
}
// Mission
mission := o.executePhase(PhaseMission)
dayRecord.Phases = append(dayRecord.Phases, mission)
// Armoury (some days)
if o.hasArmoury(day) {
armoury := o.executePhase(PhaseArmoury)
dayRecord.Phases = append(dayRecord.Phases, armoury)
}
// Round Table
roundTable := o.executePhase(PhaseRoundTable)
dayRecord.Phases = append(dayRecord.Phases, roundTable)
// Night
night := o.executePhase(PhaseNight)
dayRecord.Phases = append(dayRecord.Phases, night)
result.Days = append(result.Days, dayRecord)
}
// Determine winner
result.Winner = o.determineWinner()
result.FinalPlayers = o.getLivingPlayers()
result.PrizeSplit = o.calculatePrizeSplit()
// Compile edit moments
result.EditMoments = o.EditMomentTracker.GetAllMoments()
result.Highlights = o.EditMomentTracker.GenerateHighlights(20)
return result, nil
}
11.10.2 Result Structure
type GameResult struct {
// Outcome
Winner WinCondition
FinalPlayers []PlayerID
PrizeSplit map[PlayerID]float64
// History
Days []*DayRecord
EventLog []Event
// Entertainment
EditMoments []*EditMoment
Highlights *EpisodeHighlights
// Statistics
Stats *GameStatistics
}
type GameStatistics struct {
TotalDays int
TraitorsCaught int
TraitorsEscaped int
FaithfulsBanished int
FaithfulsMurdered int
RecruitmentAttempts int
RecruitmentSuccesses int
ShieldsAwarded int
ShieldsUsed int
DaggersUsed int
TotalPrize float64
AverageVoteMargin float64
}
11.11 Conclusion: Orchestrated Emergence
The simulation framework transforms the theoretical models from previous chapters into functioning gameplay:
- State Management: Event-sourced architecture enables replay and analysis
- Phase Controllers: Modular design allows mechanical variants
- Decision Generation: AI agents make strategic choices based on emotion, knowledge, and personality
- Conversation Flow: Natural dialogue emerges from RAG-powered generation with deception overlay
- Edit Moments: Entertainment-value tracking enables content curation
The result is not a scripted drama but an emergent narrative; the framework provides the rules and constraints, while the interactions between AI agents produce stories that unfold unpredictably.
This emergent quality is the ultimate goal: simulations that surprise even their creators while remaining mechanically sound and psychologically authentic. For optimal model selection, see the LLM evaluation and selection chapter.