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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions