🔴 Breaking Analysis: The Secret Traitor / Red Cloak Explained → Read Now

Part IV: Computational Modelling

Chapter 11: Simulation Framework

~5,000 words

Abstract

This chapter presents the complete simulation framework for The Traitors, integrating the RAG architecture (Chapter 9) and emotion/deception engine (Chapter 10) into a fully functional game simulation. I detail the game orchestrator that manages phase progression and event sourcing, mission simulation with prize accumulation and shield distribution, Round Table simulation with discussion generation and voting, night phase execution with murder and recruitment, and conversation controllers that enable natural dialogue flow. The framework produces both game outcomes and "edit moments": high-entertainment-value segments suitable for viewer consumption.

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:

  1. State Management: Event-sourced architecture enables replay and analysis
  2. Phase Controllers: Modular design allows mechanical variants
  3. Decision Generation: AI agents make strategic choices based on emotion, knowledge, and personality
  4. Conversation Flow: Natural dialogue emerges from RAG-powered generation with deception overlay
  5. 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.

Thesis Contents