|
|
|
|
|
|
|
|
|
|
|
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(); |
|
}); |
|
}); |
|
}); |