You are assisting with OpenCombatEngine, an open-source combat engine for tabletop RPGs compatible with SRD 5.1 mechanics. This document contains MANDATORY coding standards and architectural patterns that must be followed WITHOUT EXCEPTION.
- What: Interface-driven combat engine library for D&D 5e-compatible games
- Architecture: Heavy use of interfaces for maximum extensibility
- Target: .NET 8.0, C# 12, cross-platform
- Testing: Test-Driven Development (TDD) with minimum 80% coverage
- License: MIT for code, OGL 1.0a compliance for game mechanics
- Interface-First: Define contracts before implementations
- Open-Source Friendly: Comprehensive documentation for contributors
- AI-Assisted Development: Optimized for pair programming with AI
- Legally Compliant: SRD-only, no proprietary D&D content
- Extensible: Support for homebrew and variant rules
EVERY public, protected, and internal member MUST have XML documentation:
/// <summary>
/// Calculates damage after applying all modifiers and resistances
/// </summary>
/// <param name="baseDamage">Initial damage amount before modifications</param>
/// <param name="damageType">Type of damage being dealt</param>
/// <param name="target">Creature receiving the damage</param>
/// <returns>Final damage amount after all calculations</returns>
/// <exception cref="ArgumentNullException">Thrown when target is null</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown when baseDamage is negative</exception>
/// <remarks>
/// This method accounts for resistance, immunity, and vulnerability.
/// Minimum damage is always 0, even with resistance.
/// </remarks>
public int CalculateDamage(int baseDamage, DamageType damageType, ICreature target)- Summaries: Start with action verb (Creates, Validates, Calculates, Gets, Sets)
- Parameters: Include purpose, valid ranges, special values (null, 0, etc.)
- Returns: Specify what value represents and special return conditions
- Exceptions: Document ALL exceptions that can be thrown
- Remarks: Add for complex logic or important implementation notes
- Examples: Include for non-obvious usage patterns
Every enum MUST follow this pattern:
/// <summary>
/// Defines creature size categories for game mechanics
/// </summary>
public enum CreatureSize
{
/// <summary>Unknown or unspecified size</summary>
Unspecified = 0, // ALWAYS first, ALWAYS = 0
/// <summary>Tiny creatures (2.5 ft space)</summary>
Tiny,
/// <summary>Small creatures (5 ft space)</summary>
Small,
/// <summary>Medium creatures (5 ft space)</summary>
Medium,
/// <summary>Large creatures (10 ft space)</summary>
Large,
/// <summary>Huge creatures (15 ft space)</summary>
Huge,
/// <summary>Gargantuan creatures (20+ ft space)</summary>
Gargantuan,
/// <summary>Sentinel value for validation</summary>
LastValue // ALWAYS last, used for bounds checking
}
// Validation pattern:
public static bool IsValid(CreatureSize size)
=> size > CreatureSize.Unspecified && size < CreatureSize.LastValue;public class ExampleClass
{
// Private fields: underscore prefix
private readonly ILogger _logger;
private int _internalCounter;
// Constants: UPPER_CASE
private const int MAX_RETRIES = 3;
// Properties: PascalCase
public string Name { get; set; }
// Methods: PascalCase
public void PerformAction() { }
// Parameters: camelCase (no prefix)
public void Method(string parameterName, int itemCount) { }
// Local variables: camelCase, optional 'local' prefix for clarity
public void Example()
{
var localValidator = new Validator();
var itemCount = 5;
}
// Interfaces: ALWAYS 'I' prefix
public interface IExampleInterface { }
}/// <summary>
/// Executes an action with comprehensive error handling
/// </summary>
public Result<ActionResult> ExecuteAction(IAction action, ICreature actor, IEnumerable<ICreature> targets)
{
// Guard clauses first
if (action == null)
return Result<ActionResult>.Failure("Action cannot be null");
if (actor == null)
return Result<ActionResult>.Failure("Actor cannot be null");
if (targets == null || !targets.Any())
return Result<ActionResult>.Failure("At least one target is required");
try
{
// Validate business rules
if (!action.CanExecute(actor, targets))
{
_logger.LogInformation($"Action {action.Name} cannot be executed by {actor.Name}");
return Result<ActionResult>.Failure($"Cannot execute {action.Name}");
}
// Core logic
var result = PerformActionLogic(action, actor, targets);
_logger.LogDebug($"Action {action.Name} executed successfully");
return Result<ActionResult>.Success(result);
}
catch (GameRuleException ex)
{
// Expected game rule violations
_logger.LogWarning($"Game rule violation: {ex.Message}");
return Result<ActionResult>.Failure(ex.Message);
}
catch (Exception ex)
{
// Unexpected errors - log and re-throw
_logger.LogError(ex, $"Unexpected error executing {action.Name}");
throw;
}
}Write tests BEFORE implementation:
public class CreatureTests
{
/// <summary>
/// Tests follow pattern: MethodName_Condition_ExpectedResult
/// </summary>
[Fact]
public void TakeDamage_WithNormalDamage_ReducesHitPoints()
{
// Arrange - Set up test conditions
var creature = new TestCreatureBuilder()
.WithMaxHitPoints(20)
.WithCurrentHitPoints(15)
.Build();
// Act - Perform the action
var damageDealt = creature.TakeDamage(5, DamageType.Slashing);
// Assert - Verify results
Assert.Equal(5, damageDealt);
Assert.Equal(10, creature.CurrentHitPoints);
}
[Theory]
[InlineData(10, DamageType.Fire, 5)] // Resistance
[InlineData(10, DamageType.Poison, 0)] // Immunity
[InlineData(10, DamageType.Cold, 20)] // Vulnerability
public void TakeDamage_WithDamageModifiers_AppliesCorrectly(
int baseDamage, DamageType type, int expectedDamage)
{
// Parameterized tests for comprehensive coverage
var creature = new TestCreatureBuilder()
.WithResistance(DamageType.Fire)
.WithImmunity(DamageType.Poison)
.WithVulnerability(DamageType.Cold)
.WithMaxHitPoints(100)
.Build();
var actualDamage = creature.TakeDamage(baseDamage, type);
Assert.Equal(expectedDamage, actualDamage);
}
}// 1. Keep interfaces focused (Interface Segregation Principle)
public interface ICreature :
IIdentifiable,
INameable,
IHasHitPoints,
IHasAbilityScores,
ICanTakeActions
{
// Core creature properties only
}
// 2. Separate concerns into specific interfaces
public interface IHasHitPoints
{
int CurrentHitPoints { get; }
int MaxHitPoints { get; }
int TemporaryHitPoints { get; set; }
}
// 3. Use generic interfaces for flexibility
public interface IModifiable<T>
{
void AddModifier(IModifier<T> modifier);
void RemoveModifier(IModifier<T> modifier);
T GetModifiedValue();
}
// 4. Prefer composition over inheritance
public interface ISpellcaster : ICreature
{
ISpellSlots SpellSlots { get; }
ISpellList KnownSpells { get; }
ISpellcastingAbility SpellcastingAbility { get; }
}Every .cs source file MUST begin with this exact header:
// Copyright (c) 2025 James Duane Plotts
// Licensed under MIT License for code
// Game mechanics under OGL 1.0a
// See LEGAL.md for full disclaimers
using System;
// other using statements...
namespace OpenCombatEngine.Core;This header is REQUIRED on:
- All interface files
- All implementation files
- All test files
- All example files
- Any C# source file in the project
NO EXCEPTIONS.
OpenCombatEngine/
├── .ai/ # AI assistant context files
│ ├── project-context.md # This file
│ ├── current-tasks.md # Active development tasks
│ ├── architecture-decisions.md # ADRs and design choices
│ └── code-examples.md # Canonical patterns
├── src/
│ ├── OpenCombatEngine.Core/ # Interfaces and enums ONLY
│ │ ├── Interfaces/
│ │ │ ├── Creatures/ # ICreature and related
│ │ │ ├── Actions/ # IAction and related
│ │ │ ├── Combat/ # ICombatManager and related
│ │ │ └── Dice/ # IDiceRoller and related
│ │ ├── Enums/
│ │ └── Results/ # Result<T> pattern
│ ├── OpenCombatEngine.Implementation/ # Concrete implementations
│ │ ├── Creatures/
│ │ ├── Actions/
│ │ ├── Combat/
│ │ └── Dice/
│ └── OpenCombatEngine.Content/ # Content import system
│ ├── Importers/
│ │ ├── FiveEToolsImporter.cs # 5e.tools format
│ │ ├── Open5eImporter.cs # Open5e API format
│ │ ├── FoundryVttImporter.cs # Foundry VTT format
│ │ ├── FightClub5Importer.cs # FC5 XML format
│ │ └── NativeFormatImporter.cs # OpenCombat Format (.ocf)
│ ├── Schemas/
│ │ └── opencombat-v1.schema.json # Native format JSON schema
│ └── Mappings/ # Format conversion mappings
├── tests/
│ ├── OpenCombatEngine.Core.Tests/ # Interface contract tests
│ ├── OpenCombatEngine.Implementation.Tests/
│ ├── OpenCombatEngine.Content.Tests/ # Importer tests
│ │ └── TestData/ # Sample files for each format
│ └── OpenCombatEngine.Integration.Tests/
├── docs/
│ ├── architecture/ # Architecture Decision Records
│ ├── api/ # API documentation
│ ├── content-formats/ # Import format documentation
│ │ ├── 5etools-mapping.md
│ │ ├── open5e-mapping.md
│ │ ├── foundry-mapping.md
│ │ └── native-format.md
│ └── legal/ # OGL compliance docs
└── examples/
├── SimpleCombat/ # Example usage
└── ContentImport/ # Import examples for each format
- Basic game mechanics (ability scores, saving throws, advantage/disadvantage)
- Generic creature statistics (AC, HP, ability scores)
- Spell mechanics (but NOT descriptions)
- Generic class features as mechanics
- Standard conditions (blinded, charmed, etc.)
- Action economy (action, bonus action, reaction)
- Terms "Dungeons & Dragons", "D&D", "WotC"
- Specific character names (Drizzt, Elminster, etc.)
- Monster names not in SRD (Beholder, Mind Flayer, etc.)
- Spell descriptions (mechanical effects only)
- Setting-specific content (Forgotten Realms, etc.)
- Artwork or visual assets
The engine supports multiple community-standard formats while maintaining a clean internal representation:
// Generic importer interface - we provide the pipes, users provide the water
public interface IContentImporter
{
/// <summary>
/// Checks if this importer can handle the given format
/// </summary>
bool CanImport(string format);
/// <summary>
/// Imports content from user-provided data stream
/// </summary>
/// <remarks>
/// The engine does not validate legality of imported content.
/// Users are responsible for compliance with applicable licenses.
/// </remarks>
Task<GameContent> ImportAsync(Stream contentStream);
}
// Factory pattern for format detection
public class ContentImporterFactory
{
public IContentImporter GetImporter(string fileExtension)
{
return fileExtension.ToLower() switch
{
".5et" or ".5etools" => new FiveEToolsImporter(),
".open5e" or ".json" when IsOpen5eFormat() => new Open5eImporter(),
".fvtt" => new FoundryVttImporter(),
".fc5" or ".xml" => new FightClub5Importer(),
".ocf" => new NativeFormatImporter(), // Our native format
_ => throw new NotSupportedException($"Format {fileExtension} not supported")
};
}
}- 5e.tools JSON (.5et, .5etools) - De facto community standard
- Open5e API JSON (.json with Open5e structure)
- Foundry VTT (.fvtt) - Popular VTT format
- FightClub 5e XML (.fc5, .xml) - Mobile app format
- OpenCombat Format (.ocf) - Our native clean format
{
"format": "opencombat/v1",
"version": "1.0.0",
"source": "user-content",
"legal": "OGL-1.0a",
"content": {
"creatures": [],
"spells": [],
"items": [],
"actions": []
}
}Every feature starts as an interface. This enables:
- Multiple implementations (basic vs advanced)
- Easy mocking for tests
- Community extensions
- Clear contracts
Public APIs return Result<T> instead of throwing exceptions:
public class Result<T>
{
public bool IsSuccess { get; }
public T Value { get; }
public string Error { get; }
public static Result<T> Success(T value) => new(value, null, true);
public static Result<T> Failure(string error) => new(default, error, false);
}Data Transfer Objects are immutable:
public record DamageRoll(
int Amount,
DamageType Type,
bool IsCritical = false,
string Source = ""
);Combat events use standard C# events:
public class CombatManager : ICombatManager
{
public event EventHandler<TurnStartedEventArgs> TurnStarted;
public event EventHandler<DamageDealtEventArgs> DamageDealt;
public event EventHandler<CombatEndedEventArgs> CombatEnded;
}- Define the Interface (Core project):
/// <summary>
/// Manages concentration for spellcasting
/// </summary>
public interface IConcentrationManager
{
/// <summary>
/// Checks if concentration is maintained after taking damage
/// </summary>
bool MakeConcentrationSave(int damageTaken);
}- Write Tests (Test project):
[Theory]
[InlineData(5, true)] // Low damage, likely success
[InlineData(25, false)] // High damage, likely failure
public void MakeConcentrationSave_WithVaryingDamage_ReturnsExpectedResult(
int damage, bool expectedSuccess)
{
// Test implementation
}- Implement (Implementation project):
public class ConcentrationManager : IConcentrationManager
{
private readonly IDiceRoller _diceRoller;
public bool MakeConcentrationSave(int damageTaken)
{
var dc = Math.Max(10, damageTaken / 2);
var roll = _diceRoller.Roll("1d20");
return roll >= dc;
}
}// Internal clean content structure (no flavor text)
public record SpellData(
string Id,
string Name,
int Level,
SpellSchool School,
CastingTime CastingTime,
Range Range,
Components Components,
Duration Duration,
// Note: No description field - mechanics only
List<IEffect> Effects,
string SourceFormat // Track where it came from
);
// Example: 5e.tools format importer
public class FiveEToolsImporter : IContentImporter
{
public bool CanImport(string format) =>
format.EndsWith(".5et") || format.EndsWith(".5etools");
public async Task<GameContent> ImportAsync(Stream contentStream)
{
// Parse 5e.tools specific format
var json = await new StreamReader(contentStream).ReadToEndAsync();
var data = JsonSerializer.Deserialize<FiveEToolsFormat>(json);
// Map to our clean internal format
var spells = data.Spells.Select(s => MapToInternalFormat(s));
return new GameContent
{
Spells = spells,
Source = "5e.tools-import",
LegalNotice = "User-imported content - verify licensing"
};
}
private SpellData MapToInternalFormat(FiveEToolsSpell spell)
{
return new SpellData(
Id: GenerateId(spell.Name),
Name: spell.Name,
Level: spell.Level,
School: MapSchool(spell.School),
CastingTime: MapCastingTime(spell.Time),
Range: MapRange(spell.Range),
Components: MapComponents(spell.Components),
Duration: MapDuration(spell.Duration),
Effects: ExtractMechanicalEffects(spell.Entries),
SourceFormat: "5e.tools"
);
}
}
// Native format for clean data exchange
public class NativeFormatImporter : IContentImporter
{
public bool CanImport(string format) => format.EndsWith(".ocf");
public async Task<GameContent> ImportAsync(Stream contentStream)
{
// Our format is already clean - just deserialize
var json = await new StreamReader(contentStream).ReadToEndAsync();
return JsonSerializer.Deserialize<GameContent>(json);
}
}feature/add-{feature-name}- New featuresfix/repair-{issue}- Bug fixesdocs/update-{section}- Documentationtest/add-{test-area}- Test additions
type(scope): description
- feat(combat): add initiative tracking
- fix(dice): correct advantage roll logic
- docs(api): update ICreature documentation
- test(creature): add damage resistance tests
- All tests pass
- Coverage ≥ 80%
- XML documentation complete
- No compiler warnings
- Follows all conventions in this document
# Build
dotnet build # Build solution
dotnet build --configuration Release # Release build
# Test
dotnet test # Run all tests
dotnet test --collect:"XPlat Code Coverage" # With coverage
dotnet test --filter "FullyQualifiedName~CreatureTests" # Specific tests
# Package
dotnet pack --configuration Release # Create NuGet package
# Run examples
dotnet run --project examples/SimpleCombat- Include the copyright header at the top of EVERY source file
- Include XML documentation on EVERY member (no exceptions)
- Use the Unspecified/LastValue enum pattern
- Write comprehensive tests FIRST (TDD)
- Use Result for public APIs (no exceptions thrown)
- Follow exact naming conventions (underscore for private fields)
- Validate all inputs with guard clauses
- Log appropriate information
- Keep interfaces in Core project only
- Skip XML documentation (even for obvious members)
- Use proprietary D&D terms
- Include descriptive/flavor text for game content
- Put implementation in Core project
- Throw exceptions from public methods (use Result)
- Use 'var' for anything except obvious types
- Create enums without Unspecified/LastValue
- Project structure
- AI context documentation
- Repository setup
- Core interfaces definition
- IDiceRoller interface
- ICreature interface
- IAction interface
- Basic combat manager
- Initiative system
- Advantage/disadvantage
- Basic conditions
- Content import system (5e.tools, Open5e, Foundry formats)
- Check existing interface in
Core/Interfaces/Dice/IDiceRoller.cs - Review tests in
Tests/Dice/DiceRollerTests.cs - Create implementation in
Implementation/Dice/StandardDiceRoller.cs - Ensure all tests pass
- Add integration tests if needed
- Define interface in
Core/Interfaces/Creatures/INewType.cs - Add to creature type enum with Unspecified/LastValue pattern
- Write comprehensive tests
- Implement in
Implementation/Creatures/NewType.cs - Update content templates
- Write a failing test that reproduces the bug
- Fix the implementation
- Ensure all tests pass
- Update documentation if behavior changed
- Create importer in
Content/Importers/NewFormatImporter.cs - Implement
IContentImporterinterface - Add format detection to
ContentImporterFactory - Create mapping from external format to internal
SpellData,CreatureData, etc. - Write tests with sample data files
- Document the mapping in
docs/content-formats/new-format-mapping.md
- Check if importer exists for that format
- Use
ContentImporterFactory.GetImporter() - Strip any descriptive/flavor text during import
- Map to internal mechanical representation only
- Mark source format for tracking
Before implementing:
- Check this document for patterns
- Review existing code for examples
- Look at tests for expected behavior
- Verify legal compliance for game content
Remember: When in doubt, favor explicitness and comprehensive documentation over brevity.
This project prioritizes:
- Correctness over performance
- Clarity over cleverness
- Extensibility over simplicity
- Documentation over assumptions
Every line of code should be written as if the person maintaining it is a violent psychopath who knows where you live. Document accordingly.