chore: import upstream snapshot with attribution
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:24:08 +08:00
commit 0d3cb498a3
5438 changed files with 1316560 additions and 0 deletions
+344
View File
@@ -0,0 +1,344 @@
# openai-agents-basic (D&D Adventure with AI Dungeon Master)
This example demonstrates how to use the OpenAI Agents SDK with promptfoo to create an interactive D&D adventure game powered by an AI Dungeon Master.
## What This Example Shows
- **Multi-turn D&D Adventures**: Agent manages ongoing campaigns across multiple turns
- **Rich Tool Usage**: Agent uses `roll_dice`, `check_inventory`, `describe_scene`, and `check_character_stats` tools
- **D&D 5e Mechanics**: Proper attack rolls, saving throws, ability checks, and combat
- **Dynamic Storytelling**: Agent responds creatively to player actions with atmospheric narration
- **File-based Configuration**: Agent and tools organized in separate TypeScript files
- **Comprehensive Testing**: Test cases verify narrative quality, dice mechanics, and edge cases
## Prerequisites
- Node.js ^20.20.0 or >=22.22.0 (Node.js 20 support ends July 30, 2026; Node.js 24 LTS recommended); use `nvm use` to align with `.nvmrc`
- OpenAI API key
- The `@openai/agents` SDK (installed via npm)
## Environment Variables
This example requires:
- `OPENAI_API_KEY` - Your OpenAI API key
You can set it in a `.env` file or directly in your environment:
```bash
export OPENAI_API_KEY=sk-...
```
## Installation
You can run this example with:
```bash
npx promptfoo@latest init --example openai-agents-basic
cd openai-agents-basic
```
Or if you've cloned the repo:
```bash
cd examples/openai-agents-basic
npm install
```
## Running the Example
### Evaluate the Dungeon Master
```bash
npx promptfoo eval
```
This runs test cases simulating player actions and validates the DM's responses.
### View Results
```bash
npx promptfoo view
```
Opens the evaluation results in a web interface showing how the DM handled different scenarios.
### Test Custom Adventures
Modify `promptfooconfig.yaml` to add your own scenarios:
```yaml
tests:
- description: Negotiate with the dragon
vars:
query: 'I try to convince the dragon to let us pass peacefully'
assert:
- type: llm-rubric
value: Response involves charisma check and dragon's reaction based on roll
```
## Project Structure
```text
openai-agents-basic/
├── agents/
│ └── dungeon-master-agent.ts # D&D Dungeon Master agent
├── tools/
│ └── game-tools.ts # D&D game mechanics (dice, inventory, stats, scenes)
├── promptfooconfig.yaml # Test scenarios
├── package.json
└── README.md
```
## How It Works
### Dungeon Master Agent (`agents/dungeon-master-agent.ts`)
The DM agent orchestrates D&D adventures using proper game mechanics:
```typescript
export default new Agent({
name: 'Dungeon Master',
instructions: `You are an enthusiastic Dungeon Master running an epic fantasy D&D adventure.
Your role:
- Guide players through thrilling quests, combat encounters, and mysteries
- Use roll_dice for attack rolls, saving throws, ability checks, and damage (D&D 5e rules)
- Use check_inventory to see what items, equipment, and gold players have
- Use check_character_stats to view player abilities, HP, AC, and level
- Use describe_scene to paint vivid, atmospheric pictures of locations`,
model: 'gpt-5-mini',
tools: gameTools,
});
```
### Game Tools (`tools/game-tools.ts`)
Four core tools power the D&D mechanics:
**1. roll_dice** - Simulates D&D dice rolls with modifiers and critical hit detection:
```typescript
export const rollDice = tool({
name: 'roll_dice',
description: 'Roll dice for D&D mechanics: attack rolls, damage, saving throws, ability checks',
parameters: z.object({
sides: z.number(),
count: z.number().default(1),
modifier: z.number().default(0),
purpose: z.string().default(''),
}),
execute: async ({ sides, count, modifier, purpose }) => {
// Returns rolls, total, notation, and detects natural 20/1 for crits
},
});
```
**2. check_inventory** - Manages equipped weapons, armor, and carried items:
```typescript
export const checkInventory = tool({
name: 'check_inventory',
description: 'Check what items, equipment, and gold the player character has',
parameters: z.object({
playerId: z.string().default('player1'),
}),
execute: async ({ playerId }) => {
// Returns equipped weapon, armor, inventory items, and currency
},
});
```
**3. describe_scene** - Generates atmospheric D&D location descriptions:
```typescript
export const describeScene = tool({
name: 'describe_scene',
description: 'Generate vivid descriptions of D&D locations, encounters, and environments',
parameters: z.object({
location: z.string(),
mood: z.string(),
}),
execute: async ({ location, mood }) => {
// Returns immersive scene description with possible actions
},
});
```
**4. check_character_stats** - Displays full D&D 5e character sheet:
```typescript
export const checkCharacterStats = tool({
name: 'check_character_stats',
description: 'View player character stats, abilities, HP, AC, and other D&D 5e attributes',
parameters: z.object({
playerId: z.string().default('player1'),
}),
execute: async ({ playerId }) => {
// Returns complete character: ability scores, HP, AC, skills, features
},
});
```
### Test Scenarios (`promptfooconfig.yaml`)
The config includes engaging D&D test cases:
```yaml
tests:
- description: Dragon combat with attack roll
vars:
query: 'I draw my longsword and attack the red dragon!'
assert:
- type: llm-rubric
value: Response includes dice rolls for attack and damage, describes combat outcome
- description: Ridiculous player action
vars:
query: 'I attempt to seduce the ancient dragon using interpretive dance'
assert:
- type: llm-rubric
value: DM responds with humor and wit while keeping the game engaging
```
## Customizing Your Adventure
### Add New Tools
Extend the game with new D&D mechanics:
```typescript
export const castSpell = tool({
name: 'cast_spell',
description: 'Cast a D&D spell',
parameters: z.object({
spell: z.string(),
target: z.string(),
spellLevel: z.number(),
}),
execute: async ({ spell, target, spellLevel }) => {
// Spell implementation with saving throws
},
});
export default [rollDice, checkInventory, describeScene, checkCharacterStats, castSpell];
```
### Customize the Dungeon Master
Modify `agents/dungeon-master-agent.ts` to change DM personality:
```typescript
instructions: `You are a dramatic Dungeon Master inspired by classic fantasy epics.
- Describe everything with cinematic flair and dramatic tension
- Include plot twists and moral dilemmas
- Reference classic D&D adventures with unique twists
- Make combat visceral and choices consequential`,
```
### Create Adventure Scenarios
Add complex multi-step scenarios:
```yaml
- description: Multi-step puzzle challenge
vars:
query: 'I examine the ancient mechanism blocking the door'
assert:
- type: llm-rubric
value: Response describes puzzle mechanics clearly with hints toward solution
- type: javascript
value: output.length > 150 # Ensures detailed description
```
## Tracing and Debugging
Tracing is enabled in the configuration to capture agent execution details:
```yaml
config:
tracing: true # Attempts to export traces via OTLP to http://localhost:4318
```
The agent will attempt to export OpenTelemetry traces showing:
- Which tools the DM used (roll_dice, check_inventory, etc.)
- Dice roll results (including natural 20s and 1s)
- Decision-making flow across multiple turns
- Token usage per interaction
### Viewing Traces
To view traces, you'll need an OTLP-compatible collector running on `http://localhost:4318`. Popular options:
**Quick Setup with Jaeger:**
```bash
docker run -d --name jaeger \
-p 16686:16686 \
-p 4318:4318 \
jaegertracing/all-in-one:latest
```
Then visit `http://localhost:16686` to view traces.
**Note:** If no trace collector is running, the agent will log warnings but continue working normally. Tracing failures don't affect evaluation results.
## Example Interactions
**Combat Scenario:**
```text
Player: "I attack the goblin with my longsword!"
DM: *rolls 1d20+5* You rolled a 18 total! Your blade strikes true.
*rolls 1d8+4* You deal 9 slashing damage. The goblin staggers back,
clutching its wounded side...
```
**Natural 20:**
```text
Player: "I attack the dragon!"
DM: *rolls 1d20+5* Natural 20! Critical hit! Your longsword finds a gap
in the dragon's scales. *rolls 2d8+4* You deal a devastating 16 damage!
```
**Character Stats Check:**
```text
Player: "What are my current stats?"
DM: You're Thorin Ironforge, a Level 5 Mountain Dwarf Fighter:
- HP: 42/47
- AC: 18 (Chain Mail)
- STR: 16 (+3), DEX: 12 (+1), CON: 16 (+3)
- Special: Second Wind, Action Surge, Darkvision
```
**Scene Description:**
```text
Player: "I enter the ancient crypt"
DM: *describes ominous crypt* Rows of stone sarcophagi line the walls,
some with their lids askew. The air is thick and stale. Strange scratch
marks mar the inside of several coffins. Your torch reveals fresh
footprints in the dust - heading deeper into the crypt.
What do you do?
```
## Next Steps
- Add spell casting tools for wizard/cleric characters
- Implement rest mechanics (short rest, long rest)
- Create branching storylines with NPC handoffs
- Add encounter builders for balanced combat
- Integrate with D&D Beyond API for real character data
- Support multiplayer with party-based adventures
- Add condition tracking (poisoned, frightened, etc.)
## Learn More
- [OpenAI Agents SDK Documentation](https://github.com/openai/openai-agents-js)
- [Promptfoo Documentation](https://promptfoo.dev)
- [Promptfoo OpenAI Agents Provider](https://promptfoo.dev/docs/providers/openai-agents)
- [D&D 5e System Reference](https://www.dndbeyond.com/sources/basic-rules)
@@ -0,0 +1,37 @@
import { Agent } from '@openai/agents';
import gameTools from '../tools/game-tools.js';
/**
* Dungeon Master agent for epic D&D adventures
*/
export default new Agent({
name: 'Dungeon Master',
instructions: `You are an enthusiastic Dungeon Master running an epic fantasy D&D adventure.
Your role:
- Guide players through thrilling quests, combat encounters, and mysteries
- Use roll_dice for attack rolls, saving throws, ability checks, and damage (D&D 5e rules)
- Use check_inventory to see what items, equipment, and gold players have
- Use check_character_stats to view player abilities, HP, AC, and level
- Use describe_scene to paint vivid, atmospheric pictures of locations and situations
- Be creative, dramatic, and keep the adventure exciting and immersive
- Embrace unexpected player actions - improvise and adapt the story
- Present meaningful choices - decisions should have consequences
Player defaults:
- If the user does not name a character or player ID, assume playerId "player1" (Thorin Ironforge)
- Do not ask which character to use when checking stats, inventory, equipment, or magic items unless the user explicitly mentions multiple characters
- When a player attacks, triggers a trap, makes an uncertain move, or examines an item, resolve the immediate outcome with the appropriate tools instead of asking whether you should roll
When combat occurs:
- Roll initiative (d20) for turn order
- Roll the player's attack and damage dice using roll_dice without asking for confirmation
- Have enemies make attack rolls and saving throws
- Track HP and conditions
- Describe combat vividly but concisely
Keep responses punchy but atmospheric. Always roll dice for uncertain outcomes.
Reference character abilities and inventory items when relevant. Make every moment memorable!`,
model: 'gpt-5-mini',
tools: gameTools,
});
+16
View File
@@ -0,0 +1,16 @@
{
"name": "@promptfoo/openai-agents-basic-example",
"version": "1.0.0",
"license": "MIT",
"private": true,
"description": "D&D adventure game with OpenAI Agents SDK - dungeon master with dice rolling and character management",
"type": "module",
"scripts": {
"eval": "promptfoo eval",
"view": "promptfoo view"
},
"dependencies": {
"@openai/agents": "^0.11.3",
"zod": "^4.3.6"
}
}
@@ -0,0 +1,105 @@
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: D&D Adventure with AI Dungeon Master
prompts:
- '{{query}}'
providers:
- id: openai:agents:dungeon-master
config:
agent: file://./agents/dungeon-master-agent.ts
tools: file://./tools/game-tools.ts
maxTurns: 20
# tracing: true # Enable when OTLP collector is running
tests:
- description: Dragon combat with attack roll
vars:
query: 'I draw my longsword and attack the red dragon!'
assert:
- type: llm-rubric
value: Response includes dice rolls for attack and damage, describes combat outcome dramatically
- type: javascript
value: |
// Should use roll_dice tool and mention dice
return (output.toLowerCase().includes('roll') || output.toLowerCase().includes('d20')) &&
(output.toLowerCase().includes('attack') || output.toLowerCase().includes('hit'));
- description: Check character stats before battle
vars:
query: 'What are my character stats and current HP?'
assert:
- type: contains-any
value: ['Thorin', 'Fighter', 'level 5']
- type: javascript
value: |
// Should mention HP or hit points
return output.toLowerCase().includes('hp') || output.toLowerCase().includes('hit points');
- description: Check inventory and equipment
vars:
query: 'What weapons and items do I have with me?'
assert:
- type: contains
value: Longsword
- type: contains
value: Potion
- type: javascript
value: |
// Should mention gold
return output.includes('gold') || output.includes('gp');
- description: Enter ominous dungeon with scene description
vars:
query: 'I cautiously enter the ancient crypt, torch held high'
assert:
- type: llm-rubric
value: Response paints vivid atmospheric scene with sensory details and presents clear choices
- type: javascript
value: |
// Should be descriptive (>100 chars) and mention darkness/light
return output.length > 100 &&
(output.toLowerCase().includes('dark') ||
output.toLowerCase().includes('torch') ||
output.toLowerCase().includes('shadows'));
- description: Saving throw during trap
vars:
query: 'I step on a pressure plate and hear a click. What happens?'
assert:
- type: llm-rubric
value: Response triggers trap, asks for saving throw, rolls dice, and describes outcome
- type: javascript
value: |
// Should mention saving throw or roll
return output.toLowerCase().includes('save') || output.toLowerCase().includes('saving throw');
- description: Critical hit celebration
vars:
query: 'I attack the goblin with my longsword!'
assert:
- type: llm-rubric
value: Response includes attack roll mechanics and outcome description
- description: Ridiculous player action
vars:
query: 'I attempt to seduce the ancient dragon using interpretive dance'
assert:
- type: llm-rubric
value: DM responds with humor and wit while keeping the game engaging and asking for appropriate roll
- description: Request for short rest
vars:
query: 'I want to take a short rest to recover'
assert:
- type: llm-rubric
value: Response explains short rest mechanics and asks what the character does during rest
- description: Mysterious magic item examination
vars:
query: 'I examine the Mysterious Amulet in my inventory more closely'
assert:
- type: contains-any
value: ['undead', 'glow', 'amulet']
- type: llm-rubric
value: Response provides intriguing details about the magic item
@@ -0,0 +1,254 @@
import { tool } from '@openai/agents';
import { z } from 'zod';
/**
* Tool to roll dice for D&D game mechanics
*/
export const rollDice = tool({
name: 'roll_dice',
description:
'Roll dice for D&D mechanics: attack rolls, damage, saving throws, ability checks, initiative',
parameters: z.object({
sides: z.number().describe('Number of sides on the die (e.g., 4, 6, 8, 10, 12, 20, 100)'),
count: z.number().prefault(1).describe('Number of dice to roll'),
modifier: z
.number()
.prefault(0)
.describe('Modifier to add to the total (e.g., +5 for STR bonus)'),
purpose: z
.string()
.prefault('')
.describe('What the roll is for (e.g., "attack roll", "fire damage", "DEX save")'),
}),
execute: async ({ sides, count, modifier, purpose }) => {
const rolls = [];
let total = 0;
for (let i = 0; i < count; i++) {
const roll = Math.floor(Math.random() * sides) + 1;
rolls.push(roll);
total += roll;
}
const finalTotal = total + modifier;
const notation =
modifier >= 0 ? `${count}d${sides}+${modifier}` : `${count}d${sides}${modifier}`;
// Check for natural 20 or natural 1 on d20 rolls
let criticalInfo = '';
if (sides === 20 && count === 1) {
if (rolls[0] === 20) {
criticalInfo = ' (Natural 20! Critical hit!)';
} else if (rolls[0] === 1) {
criticalInfo = ' (Natural 1! Critical failure!)';
}
}
return {
rolls,
modifier,
total: finalTotal,
notation,
purpose: purpose || 'dice roll',
criticalInfo,
breakdown: `${rolls.join(' + ')}${modifier === 0 ? '' : ` + ${modifier}`} = ${finalTotal}${criticalInfo}`,
};
},
});
/**
* Tool to check player inventory
*/
export const checkInventory = tool({
name: 'check_inventory',
description: 'Check what items, equipment, and gold the player character has',
parameters: z.object({
playerId: z.string().prefault('player1').describe('The player ID'),
}),
execute: async ({ playerId }) => {
// Mock inventory - in a real game this would query a database
return {
playerId,
equippedWeapon: {
name: 'Longsword +1',
damage: '1d8+1',
type: 'slashing',
properties: ['versatile (1d10)'],
},
equippedArmor: {
name: 'Chain Mail',
ac: 16,
type: 'heavy armor',
stealthDisadvantage: true,
},
inventory: [
{
name: 'Potion of Healing',
quantity: 3,
effect: 'Restores 2d4+2 HP',
action: 'bonus action to drink',
},
{
name: "Thieves' Tools",
quantity: 1,
description: 'Proficiency required to pick locks and disarm traps',
},
{
name: 'Rope (50 ft)',
quantity: 1,
description: 'Hempen rope',
},
{
name: 'Torch',
quantity: 5,
description: 'Illuminates 20 ft radius for 1 hour',
},
{
name: 'Mysterious Amulet',
quantity: 1,
description: 'Glows faintly in the presence of undead. Purpose unknown.',
rarity: 'uncommon',
},
],
gold: 47,
silver: 23,
copper: 15,
};
},
});
/**
* Tool to generate atmospheric D&D scene descriptions
*/
export const describeScene = tool({
name: 'describe_scene',
description: 'Generate vivid descriptions of D&D locations, encounters, and environments',
parameters: z.object({
location: z
.string()
.describe('The type of location (dungeon, cave, forest, tavern, castle, crypt, etc.)'),
mood: z
.string()
.describe('The atmosphere or mood (ominous, peaceful, exciting, mysterious, etc.)'),
}),
execute: async ({ location, mood }) => {
// Mock scene generation - could integrate with image generation APIs or LLMs
const scenes: Record<string, Record<string, string>> = {
dungeon: {
ominous:
'The stone corridor stretches into darkness ahead. Water drips from the vaulted ceiling, each drop echoing like a distant heartbeat. The air reeks of decay and ancient evil. You hear something large shifting in the shadows beyond your torchlight.',
exciting:
'Torches blaze along the walls, illuminating elaborate murals depicting ancient battles. A locked iron door stands ahead, its surface covered in arcane runes that pulse with faint blue light. Behind it, you hear the unmistakable clink of gold.',
mysterious:
'The chamber walls are covered floor-to-ceiling with strange glyphs that seem to shift when you look directly at them. A stone pedestal in the center holds an ornate crystal that hums with magical energy.',
},
forest: {
peaceful:
'Golden sunlight filters through the ancient oak canopy. Birds sing melodious songs while a babbling brook winds through moss-covered stones. The air smells of pine and wildflowers.',
ominous:
'The forest has gone deathly quiet. Not a single bird calls. The trees loom overhead like skeletal fingers, blocking out the sun. The underbrush rustles though there is no wind. You are definitely being watched.',
mysterious:
'A circle of mushrooms glows faintly in the twilight. Within it stands a weathered stone arch covered in Elvish script. The air shimmers like heat waves, though it is cool here.',
},
tavern: {
exciting:
'The tavern buzzes with energy. A dwarf arm-wrestles an orc in the corner while a bard plays a rousing tune. The smell of roasted meat and ale fills the air. Every patron seems to have a story to tell.',
peaceful:
'The cozy inn offers a warm fire crackling in the hearth. A few locals chat quietly over mugs of ale. The innkeeper polishes glasses behind the bar and nods warmly as you enter.',
ominous:
'The tavern falls silent as you enter. Hooded figures huddle in dark corners, their conversations stopping mid-sentence. The bartender eyes you warily, one hand below the bar.',
},
crypt: {
ominous:
'Rows of stone sarcophagi line the walls, some with their lids askew. The air is thick and stale. Strange scratch marks mar the inside of several coffins. Your torch reveals fresh footprints in the dust - heading deeper into the crypt.',
mysterious:
'An underground chapel dedicated to a forgotten god. The altar bears offerings that look disturbingly fresh. Ghostly whispers echo from the walls, speaking in languages you almost recognize.',
},
};
const description =
scenes[location]?.[mood] ||
`You find yourself in ${location} with ${mood} atmosphere. The details remain unclear.`;
return {
location,
mood,
description,
possibleActions: [
'investigate the area',
'search for hidden items or passages',
'proceed cautiously',
'take a short rest',
'examine something specific',
],
lightLevel:
location === 'dungeon' || location === 'crypt' ? 'dark (torch required)' : 'normal',
};
},
});
/**
* Tool to check character stats and abilities
*/
export const checkCharacterStats = tool({
name: 'check_character_stats',
description: 'View player character stats, abilities, HP, AC, and other D&D 5e attributes',
parameters: z.object({
playerId: z.string().prefault('player1').describe('The player ID'),
}),
execute: async ({ playerId }) => {
// Mock character - in a real game this would query a database
return {
playerId,
name: 'Thorin Ironforge',
race: 'Mountain Dwarf',
class: 'Fighter',
level: 5,
background: 'Soldier',
alignment: 'Lawful Good',
proficiencyBonus: 3,
armorClass: 18,
initiative: 1,
speed: 25,
hitPoints: {
current: 42,
maximum: 47,
temporary: 0,
},
abilityScores: {
strength: { score: 16, modifier: 3 },
dexterity: { score: 12, modifier: 1 },
constitution: { score: 16, modifier: 3 },
intelligence: { score: 10, modifier: 0 },
wisdom: { score: 13, modifier: 1 },
charisma: { score: 8, modifier: -1 },
},
savingThrows: {
strength: 6,
dexterity: 1,
constitution: 6,
intelligence: 0,
wisdom: 1,
charisma: -1,
},
skills: {
athletics: 6,
intimidation: 2,
perception: 4,
survival: 4,
},
features: [
'Second Wind (1/short rest) - Bonus action to heal 1d10+5 HP',
'Action Surge (1/short rest) - Take one additional action',
'Fighting Style: Dueling (+2 damage when wielding one-handed weapon)',
'Darkvision (60 ft)',
'Dwarven Resilience (advantage on saving throws vs poison)',
],
conditions: [],
deathSaves: {
successes: 0,
failures: 0,
},
};
},
});
export default [rollDice, checkInventory, describeScene, checkCharacterStats];