| |
| |
| |
| |
|
|
| 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); |
| |
| |
| 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(); |
| |
| |
| 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'); |
| |
| |
| const moveActions = actions.filter(a => a.type === 'move'); |
| expect(moveActions.length).toBeGreaterThan(0); |
| |
| |
| 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); |
| |
| |
| 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 |
| }], |
| opponent: [{ |
| type: 'move', |
| side: 'opponent', |
| position: 0, |
| moveIndex: 0 |
| }] |
| }; |
|
|
| 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 |
| }], |
| 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 |
| }, |
| { |
| 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(); |
| 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 |
| }, |
| { |
| type: 'switch', |
| side: 'player', |
| position: 1, |
| partyIndex: 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(); |
| 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 |
| } |
| ], |
| opponent: [ |
| { |
| type: 'switch', |
| side: 'opponent', |
| position: 0, |
| partyIndex: 1 |
| } |
| ] |
| }; |
|
|
| doubleEngine.executeTurn(turnActions); |
| |
| const log = doubleEngine.getLog(); |
| const switchIndex = log.findIndex(msg => msg.includes('switched')); |
| const moveIndex = log.findIndex(msg => msg.includes('used')); |
| |
| |
| if (switchIndex !== -1 && moveIndex !== -1) { |
| expect(switchIndex).toBeLessThan(moveIndex); |
| } |
| }); |
|
|
| it('should use speed for same priority actions', () => { |
| |
| const turnActions: TurnActions = { |
| player: [{ |
| type: 'move', |
| side: 'player', |
| position: 0, |
| moveIndex: 0 |
| }], |
| opponent: [{ |
| type: 'move', |
| side: 'opponent', |
| position: 0, |
| moveIndex: 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')); |
| |
| |
| expect(stellarIndex).toBeLessThan(toxicIndex); |
| }); |
| }); |
|
|
| describe('Victory Conditions', () => { |
| it('should end battle when all opponent Piclets faint', () => { |
| |
| const singleOpponentConfig: MultiBattleConfig = { |
| playerParty: [STELLAR_WOLF, BERSERKER_BEAST], |
| opponentParty: [TOXIC_CRAWLER], |
| playerActiveCount: 1, |
| opponentActiveCount: 1, |
| battleType: 'single' |
| }; |
| const singleEngine = new MultiBattleEngine(singleOpponentConfig); |
| |
| |
| (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', () => { |
| |
| |
| expect(true).toBe(true); |
| }); |
| }); |
|
|
| describe('Targeting System', () => { |
| it('should target opponents correctly in single battles', () => { |
| const turnActions: TurnActions = { |
| player: [{ |
| type: 'move', |
| side: 'player', |
| position: 0, |
| moveIndex: 0 |
| }], |
| 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); |
|
|
| |
| 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 |
| }] |
| }; |
|
|
| |
| (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 |
| }], |
| 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; |
|
|
| |
| expect(finalHp1).toBeLessThan(initialHp1); |
| expect(finalHp2).toBeLessThan(initialHp2); |
| }); |
|
|
| it('should target self correctly', () => { |
| |
| 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 |
| }] |
| }; |
|
|
| |
| (engine as any).state.activePiclets.player[0].currentHp = 50; |
| |
| |
| (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 |
| }], |
| opponent: [{ |
| type: 'move', |
| side: 'opponent', |
| position: 0, |
| moveIndex: 0 |
| }] |
| }; |
|
|
| engine.executeTurn(turnActions); |
|
|
| const finalHp = engine.getState().activePiclets.player[0]!.currentHp; |
| |
| |
| 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); |
|
|
| |
| (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); |
| }); |
| }); |
|
|
| 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 |
| }], |
| 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 |
| }], |
| 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', () => { |
| |
| (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 |
| }] |
| }; |
|
|
| engine.executeTurn(turnActions); |
| |
| const log = engine.getLog(); |
| expect(log.some(msg => msg.includes('fainted!'))).toBe(true); |
| |
| |
| 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, |
| moveIndex: 0 |
| }], |
| opponent: [{ |
| type: 'move', |
| side: 'opponent', |
| position: 0, |
| moveIndex: 0 |
| }] |
| }; |
|
|
| expect(() => { |
| engine.executeTurn(turnActions); |
| }).not.toThrow(); |
| }); |
| }); |
| }); |