piclets / src /lib /battle-engine /integration.test.ts
Fraser's picture
better logs
ba9896a
/**
* 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);
});
});
});