/** * Tests for Multi-Piclet Battle Engine * Covers battles with up to 4 Piclets on the field at once */ import { describe, it, expect, beforeEach } from 'vitest'; import { MultiBattleEngine } from './MultiBattleEngine'; import { MultiBattleConfig, TurnActions } from './multi-piclet-types'; import { PicletType, AttackType } from './types'; import { STELLAR_WOLF, TOXIC_CRAWLER, BERSERKER_BEAST, AQUA_GUARDIAN } from './test-data'; describe('MultiBattleEngine', () => { let config: MultiBattleConfig; let engine: MultiBattleEngine; beforeEach(() => { config = { playerParty: [STELLAR_WOLF, BERSERKER_BEAST], opponentParty: [TOXIC_CRAWLER, AQUA_GUARDIAN], playerActiveCount: 1, opponentActiveCount: 1, battleType: 'single' }; engine = new MultiBattleEngine(config); }); describe('Battle Initialization', () => { it('should initialize single battle correctly', () => { const state = engine.getState(); expect(state.turn).toBe(1); expect(state.phase).toBe('selection'); expect(state.activePiclets.player).toHaveLength(2); expect(state.activePiclets.opponent).toHaveLength(2); // First position should be active, second should be null expect(state.activePiclets.player[0]).not.toBeNull(); expect(state.activePiclets.player[1]).toBeNull(); expect(state.activePiclets.opponent[0]).not.toBeNull(); expect(state.activePiclets.opponent[1]).toBeNull(); }); it('should initialize double battle correctly', () => { const doubleConfig: MultiBattleConfig = { playerParty: [STELLAR_WOLF, BERSERKER_BEAST], opponentParty: [TOXIC_CRAWLER, AQUA_GUARDIAN], playerActiveCount: 2, opponentActiveCount: 2, battleType: 'double' }; const doubleEngine = new MultiBattleEngine(doubleConfig); const state = doubleEngine.getState(); // Both positions should be active expect(state.activePiclets.player[0]).not.toBeNull(); expect(state.activePiclets.player[1]).not.toBeNull(); expect(state.activePiclets.opponent[0]).not.toBeNull(); expect(state.activePiclets.opponent[1]).not.toBeNull(); }); it('should handle parties correctly', () => { const state = engine.getState(); expect(state.parties.player).toHaveLength(2); expect(state.parties.opponent).toHaveLength(2); expect(state.parties.player[0].name).toBe('Stellar Wolf'); expect(state.parties.opponent[0].name).toBe('Toxic Crawler'); }); }); describe('Action Generation', () => { it('should generate valid move actions for active Piclets', () => { const actions = engine.getValidActions('player'); // Should have move actions for the active Piclet const moveActions = actions.filter(a => a.type === 'move'); expect(moveActions.length).toBeGreaterThan(0); // All move actions should be for position 0 (the active Piclet) moveActions.forEach(action => { expect((action as any).position).toBe(0); }); }); it('should generate switch actions for party members', () => { const actions = engine.getValidActions('player'); const switchActions = actions.filter(a => a.type === 'switch'); expect(switchActions.length).toBeGreaterThan(0); // Should be able to switch the inactive party member into position 0 const switchToPosition0 = switchActions.find(a => (a as any).position === 0 && (a as any).partyIndex === 1 ); expect(switchToPosition0).toBeDefined(); }); }); describe('Single Battle Execution', () => { it('should execute a single battle turn correctly', () => { const turnActions: TurnActions = { player: [{ type: 'move', side: 'player', position: 0, moveIndex: 0 // Tackle }], opponent: [{ type: 'move', side: 'opponent', position: 0, moveIndex: 0 // Tackle }] }; const initialOpponentHp = engine.getState().activePiclets.opponent[0]!.currentHp; engine.executeTurn(turnActions); const finalOpponentHp = engine.getState().activePiclets.opponent[0]!.currentHp; expect(finalOpponentHp).toBeLessThan(initialOpponentHp); const log = engine.getLog(); expect(log.some(msg => msg.includes('used Tackle'))).toBe(true); }); it('should handle switch actions correctly', () => { const turnActions: TurnActions = { player: [{ type: 'switch', side: 'player', position: 0, partyIndex: 1 // Switch to Berserker Beast }], opponent: [{ type: 'move', side: 'opponent', position: 0, moveIndex: 0 }] }; const initialName = engine.getState().activePiclets.player[0]!.definition.name; engine.executeTurn(turnActions); const finalName = engine.getState().activePiclets.player[0]!.definition.name; expect(initialName).toBe('Stellar Wolf'); expect(finalName).toBe('Berserker Beast'); const log = engine.getLog(); expect(log.some(msg => msg.includes('switched out'))).toBe(true); expect(log.some(msg => msg.includes('switched in'))).toBe(true); }); }); describe('Double Battle System', () => { let doubleEngine: MultiBattleEngine; beforeEach(() => { const doubleConfig: MultiBattleConfig = { playerParty: [STELLAR_WOLF, BERSERKER_BEAST], opponentParty: [TOXIC_CRAWLER, AQUA_GUARDIAN], playerActiveCount: 2, opponentActiveCount: 2, battleType: 'double' }; doubleEngine = new MultiBattleEngine(doubleConfig); }); it('should execute double battle turns correctly', () => { const turnActions: TurnActions = { player: [ { type: 'move', side: 'player', position: 0, moveIndex: 0 // Stellar Wolf uses Tackle }, { type: 'move', side: 'player', position: 1, moveIndex: 0 // Berserker Beast uses Tackle } ], opponent: [ { type: 'move', side: 'opponent', position: 0, moveIndex: 0 // Toxic Crawler uses Tackle }, { type: 'move', side: 'opponent', position: 1, moveIndex: 0 // Aqua Guardian uses Tackle } ] }; doubleEngine.executeTurn(turnActions); const log = doubleEngine.getLog(); expect(log.some(msg => msg.includes('Stellar Wolf used'))).toBe(true); expect(log.some(msg => msg.includes('Berserker Beast used'))).toBe(true); expect(log.some(msg => msg.includes('Toxic Crawler used'))).toBe(true); expect(log.some(msg => msg.includes('Aqua Guardian used'))).toBe(true); }); it('should handle mixed actions in double battles', () => { const turnActions: TurnActions = { player: [ { type: 'move', side: 'player', position: 0, moveIndex: 0 // Attack }, { type: 'switch', side: 'player', position: 1, partyIndex: 0 // This would be switching to same Piclet, but tests the system } ], opponent: [ { type: 'move', side: 'opponent', position: 0, moveIndex: 0 }, { type: 'move', side: 'opponent', position: 1, moveIndex: 0 } ] }; doubleEngine.executeTurn(turnActions); const log = doubleEngine.getLog(); expect(log.some(msg => msg.includes('used'))).toBe(true); }); }); describe('Action Priority System', () => { it('should prioritize switch actions over moves', () => { const doubleConfig: MultiBattleConfig = { playerParty: [STELLAR_WOLF, BERSERKER_BEAST], opponentParty: [TOXIC_CRAWLER, AQUA_GUARDIAN], playerActiveCount: 2, opponentActiveCount: 2, battleType: 'double' }; const doubleEngine = new MultiBattleEngine(doubleConfig); const turnActions: TurnActions = { player: [ { type: 'move', side: 'player', position: 0, moveIndex: 0 // Regular move } ], opponent: [ { type: 'switch', side: 'opponent', position: 0, partyIndex: 1 // Switch action (should go first) } ] }; doubleEngine.executeTurn(turnActions); const log = doubleEngine.getLog(); const switchIndex = log.findIndex(msg => msg.includes('switched')); const moveIndex = log.findIndex(msg => msg.includes('used')); // Switch should happen before move (if both occurred) if (switchIndex !== -1 && moveIndex !== -1) { expect(switchIndex).toBeLessThan(moveIndex); } }); it('should use speed for same priority actions', () => { // Stellar Wolf (speed 70) vs Toxic Crawler (speed 55) const turnActions: TurnActions = { player: [{ type: 'move', side: 'player', position: 0, moveIndex: 0 // Tackle (priority 0) }], opponent: [{ type: 'move', side: 'opponent', position: 0, moveIndex: 0 // Tackle (priority 0) }] }; engine.executeTurn(turnActions); const log = engine.getLog(); const stellarIndex = log.findIndex(msg => msg.includes('Stellar Wolf used')); const toxicIndex = log.findIndex(msg => msg.includes('Toxic Crawler used')); // Stellar Wolf should go first due to higher speed expect(stellarIndex).toBeLessThan(toxicIndex); }); }); describe('Victory Conditions', () => { it('should end battle when all opponent Piclets faint', () => { // Create a battle with single-Piclet opponent party (no reserves) const singleOpponentConfig: MultiBattleConfig = { playerParty: [STELLAR_WOLF, BERSERKER_BEAST], opponentParty: [TOXIC_CRAWLER], // Only one Piclet, no reserves playerActiveCount: 1, opponentActiveCount: 1, battleType: 'single' }; const singleEngine = new MultiBattleEngine(singleOpponentConfig); // Set opponent to very low HP (singleEngine as any).state.activePiclets.opponent[0]!.currentHp = 1; const turnActions: TurnActions = { player: [{ type: 'move', side: 'player', position: 0, moveIndex: 0 }], opponent: [{ type: 'move', side: 'opponent', position: 0, moveIndex: 0 }] }; singleEngine.executeTurn(turnActions); expect(singleEngine.isGameOver()).toBe(true); expect(singleEngine.getWinner()).toBe('player'); }); it('should continue battle when reserves are available', () => { // This test would require implementing automatic switching // when a Piclet faints, which is more complex expect(true).toBe(true); // Placeholder }); }); describe('Targeting System', () => { it('should target opponents correctly in single battles', () => { const turnActions: TurnActions = { player: [{ type: 'move', side: 'player', position: 0, moveIndex: 0 // Attack should hit opponent }], opponent: [{ type: 'move', side: 'opponent', position: 0, moveIndex: 0 }] }; const initialHp = engine.getState().activePiclets.opponent[0]!.currentHp; engine.executeTurn(turnActions); const finalHp = engine.getState().activePiclets.opponent[0]!.currentHp; expect(finalHp).toBeLessThan(initialHp); }); it('should target all opponents in double battles', () => { const doubleConfig: MultiBattleConfig = { playerParty: [STELLAR_WOLF, BERSERKER_BEAST], opponentParty: [TOXIC_CRAWLER, AQUA_GUARDIAN], playerActiveCount: 2, opponentActiveCount: 2, battleType: 'double' }; const doubleEngine = new MultiBattleEngine(doubleConfig); // Create a multi-target move for testing const multiTargetMove = { name: 'Mass Strike', type: 'normal' as any, power: 30, accuracy: 100, pp: 10, priority: 0, flags: [] as any, effects: [{ type: 'damage' as any, target: 'allOpponents' as any, amount: 'normal' as any }] }; // Add the move to the attacker (doubleEngine as any).state.activePiclets.player[0].moves[0] = { move: multiTargetMove, currentPP: 10 }; const initialHp1 = doubleEngine.getState().activePiclets.opponent[0]!.currentHp; const initialHp2 = doubleEngine.getState().activePiclets.opponent[1]!.currentHp; const turnActions: TurnActions = { player: [{ type: 'move', side: 'player', position: 0, moveIndex: 0 // Multi-target move }], opponent: [ { type: 'move', side: 'opponent', position: 0, moveIndex: 0 }, { type: 'move', side: 'opponent', position: 1, moveIndex: 0 } ] }; doubleEngine.executeTurn(turnActions); const finalHp1 = doubleEngine.getState().activePiclets.opponent[0]!.currentHp; const finalHp2 = doubleEngine.getState().activePiclets.opponent[1]!.currentHp; // Both opponents should take damage expect(finalHp1).toBeLessThan(initialHp1); expect(finalHp2).toBeLessThan(initialHp2); }); it('should target self correctly', () => { // Create a self-targeting move (like heal) const selfTargetMove = { name: 'Self Heal', type: 'normal' as any, power: 0, accuracy: 100, pp: 10, priority: 0, flags: [] as any, effects: [{ type: 'heal' as any, target: 'self' as any, amount: 'medium' as any }] }; // Damage the Piclet first then heal (engine as any).state.activePiclets.player[0].currentHp = 50; // Add the heal move (engine as any).state.activePiclets.player[0].moves[0] = { move: selfTargetMove, currentPP: 10 }; const initialHp = engine.getState().activePiclets.player[0]!.currentHp; const turnActions: TurnActions = { player: [{ type: 'move', side: 'player', position: 0, moveIndex: 0 // Self-heal move }], opponent: [{ type: 'move', side: 'opponent', position: 0, moveIndex: 0 }] }; engine.executeTurn(turnActions); const finalHp = engine.getState().activePiclets.player[0]!.currentHp; // Player should have more HP after healing expect(finalHp).toBeGreaterThan(initialHp); }); }); describe('Status Effects in Multi-Battle', () => { it('should process status effects for all active Piclets', () => { const doubleConfig: MultiBattleConfig = { playerParty: [STELLAR_WOLF, BERSERKER_BEAST], opponentParty: [TOXIC_CRAWLER, AQUA_GUARDIAN], playerActiveCount: 2, opponentActiveCount: 2, battleType: 'double' }; const doubleEngine = new MultiBattleEngine(doubleConfig); // Apply poison to both active player Piclets (doubleEngine as any).state.activePiclets.player[0]!.statusEffects.push('poison'); (doubleEngine as any).state.activePiclets.player[1]!.statusEffects.push('poison'); const turnActions: TurnActions = { player: [ { type: 'move', side: 'player', position: 0, moveIndex: 0 }, { type: 'move', side: 'player', position: 1, moveIndex: 0 } ], opponent: [ { type: 'move', side: 'opponent', position: 0, moveIndex: 0 }, { type: 'move', side: 'opponent', position: 1, moveIndex: 0 } ] }; doubleEngine.executeTurn(turnActions); const log = doubleEngine.getLog(); const poisonMessages = log.filter(msg => msg.includes('hurt by poison')); expect(poisonMessages.length).toBe(2); // Both Piclets should take poison damage }); }); describe('Active Piclet Tracking', () => { it('should correctly track active Piclets', () => { const actives = engine.getActivePiclets(); expect(actives.player).toHaveLength(1); expect(actives.opponent).toHaveLength(1); expect(actives.player[0].definition.name).toBe('Stellar Wolf'); expect(actives.opponent[0].definition.name).toBe('Toxic Crawler'); }); it('should update active tracking after switches', () => { const turnActions: TurnActions = { player: [{ type: 'switch', side: 'player', position: 0, partyIndex: 1 // Switch to Berserker Beast }], opponent: [{ type: 'move', side: 'opponent', position: 0, moveIndex: 0 }] }; engine.executeTurn(turnActions); const actives = engine.getActivePiclets(); expect(actives.player[0].definition.name).toBe('Berserker Beast'); }); }); describe('Party Management', () => { it('should track available switches correctly', () => { const availableSwitches = engine.getAvailableSwitches('player'); expect(availableSwitches).toHaveLength(1); expect(availableSwitches[0].piclet.name).toBe('Berserker Beast'); expect(availableSwitches[0].partyIndex).toBe(1); }); it('should update available switches after switching', () => { const turnActions: TurnActions = { player: [{ type: 'switch', side: 'player', position: 0, partyIndex: 1 // Switch to Berserker Beast }], opponent: [{ type: 'move', side: 'opponent', position: 0, moveIndex: 0 }] }; engine.executeTurn(turnActions); const availableSwitches = engine.getAvailableSwitches('player'); expect(availableSwitches).toHaveLength(1); expect(availableSwitches[0].piclet.name).toBe('Stellar Wolf'); expect(availableSwitches[0].partyIndex).toBe(0); }); it('should handle fainted Piclets correctly', () => { // Set player Piclet to very low HP (engine as any).state.activePiclets.player[0]!.currentHp = 1; const turnActions: TurnActions = { player: [{ type: 'move', side: 'player', position: 0, moveIndex: 0 }], opponent: [{ type: 'move', side: 'opponent', position: 0, moveIndex: 0 // This should cause player to faint }] }; engine.executeTurn(turnActions); const log = engine.getLog(); expect(log.some(msg => msg.includes('fainted!'))).toBe(true); // Active Piclet should be removed (null) const state = engine.getState(); expect(state.activePiclets.player[0]).toBeNull(); }); }); describe('Edge Cases', () => { it('should handle empty action arrays gracefully', () => { const turnActions: TurnActions = { player: [], opponent: [{ type: 'move', side: 'opponent', position: 0, moveIndex: 0 }] }; expect(() => { engine.executeTurn(turnActions); }).not.toThrow(); }); it('should handle invalid positions gracefully', () => { const turnActions: TurnActions = { player: [{ type: 'move', side: 'player', position: 1, // Invalid position (empty slot) moveIndex: 0 }], opponent: [{ type: 'move', side: 'opponent', position: 0, moveIndex: 0 }] }; expect(() => { engine.executeTurn(turnActions); }).not.toThrow(); }); }); });