piclets / src /lib /battle-engine /extreme-risk-reward.test.ts
Fraser's picture
ALL TESTS
01d657e
import { describe, it, expect, beforeEach } from 'vitest';
import { BattleEngine } from './BattleEngine';
import type { PicletDefinition } from './types';
import { PicletType, AttackType } from './types';
describe('Extreme Risk-Reward Moves', () => {
describe('Self-Destruct Moves', () => {
it('should deal massive damage to all but KO the user', () => {
const bomber: PicletDefinition = {
name: "Suicide Bomber",
description: "Sacrifices itself for massive damage",
tier: 'medium',
primaryType: PicletType.MACHINA,
baseStats: { hp: 60, attack: 40, defense: 60, speed: 50 },
nature: "Brave",
specialAbility: { name: "No Ability", description: "" },
movepool: [
{
name: "Self Destruct",
type: AttackType.MACHINA,
power: 200,
accuracy: 100,
pp: 1,
priority: 0,
flags: ['explosive', 'contact'],
effects: [
{
type: 'damage',
target: 'all',
formula: 'standard',
multiplier: 1.5
},
{
type: 'damage',
target: 'self',
formula: 'fixed',
value: 9999,
condition: 'afterUse'
}
]
}
]
};
const opponent: PicletDefinition = {
name: "Sturdy Opponent",
description: "Tanky opponent",
tier: 'high',
primaryType: PicletType.MINERAL,
baseStats: { hp: 100, attack: 60, defense: 100, speed: 40 },
nature: "Impish",
specialAbility: { name: "No Ability", description: "" },
movepool: [
{
name: "Tackle",
type: AttackType.NORMAL,
power: 40,
accuracy: 100,
pp: 10,
priority: 0,
flags: ['contact'],
effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
}
]
};
const engine = new BattleEngine(bomber, opponent);
const initialOpponentHp = engine.getState().opponentPiclet.currentHp;
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 }, // Self Destruct
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const finalOpponentHp = engine.getState().opponentPiclet.currentHp;
const playerHp = engine.getState().playerPiclet.currentHp;
// User should be KO'd
expect(playerHp).toBe(0);
// Opponent should take massive damage
expect(finalOpponentHp).toBeLessThan(initialOpponentHp);
const damage = initialOpponentHp - finalOpponentHp;
expect(damage).toBeGreaterThan(45); // Should be very high damage for a self-destruct move
const log = engine.getLog();
expect(log.some(msg => msg.includes('Self Destruct') || msg.includes('exploded'))).toBe(true);
expect(engine.isGameOver()).toBe(true);
});
});
describe('Gambling Moves', () => {
it('should have random success/failure outcomes', () => {
const gambler: PicletDefinition = {
name: "Lucky Gambler",
description: "Relies on luck for power",
tier: 'medium',
primaryType: PicletType.CULTURE,
baseStats: { hp: 70, attack: 60, defense: 60, speed: 80 },
nature: "Hasty",
specialAbility: { name: "No Ability", description: "" },
movepool: [
{
name: "Cursed Gambit",
type: AttackType.CULTURE,
power: 0,
accuracy: 100,
pp: 1,
priority: 0,
flags: ['gambling', 'cursed'],
effects: [
{
type: 'heal',
target: 'self',
amount: 'percentage',
value: 100,
condition: 'ifLucky50'
},
{
type: 'damage',
target: 'self',
formula: 'fixed',
value: 9999,
condition: 'ifUnlucky50'
}
]
}
]
};
const opponent: PicletDefinition = {
name: "Opponent",
description: "Standard opponent",
tier: 'medium',
primaryType: PicletType.BEAST,
baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 },
nature: "Hardy",
specialAbility: { name: "No Ability", description: "" },
movepool: [
{
name: "Do Nothing",
type: AttackType.NORMAL,
power: 0,
accuracy: 100,
pp: 10,
priority: 0,
flags: [],
effects: [] // No effects - just waste a turn
}
]
};
// Test multiple times to check for randomness
let healedCount = 0;
let faintedCount = 0;
for (let i = 0; i < 20; i++) {
const engine = new BattleEngine(gambler, opponent);
// Damage the gambler first
engine['state'].playerPiclet.currentHp = Math.floor(engine['state'].playerPiclet.maxHp * 0.5);
const preGambitHp = engine.getState().playerPiclet.currentHp;
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const postGambitHp = engine.getState().playerPiclet.currentHp;
const maxHp = engine.getState().playerPiclet.maxHp;
if (postGambitHp === 0) {
faintedCount++;
} else if (postGambitHp > preGambitHp) {
healedCount++;
}
}
// Should have some of each outcome (allowing for randomness)
expect(healedCount + faintedCount).toBeGreaterThan(0);
expect(healedCount).toBeGreaterThan(0);
expect(faintedCount).toBeGreaterThan(0);
});
});
describe('Sacrifice Moves', () => {
it('should provide powerful effects at great personal cost', () => {
const sacrificer: PicletDefinition = {
name: "Blood Warrior",
description: "Sacrifices HP for power",
tier: 'medium',
primaryType: PicletType.BEAST,
baseStats: { hp: 100, attack: 70, defense: 60, speed: 60 },
nature: "Brave",
specialAbility: { name: "No Ability", description: "" },
movepool: [
{
name: "Blood Pact",
type: AttackType.BEAST,
power: 0,
accuracy: 100,
pp: 3,
priority: 0,
flags: ['sacrifice'],
effects: [
{
type: 'damage',
target: 'self',
formula: 'percentage',
value: 50
},
{
type: 'mechanicOverride',
target: 'self',
mechanic: 'damageMultiplier',
value: 2.0,
condition: 'restOfBattle'
}
]
},
{
name: "Strike",
type: AttackType.BEAST,
power: 60,
accuracy: 100,
pp: 10,
priority: 0,
flags: ['contact'],
effects: [
{
type: 'damage',
target: 'opponent',
amount: 'normal'
}
]
}
]
};
const opponent: PicletDefinition = {
name: "Opponent",
description: "Standard opponent",
tier: 'medium',
primaryType: PicletType.BEAST,
baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 },
nature: "Hardy",
specialAbility: { name: "No Ability", description: "" },
movepool: [
{
name: "Tackle",
type: AttackType.NORMAL,
power: 40,
accuracy: 100,
pp: 10,
priority: 0,
flags: ['contact'],
effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
}
]
};
const engine = new BattleEngine(sacrificer, opponent);
const initialHp = engine.getState().playerPiclet.currentHp;
// Use Blood Pact
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 }, // Blood Pact
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const hpAfterSacrifice = engine.getState().playerPiclet.currentHp;
expect(hpAfterSacrifice).toBeLessThan(initialHp);
// Now attack should do double damage
const initialOpponentHp = engine.getState().opponentPiclet.currentHp;
engine.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 1 }, // Strike (should be doubled)
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const finalOpponentHp = engine.getState().opponentPiclet.currentHp;
const damage = initialOpponentHp - finalOpponentHp;
// Should do significantly more damage than normal (doubled)
expect(damage).toBeGreaterThan(60); // Normal would be ~30-40
const log = engine.getLog();
expect(log.some(msg => msg.includes('Blood Pact') || msg.includes('sacrifice'))).toBe(true);
});
});
describe('Conditional Power Scaling', () => {
it('should scale damage based on conditions', () => {
const revengeUser: PicletDefinition = {
name: "Revenge Fighter",
description: "Gets stronger when damaged",
tier: 'medium',
primaryType: PicletType.BEAST,
baseStats: { hp: 90, attack: 70, defense: 80, speed: 50 },
nature: "Brave",
specialAbility: { name: "No Ability", description: "" },
movepool: [
{
name: "Revenge Strike",
type: AttackType.BEAST,
power: 60,
accuracy: 100,
pp: 10,
priority: 0,
flags: ['contact'],
effects: [
{
type: 'damage',
target: 'opponent',
amount: 'normal'
},
{
type: 'damage',
target: 'opponent',
amount: 'strong',
condition: 'ifDamagedThisTurn'
}
]
}
]
};
const attacker: PicletDefinition = {
name: "Fast Attacker",
description: "Quick attacker",
tier: 'medium',
primaryType: PicletType.BEAST,
baseStats: { hp: 200, attack: 80, defense: 60, speed: 100 },
nature: "Hasty",
specialAbility: { name: "No Ability", description: "" },
movepool: [
{
name: "Quick Strike",
type: AttackType.BEAST,
power: 50,
accuracy: 100,
pp: 10,
priority: 1,
flags: ['contact', 'priority'],
effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
}
]
};
// First test: revenge without being damaged first (revenge user at full HP)
const engine1 = new BattleEngine(revengeUser, attacker);
const initialOpponentHp = engine1.getState().opponentPiclet.currentHp;
engine1.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const hpAfterNormalRevenge = engine1.getState().opponentPiclet.currentHp;
const normalRevengeDamage = initialOpponentHp - hpAfterNormalRevenge;
// Second test: revenge when damaged (revenge user starts damaged)
const engine2 = new BattleEngine(revengeUser, attacker);
// Damage the revenge user to trigger the condition
engine2['state'].playerPiclet.currentHp = Math.floor(engine2['state'].playerPiclet.maxHp * 0.5);
const initialOpponentHp2 = engine2.getState().opponentPiclet.currentHp;
engine2.executeActions(
{ type: 'move', piclet: 'player', moveIndex: 0 },
{ type: 'move', piclet: 'opponent', moveIndex: 0 }
);
const hpAfterPoweredRevenge = engine2.getState().opponentPiclet.currentHp;
const poweredRevengeDamage = initialOpponentHp2 - hpAfterPoweredRevenge;
// Verify that the conditional effect triggered by checking for multiple damage instances
const damageMessages = engine2.getLog().filter(msg => msg.includes('took') && msg.includes('damage'));
expect(damageMessages.length).toBeGreaterThanOrEqual(3); // Attacker hits revenge user, then revenge user hits back twice
// Verify the powered revenge did more damage overall
expect(poweredRevengeDamage).toBeGreaterThan(100); // Should be significant damage from both effects
});
});
});