Skip to content

feat: Discord Gameplay Commands & Narrative Flow (spec #21) #22

@Fortinbra

Description

@Fortinbra

Feature Spec: Discord Gameplay Commands & Narrative Flow

Field Value
Feature Discord Gameplay Commands & Narrative Flow
Issue #TBD
Status Draft
Author The Doctor
Date 2026-04-10

Overview

What it does

Defines the core Discord slash commands players use to drive gameplay (/adventure, /action, /look, /inventory, /status). Commands are thin handlers that delegate to Core services, which build narrative context and call INarrativeEngine (spec #13). The Dungeon Master (Human) retains approval/override authority via existing /dm commands and a session-level autopilot toggle.

Why it's needed

Players need a consistent, low-friction way to interact with the story. This spec establishes a standard command surface, persistent session context per campaign, and the narrative flow from player command → AI proposal → Human DM approval → Discord response.

Out of scope


Architecture Notes (The Doctor)

Projects / layers touched

  • DungeonMaster.Bot — Discord.Net interaction modules for gameplay commands
  • DungeonMaster.Core — session context, gameplay command orchestration, domain models
  • DungeonMaster.Infrastructure — repositories for sessions/command log
  • DungeonMaster.Api — session status endpoints for DM tooling
  • DungeonMaster.Shared — DTOs for session status and command results

New interfaces in DungeonMaster.Core

  • IGameplaySessionService — lifecycle + context access for active sessions
  • IGameplayCommandService — executes /adventure, /action, /look, /inventory, /status
  • ISessionRepository — persistence for session state
  • IPlayerCommandRepository — append-only command log (for audit + replay)

Data flow

/action "I examine the altar"
  → Bot CommandHandler validates active campaign/session
  → GameplayCommandService.BuildContextAsync()
  → INarrativeEngine.RespondToPlayerActionAsync(context)
  → NarrativeResponse.RequiresHumanReview ?
       → DM approval (/dm approve|edit|reject|override)
       → Bot posts FinalText to channel
     : auto-approve
       → Bot posts Text to channel
  → PlayerCommand logged to repository

Integration points with existing systems


Domain Model

BaseEntity

All entities inherit BaseEntity with Id, CreatedAt, and UpdatedAt.

Entities

CampaignSession

  • CampaignId (Guid, required)
  • DiscordChannelId (ulong, required)
  • Status (SessionStatus enum)
  • StartedAt (DateTimeOffset, required)
  • EndedAt (DateTimeOffset?, optional)
  • AutopilotEnabled (bool, default false)
  • ActiveEncounterId (Guid?, optional)
  • LastNarrativeLogId (Guid?, optional)

SessionParticipant

  • SessionId (Guid FK)
  • PlayerCharacterId (Guid FK)
  • DiscordUserId (ulong)
  • IsDungeonMaster (bool)

PlayerCommand

  • SessionId (Guid FK)
  • PlayerCharacterId (Guid FK)
  • CommandType (GameplayCommandType enum)
  • RawText (string, optional)
  • IssuedAt (DateTimeOffset)

Enums

SessionStatus

  • Active
  • Paused
  • Completed

GameplayCommandType

  • Adventure
  • Action
  • Look
  • Inventory
  • Status

Records

public sealed record SessionStatusSnapshot(
    Guid SessionId,
    SessionStatus Status,
    bool AutopilotEnabled,
    Guid? ActiveEncounterId,
    DateTimeOffset StartedAt,
    DateTimeOffset? EndedAt);

Service Interfaces (CQRS)

public interface IGameplaySessionService
{
    Task<CampaignSession> StartSessionAsync(Guid campaignId, CancellationToken ct = default);
    Task<CampaignSession?> GetActiveSessionAsync(Guid campaignId, CancellationToken ct = default);
    Task PauseSessionAsync(Guid sessionId, CancellationToken ct = default);
    Task ResumeSessionAsync(Guid sessionId, CancellationToken ct = default);
    Task EndSessionAsync(Guid sessionId, CancellationToken ct = default);
    Task SetAutopilotAsync(Guid sessionId, bool enabled, CancellationToken ct = default);
}

public interface IGameplayCommandService
{
    Task<NarrativeResponse> ExecuteAdventureAsync(Guid sessionId, Guid playerId, CancellationToken ct = default);
    Task<NarrativeResponse> ExecuteActionAsync(Guid sessionId, Guid playerId, string actionText, CancellationToken ct = default);
    Task<NarrativeResponse> ExecuteLookAsync(Guid sessionId, Guid playerId, CancellationToken ct = default);
    Task<string> GetInventoryAsync(Guid sessionId, Guid playerId, CancellationToken ct = default);
    Task<SessionStatusSnapshot> GetStatusAsync(Guid sessionId, CancellationToken ct = default);
}

API Contract (Rory)

Endpoints

POST /api/campaigns/{id}/sessions

  • Auth: Bearer token (Dungeon Master only)
  • Response body (201): CampaignSession snapshot
  • Error responses: 404 Not Found (campaign), 409 Conflict (session already active)

GET /api/campaigns/{id}/sessions/active

  • Auth: Bearer token
  • Response body (200): CampaignSession snapshot or 204 No Content

PUT /api/sessions/{id}/status

  • Auth: Bearer token (Dungeon Master only)
  • Request body:
    { "newStatus": "Active | Paused | Completed" }
  • Response body (200): updated session snapshot

PUT /api/sessions/{id}/autopilot

  • Auth: Bearer token (Dungeon Master only)
  • Request body:
    { "enabled": true }
  • Response body (200): updated session snapshot

Discord Commands (Rory — Bot Layer)

Player commands

  • /adventure — advances the story (NarrativeType.SceneDescription)
  • /action {text} — player declares action; AI proposes response
  • /look — describes current location or encounter
  • /inventory — shows character loadout (from spec feat: Session Playback & Campaign Summaries (spec #26) #27 when available)
  • /status — shows session state, active encounter, and last narrative timestamp

DM commands

  • /dm autopilot on|off — toggles session autopilot (mirrors API)

NLP / AI Behaviour (Missy)

Trigger

Any /adventure, /action, or /look command with an active session.

Context sent to Ollama

Expected behaviour

  • Tone: immersive fantasy, second person where appropriate
  • Max length: 2–4 short paragraphs
  • Output must be a proposal unless AutopilotEnabled = true
  • Must not reveal hidden DM notes or NPC secrets unless the DM already revealed them

Edge cases in AI behaviour

  • No active session: return a human-readable error and do not call Ollama
  • Empty context: default to a short scene-setting prompt
  • Autopilot off: always set RequiresHumanReview = true

Test Scenarios (Danny) ⚠️ COMPLETE BEFORE IMPLEMENTATION

Happy path

  1. Given an active session in a campaign
    When a player runs /look
    Then the bot calls INarrativeEngine.GenerateLocationDescriptionAsync and posts the approved narrative

  2. Given autopilot is enabled for the session
    When a player runs /action
    Then the narrative response is posted immediately without DM approval

Edge cases

  1. Given no active session for the campaign
    When a player runs /adventure
    Then the bot returns an error message and no AI call occurs

  2. Given an active encounter
    When a player runs /status
    Then the response includes encounter status and current round/turn summary

Error / failure cases

  1. Given an unauthenticated DM tries /dm autopilot on
    When the bot checks authorization
    Then the command is rejected with an ephemeral error

  2. Given INarrativeEngine is unavailable
    When a player runs /action
    Then the bot returns a friendly error and logs the failure


Acceptance Criteria

Functional

  • Slash commands /adventure, /action, /look, /inventory, /status are registered via Discord.Net interactions
  • Command handlers are thin and delegate to Core services
  • Narrative output flows through INarrativeEngine with Human DM approval gating
  • Session context is persisted per campaign with an autopilot flag
  • /dm autopilot toggles session auto-approval
  • Player commands are logged for audit and summary generation

Non-functional

  • Commands respond within 2 seconds under normal load
  • All Discord IDs are stored as bigint with ulong conversion
  • No Discord.Net types leak into DungeonMaster.Core

Dependencies


Agent Work Breakdown

Agent Task Depends On
The Doctor Approve spec
Danny Write failing tests from Test Scenarios Spec approved
Rory Implement Core session/command services + Bot commands Danny's tests
Rory Add repositories + API endpoints Core entities
Danny Confirm all tests pass All implementation

Definition of Done

  • Test Scenarios section completed and approved by The Doctor before implementation
  • All failing tests written by Danny before implementation (TDD)
  • All tests written and passing (xUnit + bot command tests)
  • Code reviewed and approved by The Doctor
  • Command handlers in Bot are thin and delegate immediately
  • CampaignSession persisted with clear state transitions
  • Human DM approval/override flow enforced for narrative outputs
  • GitHub issue closed and linked to merged PR

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestspecSpecification work

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions