/** * Integration tests for complete battle scenarios * Tests complex multi-turn battles following the design document */ import { describe, it, expect } from 'vitest'; import { BattleEngine } from './BattleEngine'; import { STELLAR_WOLF, TOXIC_CRAWLER, BERSERKER_BEAST, AQUA_GUARDIAN } from './test-data'; import { BattleAction } from './types'; describe('Battle Engine Integration', () => { describe('Complete Battle Scenarios', () => { it('should handle a complete battle with type effectiveness', () => { // Space vs Bug - Space has advantage const engine = new BattleEngine(STELLAR_WOLF, TOXIC_CRAWLER); let turns = 0; const maxTurns = 20; while (!engine.isGameOver() && turns < maxTurns) { const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 1 // Flame Burst (Space type) }; const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 // Tackle }; engine.executeActions(playerAction, opponentAction); turns++; } expect(engine.isGameOver()).toBe(true); expect(turns).toBeLessThan(maxTurns); // Player should win due to type advantage expect(engine.getWinner()).toBe('player'); const log = engine.getLog(); expect(log.some(msg => msg.includes("It's super effective!"))).toBe(true); }); it('should handle a battle with status effects and healing', () => { const engine = new BattleEngine(TOXIC_CRAWLER, AQUA_GUARDIAN); // Turn 1: Toxic Crawler uses Toxic Sting to poison engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 1 }, // Toxic Sting { type: 'move', piclet: 'opponent', moveIndex: 0 } // Tackle ); // Check that opponent is poisoned expect(engine.getState().opponentPiclet.statusEffects).toContain('poison'); // Turn 2: Guardian tries to heal while poison damage occurs const hpBeforeTurn = engine.getState().opponentPiclet.currentHp; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // Tackle { type: 'move', piclet: 'opponent', moveIndex: 1 } // Healing Light ); // Poison should have done damage during turn end const log = engine.getLog(); expect(log.some(msg => msg.includes('hurt by poison'))).toBe(true); }); it('should handle conditional move effects correctly', () => { const engine = new BattleEngine(BERSERKER_BEAST, AQUA_GUARDIAN); // Damage the berserker to trigger low HP condition engine['state'].playerPiclet.currentHp = Math.floor(engine['state'].playerPiclet.maxHp * 0.2); const initialDefense = engine.getState().playerPiclet.defense; const initialOpponentHp = engine.getState().opponentPiclet.currentHp; const initialHpRatio = engine.getState().playerPiclet.currentHp / engine.getState().playerPiclet.maxHp; // Use Berserker's End while at low HP engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 1 }, // Berserker's End { type: 'move', piclet: 'opponent', moveIndex: 0 } // Tackle ); const finalDefense = engine.getState().playerPiclet.defense; const finalOpponentHp = engine.getState().opponentPiclet.currentHp; // Should deal damage (may miss due to 90% accuracy, so check if hit) const damageDealt = initialOpponentHp - finalOpponentHp; const log = engine.getLog(); const moveHit = !log.some(msg => msg.includes('attack missed')); if (moveHit) { expect(damageDealt).toBeGreaterThan(20); // Should be significant due to strong damage condition } else { expect(damageDealt).toBe(0); // No damage if missed } // The defense should decrease if HP is below 25% (0.25) due to ifLowHp condition if (initialHpRatio < 0.25) { expect(finalDefense).toBeLessThan(initialDefense); } else { // If not low HP, no defense change expected expect(finalDefense).toBe(initialDefense); } }); it('should handle stat modifications and their effects on damage', () => { const engine = new BattleEngine(STELLAR_WOLF, TOXIC_CRAWLER); // Turn 1: Power Up to increase attack engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 3 }, // Power Up { type: 'move', piclet: 'opponent', moveIndex: 0 } // Tackle ); const boostedAttack = engine.getState().playerPiclet.attack; const opponentHpAfterBoost = engine.getState().opponentPiclet.currentHp; // Turn 2: Attack with boosted stats engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // Tackle { type: 'move', piclet: 'opponent', moveIndex: 0 } // Tackle ); const finalOpponentHp = engine.getState().opponentPiclet.currentHp; const damageWithBoost = opponentHpAfterBoost - finalOpponentHp; // Damage should be higher due to attack boost expect(damageWithBoost).toBeGreaterThan(20); expect(boostedAttack).toBeGreaterThan(STELLAR_WOLF.baseStats.attack); }); it('should maintain battle log integrity throughout complex battle', () => { const engine = new BattleEngine(STELLAR_WOLF, BERSERKER_BEAST); // Execute several turns with different moves const moves = [ [3, 0], // Power Up vs Tackle [1, 1], // Flame Burst vs Berserker's End [2, 2], // Healing Light vs Healing Light [0, 0] // Tackle vs Tackle ]; for (const [playerMove, opponentMove] of moves) { if (engine.isGameOver()) break; engine.executeActions( { type: 'move', piclet: 'player', moveIndex: playerMove }, { type: 'move', piclet: 'opponent', moveIndex: opponentMove } ); } const log = engine.getLog(); expect(log.length).toBeGreaterThan(8); // Should contain move usage expect(log.some(msg => msg.includes('used Power Up'))).toBe(true); expect(log.some(msg => msg.includes('used Flame Burst'))).toBe(true); // Should contain stat changes expect(log.some(msg => msg.includes('attack rose'))).toBe(true); // Should contain healing (check for either recovered HP or no actual healing if at full HP) const hasHealing = log.some(msg => msg.includes('recovered') && msg.includes('HP')); const hasHealingAttempt = log.some(msg => msg.includes('used Healing Light')); expect(hasHealing || hasHealingAttempt).toBe(true); }); it('should handle edge case: all moves run out of PP', () => { const engine = new BattleEngine(STELLAR_WOLF, TOXIC_CRAWLER); // Drain all PP from one move engine['state'].playerPiclet.moves[0].currentPP = 0; engine['state'].playerPiclet.moves[1].currentPP = 0; engine['state'].playerPiclet.moves[2].currentPP = 0; engine['state'].playerPiclet.moves[3].currentPP = 0; // Try to use any move engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, { type: 'move', piclet: 'opponent', moveIndex: 0 } ); const log = engine.getLog(); expect(log.some(msg => msg.includes('no PP left'))).toBe(true); // Battle should continue (opponent can still act) expect(engine.isGameOver()).toBe(false); }); }); describe('Performance and Stability', () => { it('should handle very long battles without issues', () => { const engine = new BattleEngine(AQUA_GUARDIAN, AQUA_GUARDIAN); let turns = 0; const maxTurns = 100; while (!engine.isGameOver() && turns < maxTurns) { // Both use healing moves to prolong battle engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 1 }, // Healing Light { type: 'move', piclet: 'opponent', moveIndex: 1 } // Healing Light ); turns++; // Occasionally attack to prevent infinite loop if (turns % 5 === 0) { engine.executeActions( { type: 'move', piclet: 'player', moveIndex: 0 }, // Tackle { type: 'move', piclet: 'opponent', moveIndex: 0 } // Tackle ); } } // Should either end naturally or reach turn limit expect(turns).toBeLessThanOrEqual(maxTurns); // Engine should remain stable const state = engine.getState(); expect(state.turn).toBeGreaterThan(1); expect(state.log.length).toBeGreaterThan(0); }); it('should maintain state consistency after many operations', () => { const engine = new BattleEngine(STELLAR_WOLF, TOXIC_CRAWLER); // Perform many state-changing operations for (let i = 0; i < 10 && !engine.isGameOver(); i++) { const state = engine.getState(); // Verify state consistency before each turn expect(state.playerPiclet.currentHp).toBeGreaterThanOrEqual(0); expect(state.opponentPiclet.currentHp).toBeGreaterThanOrEqual(0); expect(state.playerPiclet.currentHp).toBeLessThanOrEqual(state.playerPiclet.maxHp); expect(state.opponentPiclet.currentHp).toBeLessThanOrEqual(state.opponentPiclet.maxHp); engine.executeActions( { type: 'move', piclet: 'player', moveIndex: i % 4 }, { type: 'move', piclet: 'opponent', moveIndex: i % 3 } ); } // Final state should still be consistent const finalState = engine.getState(); expect(finalState.playerPiclet.currentHp).toBeGreaterThanOrEqual(0); expect(finalState.opponentPiclet.currentHp).toBeGreaterThanOrEqual(0); }); }); });