Skip to main content
Glama
npc-ai-decision-making.test.ts79.9 kB
/** * COMPREHENSIVE NPC AI DECISION-MAKING TESTS * * Tests for AI decision-making patterns across different domains: * - Combat AI: Tactical choices, target prioritization, spell selection, movement * - Social AI: Conversation choices, relationship responses, social intent handling * - Strategic AI: Nation-level decisions, diplomacy, resource allocation * - Adaptive AI: Responses to changing circumstances and player actions * - AI Consistency: Personality consistency across time and scenarios * - Multi-Character Coordination: Group dynamics, competing priorities * - Environmental Awareness: World-state understanding and reactions * * Focus on testing AI BEHAVIORAL patterns, not just mechanical systems. * Include edge cases like contradictory traits, moral dilemmas, complex multi-step decisions. * * Run: npm test -- tests/server/npc-ai-decision-making.test.ts */ import { describe, test, expect, beforeEach, afterEach } from 'vitest'; import Database from 'better-sqlite3'; import { v4 as uuid } from 'uuid'; import { migrate } from '../../src/storage/migrations.js'; import { setDb, closeDb } from '../../src/storage/index.js'; import { CharacterRepository } from '../../src/storage/repos/character.repo.js'; import { NpcMemoryRepository } from '../../src/storage/repos/npc-memory.repo.js'; import { PartyRepository } from '../../src/storage/repos/party.repo.js'; import { NationRepository } from '../../src/storage/repos/nation.repo.js'; import { DiplomacyRepository } from '../../src/storage/repos/diplomacy.repo.js'; import { SpatialRepository } from '../../src/storage/repos/spatial.repo.js'; import { WorldRepository } from '../../src/storage/repos/world.repo.js'; import { RoomNode } from '../../src/schema/spatial.js'; // Import tool handlers for AI testing import { handleCreateEncounter, handleGetEncounterState, handleExecuteCombatAction, handleAdvanceTurn } from '../../src/server/combat-tools'; import { handleGetNpcRelationship, handleUpdateNpcRelationship, handleRecordConversationMemory, handleGetNpcContext, handleInteractSocially } from '../../src/server/npc-memory-tools'; import { handleStrategyTool } from '../../src/server/strategy-tools'; // Test utilities let db: Database.Database; let charRepo: CharacterRepository; let memoryRepo: NpcMemoryRepository; let partyRepo: PartyRepository; let nationRepo: NationRepository; let diplomacyRepo: DiplomacyRepository; let spatialRepo: SpatialRepository; let worldRepo: WorldRepository; const mockCtx = { sessionId: 'test-session' }; beforeEach(() => { db = new Database(':memory:'); migrate(db); setDb(db); charRepo = new CharacterRepository(db); memoryRepo = new NpcMemoryRepository(db); partyRepo = new PartyRepository(db); nationRepo = new NationRepository(db); diplomacyRepo = new DiplomacyRepository(db); spatialRepo = new SpatialRepository(db); worldRepo = new WorldRepository(db); worldRepo.create({ id: 'test-world', name: 'Test World', seed: 'test-seed', width: 100, height: 100, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }); }); afterEach(() => { closeDb(); }); // Helper function to create test characters function createTestCharacter(overrides: Partial<any> = {}) { const id = overrides.id || uuid(); const characterType = overrides.characterType || overrides.type || 'npc'; const characterData: any = { id, name: overrides.name || `Test Character ${id.slice(-4)}`, stats: overrides.stats || { str: 10, dex: 10, con: 10, int: 10, wis: 10, cha: 10 }, hp: overrides.hp || 20, maxHp: overrides.maxHp || 20, ac: overrides.ac || 10, level: overrides.level || 1, characterType: characterType, characterClass: overrides.characterClass || 'Fighter', knownSpells: overrides.knownSpells || [], preparedSpells: overrides.preparedSpells || [], cantripsKnown: overrides.cantripsKnown || [], maxSpellLevel: overrides.maxSpellLevel || 0, conditions: overrides.conditions || [], resistances: overrides.resistances || [], vulnerabilities: overrides.vulnerabilities || [], immunities: overrides.immunities || [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), ...overrides }; // Add NPC-specific fields if (characterType !== 'pc') { characterData.behavior = overrides.behavior || 'typical'; if (overrides.factionId) characterData.factionId = overrides.factionId; } charRepo.create(characterData); return charRepo.findById(id)!; } // Helper function to simulate AI decision-making scenarios function simulateAiDecision(characterId: string, context: any) { // This would be replaced with actual AI decision logic // For now, we'll simulate basic decision patterns return { decision: 'mock_decision', reasoning: 'Based on personality traits and context', confidence: 0.8 }; } function createTestRoom(overrides: Partial<RoomNode> = {}): RoomNode { return { id: overrides.id || uuid(), name: overrides.name || 'Test Room', baseDescription: overrides.baseDescription || 'A generic test room', biomeContext: overrides.biomeContext || 'urban', atmospherics: overrides.atmospherics || [], exits: overrides.exits || [], entityIds: overrides.entityIds || [], visitedCount: 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), ...overrides }; } function extractJsonFromResponse(text: string): any { try { return JSON.parse(text); } catch (e) { const match = text.match(/<!-- STATE_JSON\n([\s\S]*?)\nSTATE_JSON -->/); if (match) return JSON.parse(match[1]); throw e; } } // ============================================================================= // CATEGORY 1: COMBAT AI DECISION-MAKING // ============================================================================= describe('Category 1: Combat AI Decision-Making', () => { describe('Target Prioritization Logic', () => { test('1.1 - Aggressive AI prioritizes threatening targets', async () => { // Setup combat scenario const tank = createTestCharacter({ name: 'Ironclad Tank', behavior: 'aggressive', stats: { str: 16, dex: 8, con: 14, int: 8, wis: 10, cha: 6 }, characterClass: 'Fighter', hp: 35, ac: 18 }); const healer = createTestCharacter({ name: 'Mercy Healer', behavior: 'protective', stats: { str: 8, dex: 12, con: 12, int: 10, wis: 16, cha: 14 }, characterClass: 'Cleric', hp: 18, ac: 12 }); const assassin = createTestCharacter({ name: 'Shadow Assassin', behavior: 'cunning', stats: { str: 10, dex: 18, con: 10, int: 14, wis: 12, cha: 8 }, characterClass: 'Rogue', hp: 16, ac: 14 }); // Create combat encounter const encounterResult = await handleCreateEncounter({ seed: 'aggressive-targeting', participants: [ { id: tank.id, name: tank.name, initiativeBonus: 2, hp: tank.hp, maxHp: tank.maxHp, conditions: [] }, { id: healer.id, name: healer.name, initiativeBonus: 1, hp: healer.hp, maxHp: healer.maxHp, conditions: [] }, { id: assassin.id, name: assassin.name, initiativeBonus: 3, hp: assassin.hp, maxHp: assassin.maxHp, conditions: [] } ] }, mockCtx); const encounterId = extractJsonFromResponse(encounterResult.content[0].text).encounterId; // Get encounter state to analyze AI decisions const state = await handleGetEncounterState({ encounterId }, mockCtx); // Test: Aggressive AI should analyze threat levels expect(state.participants).toBeDefined(); expect(state.turnOrder).toBeDefined(); // Verify AI considers multiple factors for target selection const tankParticipant = state.participants.find((p: any) => p.id === tank.id); expect(tankParticipant).toBeDefined(); }); test('1.2 - Defensive AI prioritizes protecting allies', async () => { const defender = createTestCharacter({ name: 'Shield Guardian', behavior: 'defensive', stats: { str: 14, dex: 10, con: 16, int: 10, wis: 12, cha: 8 }, characterClass: 'Paladin', hp: 28, ac: 16 }); const ally = createTestCharacter({ name: 'Fragile Mage', behavior: 'focused', stats: { str: 6, dex: 14, con: 8, int: 16, wis: 10, cha: 12 }, characterClass: 'Wizard', hp: 12, ac: 10 }); const enemy = createTestCharacter({ name: 'Bloodthirsty Orc', behavior: 'aggressive', stats: { str: 16, dex: 12, con: 14, int: 6, wis: 8, cha: 8 }, characterClass: 'Barbarian', hp: 24, ac: 12 }); const encounterResult = await handleCreateEncounter({ seed: 'defensive-protecting', participants: [ { id: defender.id, name: defender.name, initiativeBonus: 1, hp: defender.hp, maxHp: defender.maxHp, conditions: [] }, { id: ally.id, name: ally.name, initiativeBonus: 0, hp: ally.hp, maxHp: ally.maxHp, conditions: [] }, { id: enemy.id, name: enemy.name, initiativeBonus: 2, hp: enemy.hp, maxHp: enemy.maxHp, conditions: [] } ] }, mockCtx); const encounterId = extractJsonFromResponse(encounterResult.content[0].text).encounterId; // Test defensive AI decision-making const state = await handleGetEncounterState({ encounterId }, mockCtx); // Defensive AI should prioritize ally protection over personal glory const defenderParticipant = state.participants.find((p: any) => p.id === defender.id); expect(defenderParticipant).toBeDefined(); }); test('1.3 - Cunning AI uses positioning and environment', async () => { const tactician = createTestCharacter({ name: 'Master Tactician', behavior: 'cunning', stats: { str: 10, dex: 14, con: 12, int: 16, wis: 14, cha: 10 }, characterClass: 'Bard', hp: 20, ac: 13 }); const brute = createTestCharacter({ name: 'Simple Brute', behavior: 'aggressive', stats: { str: 16, dex: 8, con: 14, int: 6, wis: 8, cha: 4 }, characterClass: 'Fighter', hp: 30, ac: 15 }); // Create encounter focused on tactical positioning const encounterResult = await handleCreateEncounter({ seed: 'cunning-tactics', participants: [ { id: tactician.id, name: tactician.name, initiativeBonus: 3, hp: tactician.hp, maxHp: tactician.maxHp, conditions: [] }, { id: brute.id, name: brute.name, initiativeBonus: 1, hp: brute.hp, maxHp: brute.maxHp, conditions: [] } ] }, mockCtx); const encounterId = extractJsonFromResponse(encounterResult.content[0].text).encounterId; // Test tactical AI considers positioning, flanking, environmental factors const state = await handleGetEncounterState({ encounterId }, mockCtx); const tacticianParticipant = state.participants.find((p: any) => p.id === tactician.id); expect(tacticianParticipant).toBeDefined(); }); }); describe('Spell Selection and Resource Management', () => { test('1.4 - Intelligent AI chooses spells strategically', async () => { const spellcaster = createTestCharacter({ name: 'Battle Mage', behavior: 'intelligent', stats: { str: 8, dex: 12, con: 12, int: 16, wis: 12, cha: 10 }, characterClass: 'Wizard', hp: 16, ac: 12, level: 5, knownSpells: ['fireball', 'shield', 'healing word', 'lightning bolt'] }); const opponent1 = createTestCharacter({ name: 'Weakling', behavior: 'neutral', hp: 8, ac: 8 }); const opponent2 = createTestCharacter({ name: 'Tank', behavior: 'defensive', hp: 25, ac: 16 }); const encounterResult = await handleCreateEncounter({ seed: 'spell-selection', participants: [ { id: spellcaster.id, name: spellcaster.name, initiativeBonus: 2, hp: spellcaster.hp, maxHp: spellcaster.maxHp, conditions: [] }, { id: opponent1.id, name: opponent1.name, initiativeBonus: 1, hp: opponent1.hp, maxHp: opponent1.maxHp, conditions: [] }, { id: opponent2.id, name: opponent2.name, initiativeBonus: 0, hp: opponent2.hp, maxHp: opponent2.maxHp, conditions: [] } ] }, mockCtx); const encounterId = extractJsonFromResponse(encounterResult.content[0].text).encounterId; // Test: AI should analyze spell effectiveness vs targets const state = await handleGetEncounterState({ encounterId }, mockCtx); // Intelligent AI would choose between AoE (fireball) vs single target (lightning bolt) // vs defensive spells (shield) based on situation expect(state.participants).toBeDefined(); }); test('1.5 - Conservative AI manages spell slots carefully', async () => { const frugalMage = createTestCharacter({ name: 'Frugal Mage', behavior: 'cautious', stats: { str: 6, dex: 10, con: 10, int: 18, wis: 14, cha: 8 }, characterClass: 'Wizard', hp: 14, ac: 10, level: 3, knownSpells: ['magic missile', 'shield', 'sleep'] }); const weakEnemy = createTestCharacter({ name: 'Goblin', hp: 6, ac: 10 }); const encounterResult = await handleCreateEncounter({ seed: 'resource-management', participants: [ { id: frugalMage.id, name: frugalMage.name, initiativeBonus: 2, hp: frugalMage.hp, maxHp: frugalMage.maxHp, conditions: [] }, { id: weakEnemy.id, name: weakEnemy.name, initiativeBonus: 1, hp: weakEnemy.hp, maxHp: weakEnemy.maxHp, conditions: [] } ] }, mockCtx); const encounterId = extractJsonFromResponse(encounterResult.content[0].text).encounterId; // Test: AI should prefer low-level spells when appropriate // vs saving high-level spells for tougher opponents const state = await handleGetEncounterState({ encounterId }, mockCtx); const mageParticipant = state.participants.find((p: any) => p.id === frugalMage.id); expect(mageParticipant).toBeDefined(); }); }); describe('Movement and Positioning', () => { test('1.6 - Tactically-minded AI uses positioning', async () => { const ranger = createTestCharacter({ name: 'Woodland Ranger', behavior: 'strategic', stats: { str: 12, dex: 16, con: 12, int: 12, wis: 14, cha: 8 }, characterClass: 'Ranger', hp: 22, ac: 14 }); const melee1 = createTestCharacter({ name: 'Orc Warrior 1', behavior: 'aggressive', hp: 20, ac: 13 }); const melee2 = createTestCharacter({ name: 'Orc Warrior 2', behavior: 'aggressive', hp: 20, ac: 13 }); const encounterResult = await handleCreateEncounter({ seed: 'positioning-tactics', participants: [ { id: ranger.id, name: ranger.name, initiativeBonus: 3, hp: ranger.hp, maxHp: ranger.maxHp, conditions: [] }, { id: melee1.id, name: melee1.name, initiativeBonus: 1, hp: melee1.hp, maxHp: melee1.maxHp, conditions: [] }, { id: melee2.id, name: melee2.name, initiativeBonus: 1, hp: melee2.hp, maxHp: melee2.maxHp, conditions: [] } ] }, mockCtx); const encounterId = extractJsonFromResponse(encounterResult.content[0].text).encounterId; // Test: Ranged AI should maintain distance, use kiting tactics const state = await handleGetEncounterState({ encounterId }, mockCtx); // Strategic AI should consider flanking, cover, positioning advantages expect(state.participants).toBeDefined(); }); }); }); // ============================================================================= // CATEGORY 2: SOCIAL AI DECISION-MAKING // ============================================================================= describe('Category 2: Social AI Decision-Making', () => { describe('Relationship Response Logic', () => { test('2.1 - Friendly NPC remembers and reciprocates kindness', async () => { const hero = createTestCharacter({ name: 'Helpful Hero', characterType: 'pc', stats: { str: 12, dex: 12, con: 12, int: 12, wis: 14, cha: 14 } }); const merchant = createTestCharacter({ name: 'Honest Merchant', behavior: 'friendly', stats: { str: 8, dex: 12, con: 10, int: 14, wis: 12, cha: 16 } }); // Simulate previous positive interaction await handleUpdateNpcRelationship({ characterId: hero.id, npcId: merchant.id, familiarity: 'friend', disposition: 'friendly', notes: 'Helped save my caravan from bandits' }, mockCtx); // Record positive conversation memory await handleRecordConversationMemory({ characterId: hero.id, npcId: merchant.id, summary: 'Hero defended merchant caravan from bandits', importance: 'high', topics: ['bandits', 'defense', 'gratitude'] }, mockCtx); // Test: Merchant should show increased friendliness const relationship = await handleGetNpcRelationship({ characterId: hero.id, npcId: merchant.id }, mockCtx); const relationshipData = JSON.parse(relationship.content[0].text); expect(relationshipData.familiarity).toBe('friend'); expect(relationshipData.disposition).toBe('friendly'); expect(relationshipData.notes).toContain('bandits'); }); test('2.2 - Revenge-minded NPC holds grudges', async () => { const hero = createTestCharacter({ name: 'Hero', characterType: 'pc' }); const vengeful = createTestCharacter({ name: 'Vengeful Noble', behavior: 'vengeful', stats: { str: 10, dex: 10, con: 12, int: 14, wis: 12, cha: 14 } }); // Simulate previous betrayal await handleUpdateNpcRelationship({ characterId: hero.id, npcId: vengeful.id, familiarity: 'enemy', disposition: 'hostile', notes: 'Betrayed me and stole my family heirloom' }, mockCtx); await handleRecordConversationMemory({ characterId: hero.id, npcId: vengeful.id, summary: 'Hero stole the Sacred Chalice from my family vault', importance: 'critical', topics: ['theft', 'betrayal', 'revenge'] }, mockCtx); // Test: Vengeful NPC should maintain hostility const relationship = await handleGetNpcRelationship({ characterId: hero.id, npcId: vengeful.id }, mockCtx); const relationshipData = JSON.parse(relationship.content[0].text); expect(relationshipData.familiarity).toBe('enemy'); expect(relationshipData.disposition).toBe('hostile'); }); test('2.3 - Pragmatic NPC adapts based on circumstances', async () => { const hero = createTestCharacter({ name: 'Powerful Warrior', characterType: 'pc', stats: { str: 18, dex: 14, con: 16, int: 10, wis: 12, cha: 10 } }); const pragmatist = createTestCharacter({ name: 'Pragmatic Captain', behavior: 'pragmatic', stats: { str: 14, dex: 12, con: 14, int: 14, wis: 14, cha: 12 }, factionId: 'city-guard' }); // Initial neutral relationship await handleUpdateNpcRelationship({ characterId: hero.id, npcId: pragmatist.id, familiarity: 'acquaintance', disposition: 'neutral', notes: 'Local guard captain' }, mockCtx); // Test: Pragmatic NPC should adapt disposition based on hero's power/usefulness const relationship = await handleGetNpcRelationship({ characterId: hero.id, npcId: pragmatist.id }, mockCtx); const relationshipData = JSON.parse(relationship.content[0].text); expect(relationshipData.familiarity).toBe('acquaintance'); // Pragmatic AI would recognize hero's strength and become more helpful // This would be tested with actual AI logic }); }); describe('Conversation Response Patterns', () => { test('2.4 - Shy NPC responds differently than confident NPC', async () => { const hero = createTestCharacter({ name: 'Charismatic Hero', characterType: 'pc', stats: { str: 12, dex: 12, con: 12, int: 12, wis: 12, cha: 18 } }); const shyNpc = createTestCharacter({ name: 'Shy Librarian', behavior: 'shy', stats: { str: 6, dex: 10, con: 8, int: 16, wis: 14, cha: 6 } }); const confidentNpc = createTestCharacter({ name: 'Confident Merchant', behavior: 'confident', stats: { str: 10, dex: 12, con: 12, int: 12, wis: 10, cha: 16 } }); // Create room and add characters const room = createTestRoom({ entityIds: [hero.id, shyNpc.id, confidentNpc.id] }); spatialRepo.create(room); spatialRepo.addEntityToRoom(room.id, hero.id); spatialRepo.addEntityToRoom(room.id, shyNpc.id); spatialRepo.addEntityToRoom(room.id, confidentNpc.id); charRepo.update(hero.id, { currentRoomId: room.id }); charRepo.update(shyNpc.id, { currentRoomId: room.id }); charRepo.update(confidentNpc.id, { currentRoomId: room.id }); // Test conversation with both NPCs await handleInteractSocially({ speakerId: hero.id, targetId: shyNpc.id, content: 'Hello there! I need some information.', volume: 'TALK', intent: 'greeting' }, mockCtx); await handleInteractSocially({ speakerId: hero.id, targetId: confidentNpc.id, content: 'Hello there! I need some information.', volume: 'TALK', intent: 'greeting' }, mockCtx); // Both should respond, but differently based on personality const shyContext = await handleGetNpcContext({ characterId: hero.id, npcId: shyNpc.id }, mockCtx); const confidentContext = await handleGetNpcContext({ characterId: hero.id, npcId: confidentNpc.id }, mockCtx); expect(JSON.parse(shyContext.content[0].text)).toBeDefined(); expect(JSON.parse(confidentContext.content[0].text)).toBeDefined(); }); test('2.5 - Suspicious NPC interrogates unknown visitors', async () => { const stranger = createTestCharacter({ name: 'Suspicious Stranger', characterType: 'pc' }); const paranoidGuard = createTestCharacter({ name: 'Paranoid City Guard', behavior: 'paranoid', stats: { str: 12, dex: 12, con: 14, int: 12, wis: 16, cha: 10 }, factionId: 'city-guard' }); // First time meeting - should be suspicious await handleUpdateNpcRelationship({ characterId: stranger.id, npcId: paranoidGuard.id, familiarity: 'stranger', disposition: 'unfriendly' }, mockCtx); const relationship = await handleGetNpcRelationship({ characterId: stranger.id, npcId: paranoidGuard.id }, mockCtx); const relationshipData = JSON.parse(relationship.content[0].text); expect(relationshipData.familiarity).toBe('stranger'); expect(relationshipData.disposition).toBe('unfriendly'); }); }); describe('Social Intent Processing', () => { test('2.6 - NPC responds appropriately to different social intents', async () => { const hero = createTestCharacter({ name: 'Diplomatic Hero', characterType: 'pc' }); const elder = createTestCharacter({ name: 'Village Elder', behavior: 'wise', stats: { str: 8, dex: 10, con: 12, int: 16, wis: 18, cha: 14 } }); // Create room and add characters const room = createTestRoom({ entityIds: [hero.id, elder.id] }); spatialRepo.create(room); spatialRepo.addEntityToRoom(room.id, hero.id); spatialRepo.addEntityToRoom(room.id, elder.id); charRepo.update(hero.id, { currentRoomId: room.id }); charRepo.update(elder.id, { currentRoomId: room.id }); // Test different intents const intents = ['greeting', 'question', 'request', 'threaten', 'bargain', 'confide']; for (const intent of intents) { await handleInteractSocially({ speakerId: hero.id, targetId: elder.id, content: `Testing ${intent} intent`, volume: 'TALK', intent: intent }, mockCtx); // Record memory of the interaction await handleRecordConversationMemory({ characterId: hero.id, npcId: elder.id, summary: `Hero approached with ${intent} intent`, importance: intent === 'threaten' ? 'high' : 'medium', topics: [intent] }, mockCtx); } // Test: Elder should remember different intents and respond accordingly const context = await handleGetNpcContext({ characterId: hero.id, npcId: elder.id }, mockCtx); const contextData = JSON.parse(context.content[0].text); expect(contextData.relationship).toBeDefined(); }); }); }); // ============================================================================= // CATEGORY 3: STRATEGIC AI DECISION-MAKING // ============================================================================= describe('Category 3: Strategic AI Decision-Making', () => { describe('Nation-Level Strategic Decisions', () => { test('3.1 - Aggressive nation initiates conflicts', async () => { // Create aggressive nation const aggressiveNation = await handleStrategyTool('create_nation', { worldId: 'test-world', name: 'Warhammer Empire', leader: 'Emperor Krieg', ideology: 'autocracy', aggression: 85, trust: 20, paranoia: 60, startingResources: { food: 100, metal: 200, oil: 50 } }, mockCtx); const aggressiveData = JSON.parse(aggressiveNation.content[0].text); // Create peaceful neighbor const peacefulNation = await handleStrategyTool('create_nation', { worldId: 'test-world', name: 'Republic of Harmony', leader: 'President Peace', ideology: 'democracy', aggression: 15, trust: 80, paranoia: 30, startingResources: { food: 150, metal: 100, oil: 30 } }, mockCtx); const peacefulData = JSON.parse(peacefulNation.content[0].text); // Test: Aggressive AI should consider military expansion expect(aggressiveData.aggression).toBe(85); expect(peacefulData.aggression).toBe(15); }); test('3.2 - Diplomatic AI forms alliances strategically', async () => { const nation1 = await handleStrategyTool('create_nation', { worldId: 'test-world', name: 'Trade Federation', leader: 'Chancellor Commerce', ideology: 'democracy', aggression: 30, trust: 70, paranoia: 40 }, mockCtx); const nation2 = await handleStrategyTool('create_nation', { worldId: 'test-world', name: 'Merchant Alliance', leader: 'High Trader', ideology: 'democracy', aggression: 25, trust: 75, paranoia: 35 }, mockCtx); const tradeData = JSON.parse(nation1.content[0].text); const merchantData = JSON.parse(nation2.content[0].text); // Test diplomatic AI decision-making // Both nations should see mutual benefit in trade alliance const allianceResult = await handleStrategyTool('propose_alliance', { fromNationId: tradeData.id, toNationId: merchantData.id }, mockCtx); expect(allianceResult).toBeDefined(); }); test('3.3 - Resource management AI allocates efficiently', async () => { const nation = await handleStrategyTool('create_nation', { worldId: 'test-world', name: 'Industrial Complex', leader: 'Director Efficiency', ideology: 'autocracy', aggression: 50, trust: 60, paranoia: 45, startingResources: { food: 80, metal: 300, oil: 100 } }, mockCtx); const nationData = JSON.parse(nation.content[0].text); // Test: AI should recognize metal surplus, food shortage expect(nationData.resources.metal).toBeGreaterThan(nationData.resources.food); // Strategic AI should consider resource redistribution or trade // This would involve actual AI decision logic }); }); describe('Diplomatic Response Patterns', () => { test('3.4 - Trust-based AI responds to diplomatic overtures', async () => { const trustworthy = await handleStrategyTool('create_nation', { worldId: 'test-world', name: 'Honorable Kingdom', leader: 'King Truth', ideology: 'autocracy', aggression: 20, trust: 90, paranoia: 20 }, mockCtx); const untrustworthy = await handleStrategyTool('create_nation', { worldId: 'test-world', name: 'Deceitful Duchy', leader: 'Duke Deception', ideology: 'autocracy', aggression: 60, trust: 10, paranoia: 80 }, mockCtx); const honorableData = JSON.parse(trustworthy.content[0].text); const deceitfulData = JSON.parse(untrustworthy.content[0].text); // Honorable kingdom should respond positively to diplomacy expect(honorableData.trust).toBe(90); expect(deceitfulData.trust).toBe(10); }); test('3.5 - Paranoid AI sees threats everywhere', async () => { const paranoidNation = await handleStrategyTool('create_nation', { worldId: 'test-world', name: 'Fortress State', leader: 'General Paranoia', ideology: 'autocracy', aggression: 40, trust: 15, paranoia: 95 }, mockCtx); const paranoidData = JSON.parse(paranoidNation.content[0].text); // Test: Paranoid AI should interpret neutral actions as threats expect(paranoidData.paranoia).toBe(95); expect(paranoidData.trust).toBe(15); }); }); }); // ============================================================================= // CATEGORY 4: ADAPTIVE AI BEHAVIOR // ============================================================================= describe('Category 4: Adaptive AI Behavior', () => { describe('Response to Player Actions', () => { test('4.1 - NPC adapts to player reputation changes', async () => { const hero = createTestCharacter({ name: 'Reformed Rogue', characterType: 'pc', behavior: 'redeemed', stats: { str: 12, dex: 16, con: 12, int: 12, wis: 12, cha: 14 } }); const shopkeeper = createTestCharacter({ name: 'Local Shopkeeper', behavior: 'pragmatic', stats: { str: 8, dex: 10, con: 10, int: 14, wis: 12, cha: 12 } }); // Initial neutral relationship await handleUpdateNpcRelationship({ characterId: hero.id, npcId: shopkeeper.id, familiarity: 'stranger', disposition: 'neutral', notes: 'New customer, unknown reputation' }, mockCtx); // Hero performs good deed await handleRecordConversationMemory({ characterId: hero.id, npcId: shopkeeper.id, summary: 'Hero saved shopkeeper from pickpockets', importance: 'high', topics: ['good deed', 'heroism', 'trust'] }, mockCtx); // Test: Shopkeeper should adapt disposition based on hero's actions await handleUpdateNpcRelationship({ characterId: hero.id, npcId: shopkeeper.id, familiarity: 'acquaintance', disposition: 'friendly', notes: 'Proven trustworthy by helping me' }, mockCtx); const updatedRelationship = await handleGetNpcRelationship({ characterId: hero.id, npcId: shopkeeper.id }, mockCtx); const relationshipData = JSON.parse(updatedRelationship.content[0].text); expect(relationshipData.disposition).toBe('friendly'); }); test('4.2 - AI changes strategy based on combat outcomes', async () => { const adaptiveFighter = createTestCharacter({ name: 'Adaptive Warrior', behavior: 'adaptive', stats: { str: 14, dex: 12, con: 14, int: 12, wis: 12, cha: 10 }, characterClass: 'Fighter', hp: 25, ac: 14 }); const enemy1 = createTestCharacter({ name: 'Spearman', hp: 20, ac: 12 }); // First encounter - loses due to enemy reach const encounter1 = await handleCreateEncounter({ seed: 'adaptive-lesson-1', participants: [ { id: adaptiveFighter.id, name: adaptiveFighter.name, initiativeBonus: 1, hp: adaptiveFighter.hp, maxHp: adaptiveFighter.maxHp, conditions: [] }, { id: enemy1.id, name: enemy1.name, initiativeBonus: 2, hp: enemy1.hp, maxHp: enemy1.maxHp, conditions: [] } ] }, mockCtx); const encounterId1 = extractJsonFromResponse(encounter1.content[0].text).encounterId; // Simulate combat where fighter loses await handleExecuteCombatAction({ encounterId: encounterId1, action: 'attack', actorId: enemy1.id, targetId: adaptiveFighter.id, attackBonus: 4, dc: 12, damage: 8 }, mockCtx); // Test: Adaptive AI should learn from defeat // Next encounter should show different behavior (e.g., use shield, keep distance) const encounter2 = await handleCreateEncounter({ seed: 'adaptive-lesson-2', participants: [ { id: adaptiveFighter.id, name: adaptiveFighter.name, initiativeBonus: 1, hp: 25, maxHp: 25, conditions: [] }, { id: enemy1.id, name: enemy1.name, initiativeBonus: 2, hp: 20, maxHp: 20, conditions: [] } ] }, mockCtx); const state2 = await handleGetEncounterState({ encounterId: extractJsonFromResponse(encounter2.content[0].text).encounterId }, mockCtx); expect(state2.participants).toBeDefined(); }); }); describe('Environmental Adaptation', () => { test('4.3 - AI adapts behavior in different environments', async () => { const cityGuard = createTestCharacter({ name: 'City Watch Captain', behavior: 'authoritative', stats: { str: 12, dex: 12, con: 14, int: 12, wis: 14, cha: 12 }, characterClass: 'Fighter', factionId: 'city-guard' }); const drunk = createTestCharacter({ name: 'Disorderly Drunk', behavior: 'reckless', stats: { str: 10, dex: 8, con: 12, int: 6, wis: 8, cha: 10 } }); // Create room representing tavern const tavernRoom = createTestRoom({ name: 'The Drunken Dragon Tavern', baseDescription: 'A crowded tavern with low lighting and wooden tables', biomeContext: 'urban', atmospherics: ['BRIGHT'], entityIds: [cityGuard.id, drunk.id] }); spatialRepo.create(tavernRoom); // Test: Authoritative AI should be more lenient in tavern vs street const roomContext = spatialRepo.findById(tavernRoom.id); expect(roomContext).toBeDefined(); expect(roomContext?.biomeContext).toBe('urban'); }); test('4.4 - Religious AI responds to sacred spaces', async () => { const zealot = createTestCharacter({ name: 'Temple Guardian', behavior: 'zealous', stats: { str: 12, dex: 10, con: 14, int: 10, wis: 16, cha: 12 }, characterClass: 'Cleric', factionId: 'temple' }); const troublemaker = createTestCharacter({ name: 'Blasphemer', behavior: 'defiant', stats: { str: 10, dex: 12, con: 10, int: 10, wis: 6, cha: 12 } }); // Create sacred space const templeRoom = createTestRoom({ name: 'Sacred Altar of Light', baseDescription: 'A consecrated space filled with divine radiance', biomeContext: 'divine', atmospherics: ['BRIGHT', 'MAGICAL'], entityIds: [zealot.id, troublemaker.id] }); spatialRepo.create(templeRoom); const temple = spatialRepo.findById(templeRoom.id); expect(temple?.atmospherics).toContain('BRIGHT'); expect(temple?.atmospherics).toContain('MAGICAL'); }); }); }); // ============================================================================= // CATEGORY 5: AI CONSISTENCY // ============================================================================= describe('Category 5: AI Consistency', () => { describe('Personality Trait Consistency', () => { test('5.1 - Greedy NPC remains greedy across scenarios', async () => { const merchant = createTestCharacter({ name: 'Greedy Merchant', behavior: 'greedy', stats: { str: 8, dex: 12, con: 10, int: 14, wis: 10, cha: 16 } }); // Test scenario 1: Simple transaction await handleUpdateNpcRelationship({ characterId: 'pc-1', npcId: merchant.id, familiarity: 'stranger', disposition: 'neutral', notes: 'First customer of the day' }, mockCtx); // Test scenario 2: Bulk purchase offer await handleRecordConversationMemory({ characterId: 'pc-1', npcId: merchant.id, summary: 'Customer wants to buy entire inventory', importance: 'high', topics: ['profit', 'business'] }, mockCtx); // Test scenario 3: Charity request await handleRecordConversationMemory({ characterId: 'pc-1', npcId: merchant.id, summary: 'Orphanage asks for donation', importance: 'medium', topics: ['charity', 'refusal'] }, mockCtx); // Test: Greedy merchant should consistently prioritize profit const context = await handleGetNpcContext({ characterId: 'pc-1', npcId: merchant.id }, mockCtx); const contextData = JSON.parse(context.content[0].text); expect(contextData.relationship).toBeDefined(); }); test('5.2 - Honorable NPC maintains principles under pressure', async () => { const knight = createTestCharacter({ name: 'Sir Honorbound', behavior: 'honorable', stats: { str: 14, dex: 12, con: 14, int: 12, wis: 14, cha: 14 }, characterClass: 'Paladin' }); // Test various moral dilemmas const dilemmas = [ { scenario: 'Pay bribe to save innocent', summary: 'Bandit offers deal: pay gold to let innocent go', behavior: 'honorable' }, { scenario: 'Break law to save life', summary: 'Breaking curfew could save a life', behavior: 'lawful' }, { scenario: 'Lie to protect friend', summary: 'Friend asks for alibi for crime they didn\'t commit', behavior: 'honest' } ]; for (const dilemma of dilemmas) { await handleRecordConversationMemory({ characterId: 'pc-1', npcId: knight.id, summary: `${knight.name} faced dilemma: ${dilemma.summary}`, importance: 'high', topics: [dilemma.behavior, 'moral'] }, mockCtx); } // Test: Knight should maintain consistent honorable behavior const context = await handleGetNpcContext({ characterId: 'pc-1', npcId: knight.id }, mockCtx); const contextData = JSON.parse(context.content[0].text); expect(contextData.relationship).toBeDefined(); }); }); describe('Behavior Over Time', () => { test('5.3 - NPC development shows gradual change', async () => { const youngWarrior = createTestCharacter({ name: 'Aspiring Knight', behavior: 'ambitious', stats: { str: 12, dex: 12, con: 12, int: 10, wis: 10, cha: 10 }, level: 1 }); // Simulate character progression const experiences = [ { level: 1, summary: 'First battle, barely survived', importance: 'medium' }, { level: 2, summary: 'Saved innocent from bandits', importance: 'high' }, { level: 3, summary: 'Led village defense against raiders', importance: 'high' }, { level: 4, summary: 'Made difficult moral choice', importance: 'critical' } ]; for (const exp of experiences) { // Update character level (simulated growth) charRepo.update(youngWarrior.id, { level: exp.level, stats: { str: 12 + Math.floor(exp.level / 2), dex: 12 + Math.floor(exp.level / 4), con: 12 + Math.floor(exp.level / 3), int: 10 + Math.floor(exp.level / 5), wis: 10 + Math.floor(exp.level / 4), cha: 10 + Math.floor(exp.level / 3) } }); await handleRecordConversationMemory({ characterId: 'pc-mentor', npcId: youngWarrior.id, summary: `${youngWarrior.name} ${exp.summary}`, importance: exp.importance, topics: ['growth', 'experience'] }, mockCtx); } // Test: Character should show development over time const updatedChar = charRepo.findById(youngWarrior.id); expect(updatedChar?.level).toBe(4); expect(updatedChar?.stats.str).toBeGreaterThan(12); }); }); }); // ============================================================================= // CATEGORY 6: MULTI-CHARACTER COORDINATION // ============================================================================= describe('Category 6: Multi-Character Coordination', () => { describe('Group Dynamics', () => { test('6.1 - Party members coordinate in combat', async () => { const partyLeader = createTestCharacter({ name: 'Captain Blade', behavior: 'leadership', stats: { str: 14, dex: 12, con: 14, int: 12, wis: 12, cha: 16 }, characterClass: 'Fighter', hp: 30, ac: 16 }); const healer = createTestCharacter({ name: 'Healing Light', behavior: 'supportive', stats: { str: 8, dex: 10, con: 12, int: 12, wis: 16, cha: 14 }, characterClass: 'Cleric', hp: 18, ac: 12 }); const attacker = createTestCharacter({ name: 'Shadow Stalker', behavior: 'aggressive', stats: { str: 12, dex: 18, con: 10, int: 12, wis: 10, cha: 8 }, characterClass: 'Rogue', hp: 20, ac: 14 }); // Create party const party = partyRepo.create({ id: uuid(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), name: 'The Companions', description: 'Adventuring party', worldId: 'test-world', currentLocation: 'Tavern', formation: 'defensive', status: 'active' }); // Add members explicitly partyRepo.addMember({ id: uuid(), partyId: party.id, characterId: partyLeader.id, role: 'leader', isActive: true, sharePercentage: 1, joinedAt: new Date().toISOString(), position: 1, notes: 'Party leader' }); partyRepo.addMember({ id: uuid(), partyId: party.id, characterId: healer.id, role: 'member', isActive: true, sharePercentage: 1, joinedAt: new Date().toISOString(), position: 2, notes: 'Healer support' }); partyRepo.addMember({ id: uuid(), partyId: party.id, characterId: attacker.id, role: 'member', isActive: true, sharePercentage: 1, joinedAt: new Date().toISOString(), position: 3, notes: 'Flanking specialist' }); // Test: Party should coordinate in combat scenarios const partyData = await Promise.all([ handleGetNpcContext({ characterId: partyLeader.id, npcId: healer.id }, mockCtx), handleGetNpcContext({ characterId: partyLeader.id, npcId: attacker.id }, mockCtx) ]); expect(party).toBeDefined(); expect(partyData[0]).toBeDefined(); expect(partyData[1]).toBeDefined(); }); test('6.2 - Conflicting personalities create tension', async () => { const lawfulPaladin = createTestCharacter({ name: 'Sir Lawful', behavior: 'rigid', stats: { str: 14, dex: 10, con: 14, int: 12, wis: 14, cha: 14 }, characterClass: 'Paladin' }); const chaoticRogue = createTestCharacter({ name: 'Kitty', behavior: 'chaotic', stats: { str: 10, dex: 18, con: 10, int: 12, wis: 10, cha: 12 }, characterClass: 'Rogue' }); // Test: Conflicting personalities should create interesting dynamics await handleUpdateNpcRelationship({ characterId: 'pc-dm', npcId: lawfulPaladin.id, familiarity: 'acquaintance', disposition: 'neutral', notes: 'Fellow adventurer' }, mockCtx); await handleUpdateNpcRelationship({ characterId: 'pc-dm', npcId: chaoticRogue.id, familiarity: 'acquaintance', disposition: 'neutral', notes: 'Fellow adventurer' }, mockCtx); // Record conflicting opinions await handleRecordConversationMemory({ characterId: 'pc-dm', npcId: lawfulPaladin.id, summary: 'Paladin condemns Rogue\'s unorthodox methods', importance: 'medium', topics: ['conflict', 'methods'] }, mockCtx); await handleRecordConversationMemory({ characterId: 'pc-dm', npcId: chaoticRogue.id, summary: 'Rogue mocks Paladin\'s rigid adherence to rules', importance: 'medium', topics: ['conflict', 'rules'] }, mockCtx); const paladinContext = await handleGetNpcContext({ characterId: 'pc-dm', npcId: lawfulPaladin.id }, mockCtx); const rogueContext = await handleGetNpcContext({ characterId: 'pc-dm', npcId: chaoticRogue.id }, mockCtx); expect(JSON.parse(paladinContext.content[0].text)).toBeDefined(); expect(JSON.parse(rogueContext.content[0].text)).toBeDefined(); }); }); describe('Competing Priorities', () => { test('6.3 - NPCs with conflicting goals make different choices', async () => { const merchant = createTestCharacter({ name: 'Trade Master', behavior: 'profit-focused', stats: { str: 8, dex: 12, con: 10, int: 16, wis: 12, cha: 14 }, factionId: 'merchant-guild' }); const guard = createTestCharacter({ name: 'City Guardian', behavior: 'duty-bound', stats: { str: 12, dex: 12, con: 14, int: 12, wis: 14, cha: 10 }, factionId: 'city-guard' }); // Scenario: Stolen goods recovery await handleRecordConversationMemory({ characterId: 'pc-detective', npcId: merchant.id, summary: 'Merchant\'s goods were stolen, wants quick resolution', importance: 'high', topics: ['profit', 'theft'] }, mockCtx); await handleRecordConversationMemory({ characterId: 'pc-detective', npcId: guard.id, summary: 'Guard wants proper investigation and legal process', importance: 'high', topics: ['justice', 'procedure'] }, mockCtx); // Test: Different priorities should lead to different suggested solutions const merchantContext = await handleGetNpcContext({ characterId: 'pc-detective', npcId: merchant.id }, mockCtx); const guardContext = await handleGetNpcContext({ characterId: 'pc-detective', npcId: guard.id }, mockCtx); const merchantData = JSON.parse(merchantContext.content[0].text); const guardData = JSON.parse(guardContext.content[0].text); expect(merchantData.relationship).toBeDefined(); expect(guardData.relationship).toBeDefined(); }); }); }); // ============================================================================= // CATEGORY 7: ENVIRONMENTAL AWARENESS // ============================================================================= describe('Category 7: Environmental Awareness', () => { describe('World-State Understanding', () => { test('7.1 - Local NPC understands regional politics', async () => { const borderGuard = createTestCharacter({ name: 'Border Sentinel', behavior: 'vigilant', stats: { str: 12, dex: 12, con: 14, int: 12, wis: 14, cha: 10 }, factionId: 'frontier-guard' }); const citizen = createTestCharacter({ name: 'Local Farmer', behavior: 'pragmatic', stats: { str: 10, dex: 10, con: 12, int: 10, wis: 12, cha: 8 } }); // Test: Border guard should know about territorial disputes await handleRecordConversationMemory({ characterId: 'pc-stranger', npcId: borderGuard.id, summary: 'Guard warns about hostile nation to the east', importance: 'high', topics: ['politics', 'threat'] }, mockCtx); await handleRecordConversationMemory({ characterId: 'pc-stranger', npcId: citizen.id, summary: 'Farmer mentions increased taxes due to border tensions', importance: 'medium', topics: ['politics', 'economy'] }, mockCtx); const guardContext = await handleGetNpcContext({ characterId: 'pc-stranger', npcId: borderGuard.id }, mockCtx); const citizenContext = await handleGetNpcContext({ characterId: 'pc-stranger', npcId: citizen.id }, mockCtx); expect(JSON.parse(guardContext.content[0].text)).toBeDefined(); expect(JSON.parse(citizenContext.content[0].text)).toBeDefined(); }); test('7.2 - NPCs react to environmental changes', async () => { const weatherWatcher = createTestCharacter({ name: 'Storm Watcher', behavior: 'observant', stats: { str: 8, dex: 10, con: 10, int: 14, wis: 16, cha: 12 } }); // Simulate environmental change (storm approaching) await handleRecordConversationMemory({ characterId: 'pc-traveler', npcId: weatherWatcher.id, summary: 'Weather patterns changing, storm approaching from the north', importance: 'high', topics: ['weather', 'storm'] }, mockCtx); // Test: NPC should remember environmental threats const context = await handleGetNpcContext({ characterId: 'pc-traveler', npcId: weatherWatcher.id }, mockCtx); const contextData = JSON.parse(context.content[0].text); expect(contextData.relationship).toBeDefined(); expect(contextData.recentMemories).toBeDefined(); }); }); describe('Situational Awareness', () => { test('7.3 - NPCs assess threats appropriately', async () => { const cityWatch = createTestCharacter({ name: 'Street Patrol', behavior: 'alert', stats: { str: 12, dex: 12, con: 14, int: 10, wis: 14, cha: 10 }, characterClass: 'Fighter', factionId: 'city-guard' }); const suspiciousCharacter = createTestCharacter({ name: 'Suspicious Person', behavior: 'skulking', stats: { str: 10, dex: 14, con: 10, int: 12, wis: 10, cha: 8 } }); // Test: Watch should be aware of suspicious activity await handleRecordConversationMemory({ characterId: 'pc-witness', npcId: cityWatch.id, summary: 'Watch notices suspicious character lurking near bank', importance: 'high', topics: ['threat', 'suspicious'] }, mockCtx); const context = await handleGetNpcContext({ characterId: 'pc-witness', npcId: cityWatch.id }, mockCtx); expect(JSON.parse(context.content[0].text)).toBeDefined(); }); test('7.4 - NPCs coordinate during emergencies', async () => { const captain = createTestCharacter({ name: 'Fire Captain', behavior: 'commanding', stats: { str: 12, dex: 12, con: 14, int: 14, wis: 14, cha: 16 }, characterClass: 'Fighter', factionId: 'fire-dept' }); const firefighter = createTestCharacter({ name: 'Rescue Worker', behavior: 'brave', stats: { str: 14, dex: 12, con: 14, int: 10, wis: 12, cha: 10 }, factionId: 'fire-dept' }); // Emergency scenario await handleRecordConversationMemory({ characterId: 'pc-civilian', npcId: captain.id, summary: 'Captain coordinates fire rescue effort', importance: 'critical', topics: ['emergency', 'coordination'] }, mockCtx); await handleRecordConversationMemory({ characterId: 'pc-civilian', npcId: firefighter.id, summary: 'Firefighter follows captain\'s orders during rescue', importance: 'critical', topics: ['emergency', 'obedience'] }, mockCtx); // Test: NPCs should show coordinated emergency response const captainContext = await handleGetNpcContext({ characterId: 'pc-civilian', npcId: captain.id }, mockCtx); const firefighterContext = await handleGetNpcContext({ characterId: 'pc-civilian', npcId: firefighter.id }, mockCtx); expect(JSON.parse(captainContext.content[0].text)).toBeDefined(); expect(JSON.parse(firefighterContext.content[0].text)).toBeDefined(); }); }); }); // ============================================================================= // CATEGORY 8: EDGE CASES AND COMPLEX SCENARIOS // ============================================================================= describe('Category 8: Edge Cases and Complex Scenarios', () => { describe('Contradictory Personality Traits', () => { test('8.1 - NPC with conflicting traits makes complex decisions', async () => { const paradoxical = createTestCharacter({ name: 'The Paradox', behavior: 'complex', stats: { str: 12, dex: 12, con: 12, int: 16, wis: 12, cha: 14 } // Complex traits: generous yet selfish, brave yet cowardly }); // Test various scenarios that trigger conflicting traits const scenarios = [ { summary: 'Opportunity to help stranger costs money', traits: ['generous vs selfish'] }, { summary: 'Dangerous situation requires intervention', traits: ['brave vs cowardly'] }, { summary: 'Moral dilemma with no clear right answer', traits: ['complex reasoning'] } ]; for (const scenario of scenarios) { await handleRecordConversationMemory({ characterId: 'pc-observer', npcId: paradoxical.id, summary: `${paradoxical.name} faced: ${scenario.summary}`, importance: 'high', topics: scenario.traits }, mockCtx); } // Test: Complex AI should show nuanced decision-making const context = await handleGetNpcContext({ characterId: 'pc-observer', npcId: paradoxical.id }, mockCtx); const contextData = JSON.parse(context.content[0].text); expect(contextData.relationship).toBeDefined(); expect(contextData.recentMemories.length).toBeGreaterThan(0); }); test('8.2 - NPC adapts when traits evolve over time', async () => { const evolvingCharacter = createTestCharacter({ name: 'The Redeemed', behavior: 'redeemable', stats: { str: 12, dex: 12, con: 12, int: 12, wis: 12, cha: 12 } // Starting with potential for growth }); // Simulate character arc const redemptionSteps = [ 'Shows mercy to defeated enemy', 'Helps stranger in need', 'Refuses payment for good deed', 'Sacrifices self for others' ]; for (const step of redemptionSteps) { await handleRecordConversationMemory({ characterId: 'pc-witness', npcId: evolvingCharacter.id, summary: `${evolvingCharacter.name} ${step}`, importance: 'high', topics: ['redemption', 'growth'] }, mockCtx); } // Character should show alignment change over time const finalContext = await handleGetNpcContext({ characterId: 'pc-witness', npcId: evolvingCharacter.id }, mockCtx); const contextData = JSON.parse(finalContext.content[0].text); expect(contextData.recentMemories.length).toBe(4); }); }); describe('Multi-Step Decision Chains', () => { test('8.3 - NPCs plan multi-step strategies', async () => { const strategist = createTestCharacter({ name: 'Master Planner', behavior: 'strategic', stats: { str: 10, dex: 12, con: 12, int: 18, wis: 16, cha: 12 } }); // Complex scenario requiring multi-step planning const plan = [ 'Gather intelligence about target', 'Build alliances with key figures', 'Acquire necessary resources', 'Execute primary objective', 'Manage consequences' ]; for (const step of plan) { await handleRecordConversationMemory({ characterId: 'pc-assistant', npcId: strategist.id, summary: `Planning phase: ${step}`, importance: 'medium', topics: ['planning', 'strategy'] }, mockCtx); } // Test: Strategic NPC should show forward-thinking behavior const context = await handleGetNpcContext({ characterId: 'pc-assistant', npcId: strategist.id }, mockCtx); const contextData = JSON.parse(context.content[0].text); expect(contextData.recentMemories.length).toBe(5); }); test('8.4 - NPCs handle unexpected complications', async () => { const adaptableLeader = createTestCharacter({ name: 'Flexible Commander', behavior: 'adaptable', stats: { str: 12, dex: 12, con: 14, int: 14, wis: 14, cha: 14 } }); // Plan with complications const complications = [ 'Plan initial assault', 'Enemy reinforcements arrive (unexpected)', 'Weather turns bad (environmental change)', 'Key ally betrays (social complication)', 'Adapt strategy on the fly' ]; for (const complication of complications) { await handleRecordConversationMemory({ characterId: 'pc-soldier', npcId: adaptableLeader.id, summary: `Situation: ${complication}`, importance: 'high', topics: ['adaptation', 'crisis'] }, mockCtx); } // Test: Adaptable AI should handle complications gracefully const context = await handleGetNpcContext({ characterId: 'pc-soldier', npcId: adaptableLeader.id }, mockCtx); const contextData = JSON.parse(context.content[0].text); expect(contextData.relationship).toBeDefined(); }); }); describe('Moral and Ethical Dilemmas', () => { test('8.5 - NPCs resolve complex ethical dilemmas', async () => { const moralAdvisor = createTestCharacter({ name: 'Wise Sage', behavior: 'wise', stats: { str: 8, dex: 10, con: 12, int: 18, wis: 18, cha: 14 } }); const dilemmas = [ { scenario: 'Lie to save lives', summary: 'Must lie to prevent massacre', ethics: 'utilitarian vs deontological' }, { scenario: 'Sacrifice one to save many', summary: 'Trolley problem variant', ethics: 'utilitarian calculus' }, { scenario: 'Honor vs compassion', summary: 'Oath requires harmful action', ethics: 'competing virtues' } ]; for (const dilemma of dilemmas) { await handleRecordConversationMemory({ characterId: 'pc-seeker', npcId: moralAdvisor.id, summary: `Dilemma: ${dilemma.summary}`, importance: 'critical', topics: [dilemma.ethics, 'moral'] }, mockCtx); } // Test: Wise NPC should provide nuanced ethical reasoning const context = await handleGetNpcContext({ characterId: 'pc-seeker', npcId: moralAdvisor.id }, mockCtx); const contextData = JSON.parse(context.content[0].text); expect(contextData.recentMemories.length).toBe(3); }); }); describe('Cultural and Social Context', () => { test('8.6 - NPCs react based on cultural backgrounds', async () => { const noble = createTestCharacter({ name: 'High Noble', behavior: 'honor-bound', stats: { str: 10, dex: 12, con: 12, int: 14, wis: 12, cha: 16 }, factionId: 'nobility' }); const commoner = createTestCharacter({ name: 'Street Urchin', behavior: 'survival-focused', stats: { str: 10, dex: 14, con: 10, int: 10, wis: 12, cha: 8 }, factionId: 'street' }); // Same situation, different cultural responses const situation = 'Public confrontation over minor insult'; await handleRecordConversationMemory({ characterId: 'pc-witness', npcId: noble.id, summary: `Noble responds to ${situation} with formal challenge`, importance: 'medium', topics: ['honor', 'culture'] }, mockCtx); await handleRecordConversationMemory({ characterId: 'pc-witness', npcId: commoner.id, summary: `Urchin responds to ${situation} with street smarts`, importance: 'medium', topics: ['survival', 'culture'] }, mockCtx); // Test: Different cultural backgrounds should produce different responses const nobleContext = await handleGetNpcContext({ characterId: 'pc-witness', npcId: noble.id }, mockCtx); const commonerContext = await handleGetNpcContext({ characterId: 'pc-witness', npcId: commoner.id }, mockCtx); expect(JSON.parse(nobleContext.content[0].text)).toBeDefined(); expect(JSON.parse(commonerContext.content[0].text)).toBeDefined(); }); }); }); // ============================================================================= // TEST UTILITIES AND HELPERS // ============================================================================= describe('Test Utilities and Helpers', () => { test('9.1 - Helper functions work correctly', () => { expect(createTestCharacter).toBeDefined(); expect(typeof createTestCharacter).toBe('function'); const testChar = createTestCharacter({ name: 'Helper Test' }); expect(testChar.name).toBe('Helper Test'); }); test('9.2 - Mock context provides required session data', () => { expect(mockCtx.sessionId).toBe('test-session'); expect(typeof mockCtx.sessionId).toBe('string'); }); test('9.3 - Database setup and cleanup works', () => { expect(db).toBeDefined(); expect(charRepo).toBeDefined(); expect(memoryRepo).toBeDefined(); // Verify we can create and query data const testChar = createTestCharacter({ name: 'Cleanup Test' }); const found = charRepo.findById(testChar.id); expect(found).toBeDefined(); if (found) { expect(found.name).toBe('Cleanup Test'); } }); }); // ============================================================================= // PERFORMANCE AND SCALABILITY // ============================================================================= describe('Performance and Scalability', () => { test('10.1 - AI decision tests handle multiple NPCs efficiently', async () => { // Create many NPCs to test scalability const npcs = []; for (let i = 0; i < 50; i++) { npcs.push(createTestCharacter({ name: `NPC ${i}`, behavior: ['friendly', 'aggressive', 'shy', 'wise'][i % 4] })); } // Create relationships with many NPCs for (const npc of npcs) { await handleUpdateNpcRelationship({ characterId: 'pc-test', npcId: npc.id, familiarity: 'acquaintance', disposition: 'neutral' }, mockCtx); } // Test: System should handle multiple NPCs without performance issues const contexts = await Promise.all( npcs.slice(0, 10).map(npc => handleGetNpcContext({ characterId: 'pc-test', npcId: npc.id }, mockCtx) ) ); expect(contexts).toHaveLength(10); contexts.forEach(context => { expect(JSON.parse(context.content[0].text)).toBeDefined(); }); }); test('10.2 - Memory systems scale with conversation history', async () => { const chattyNpc = createTestCharacter({ name: 'Chatty Companion', behavior: 'friendly' }); // Record many conversations for (let i = 0; i < 100; i++) { await handleRecordConversationMemory({ characterId: 'pc-regular', npcId: chattyNpc.id, summary: `Conversation ${i}: ${['weather', 'politics', 'adventure', 'family'][i % 4]}`, importance: ['low', 'medium', 'high'][i % 3] as any, topics: [`topic${i}`] }, mockCtx); } // Test: Should handle large conversation history efficiently const context = await handleGetNpcContext({ characterId: 'pc-regular', npcId: chattyNpc.id, memoryLimit: 5 }, mockCtx); const contextData = JSON.parse(context.content[0].text); expect(contextData.relationship).toBeDefined(); expect(contextData.recentMemories).toHaveLength(5); // Limited by memoryLimit }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Mnehmos/rpg-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server