Files
claude-code/packages/pokemon/src/__tests__/battle.test.ts
claude-code-best fae96c3e7f test: 补全 spriteCache/renderer/battle 测试用例
- 新增 spriteCache.test.ts: getSpeciesDisplay 格式化测试
- 扩展 renderer.test.ts: 覆盖所有 AnimMode + getIdleAnimMode + getPetOverlay
- 扩展 battle.test.ts: AI 边界情况 + settlement XP/EV 奖励 + 失败路径

188 tests / 0 fail (was 164)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-22 01:54:28 +08:00

305 lines
8.8 KiB
TypeScript

import { describe, test, expect } from 'bun:test'
import { createBattle, executeTurn } from '../battle/engine'
import { settleBattle, applyMoveLearn, applyEvolution } from '../battle/settlement'
import { chooseAIMove } from '../battle/ai'
import type { Creature, BuddyData } from '../types'
function makeTestCreature(overrides: Partial<Creature> = {}): Creature {
return {
id: overrides.id ?? 'test-1',
speciesId: overrides.speciesId ?? 'charmander',
gender: overrides.gender ?? 'male',
level: overrides.level ?? 50,
xp: 0,
totalXp: 0,
nature: overrides.nature ?? 'adamant',
ev: overrides.ev ?? { hp: 0, attack: 0, defense: 0, spAtk: 0, spDef: 0, speed: 0 },
iv: overrides.iv ?? { hp: 31, attack: 31, defense: 31, spAtk: 31, spDef: 31, speed: 31 },
moves: overrides.moves ?? [
{ id: 'flamethrower', pp: 15, maxPp: 15 },
{ id: 'airslash', pp: 15, maxPp: 15 },
{ id: 'dragontail', pp: 10, maxPp: 10 },
{ id: 'slash', pp: 20, maxPp: 20 },
],
ability: overrides.ability ?? 'blaze',
heldItem: null,
friendship: 70,
isShiny: false,
hatchedAt: Date.now(),
pokeball: 'pokeball',
}
}
function makeTestBuddyData(creatures: Creature[] = [makeTestCreature()]): BuddyData {
return {
version: 2,
party: [creatures[0]!.id, null, null, null, null, null],
boxes: [],
creatures: creatures,
eggs: [],
dex: [],
bag: { items: [] },
stats: {
totalTurns: 0,
consecutiveDays: 0,
lastActiveDate: '',
totalEggsObtained: 0,
totalEvolutions: 0,
battlesWon: 0,
battlesLost: 0,
},
}
}
describe('createBattle', () => {
test('creates battle with valid initial state', () => {
const creature = makeTestCreature()
const init = createBattle([creature], 'squirtle', 50)
expect(init.state).toBeDefined()
expect(init.state.playerPokemon).toBeDefined()
expect(init.state.opponentPokemon).toBeDefined()
expect(init.state.finished).toBe(false)
})
test('player pokemon has correct species', () => {
const creature = makeTestCreature()
const init = createBattle([creature], 'bulbasaur', 30)
expect(init.state.playerPokemon.speciesId).toBe('charmander')
expect(init.state.opponentPokemon.speciesId).toBe('bulbasaur')
})
test('player pokemon has moves', () => {
const creature = makeTestCreature()
const init = createBattle([creature], 'squirtle', 50)
expect(init.state.playerPokemon.moves.length).toBeGreaterThan(0)
})
})
describe('executeTurn', () => {
test('move action generates events', () => {
const creature = makeTestCreature()
const init = createBattle([creature], 'squirtle', 50)
const initialEventCount = init.state.events.length
const newState = executeTurn(init, { type: 'move', moveIndex: 0 })
expect(newState.events.length).toBeGreaterThanOrEqual(initialEventCount)
})
test('battle eventually ends within 50 turns', () => {
const creature = makeTestCreature({ level: 100, ev: { hp: 252, attack: 252, defense: 0, spAtk: 0, spDef: 0, speed: 252 } })
const init = createBattle([creature], 'squirtle', 5)
let state = init.state
for (let i = 0; i < 50 && !state.finished; i++) {
state = executeTurn(init, { type: 'move', moveIndex: 0 })
}
expect(state.finished).toBe(true)
})
})
describe('settleBattle', () => {
test('player win increments battlesWon', () => {
const creature = makeTestCreature()
const data: BuddyData = {
version: 2,
party: [creature.id, null, null, null, null, null],
boxes: [],
creatures: [creature],
eggs: [],
dex: [],
bag: { items: [] },
stats: {
totalTurns: 0,
consecutiveDays: 0,
lastActiveDate: '',
totalEggsObtained: 0,
totalEvolutions: 0,
battlesWon: 0,
battlesLost: 0,
},
}
const result = {
winner: 'player' as const,
turns: 5,
xpGained: 0,
evGained: { hp: 0, attack: 0, defense: 0, spAtk: 0, spDef: 0, speed: 0 },
participantIds: [creature.id],
}
const settlement = settleBattle(data, result, 'squirtle', 20)
expect(settlement.data.stats.battlesWon).toBe(1)
})
test('player loss returns unchanged data', () => {
const creature = makeTestCreature()
const data: BuddyData = {
version: 2,
party: [creature.id, null, null, null, null, null],
boxes: [],
creatures: [creature],
eggs: [],
dex: [],
bag: { items: [] },
stats: {
totalTurns: 0,
consecutiveDays: 0,
lastActiveDate: '',
totalEggsObtained: 0,
totalEvolutions: 0,
battlesWon: 0,
battlesLost: 0,
},
}
const result = {
winner: 'opponent' as const,
turns: 3,
xpGained: 0,
evGained: { hp: 0, attack: 0, defense: 0, spAtk: 0, spDef: 0, speed: 0 },
participantIds: [creature.id],
}
const settlement = settleBattle(data, result, 'squirtle', 20)
// Loss early-returns unchanged data
expect(settlement.data.creatures[0]!.totalXp).toBe(creature.totalXp)
expect(settlement.learnableMoves).toEqual([])
expect(settlement.pendingEvolutions).toEqual([])
})
})
describe('applyMoveLearn', () => {
test('replaces move at given index', () => {
const creature = makeTestCreature()
const data: BuddyData = {
version: 2,
party: [creature.id, null, null, null, null, null],
boxes: [],
creatures: [creature],
eggs: [],
dex: [],
bag: { items: [] },
stats: {
totalTurns: 0,
consecutiveDays: 0,
lastActiveDate: '',
totalEggsObtained: 0,
totalEvolutions: 0,
battlesWon: 0,
battlesLost: 0,
},
}
const updated = applyMoveLearn(data, creature.id, 'fireblast', 3)
expect(updated.creatures[0]!.moves[3]!.id).toBe('fireblast')
})
})
describe('applyEvolution', () => {
test('evolves charmander to charmeleon and increments counter', () => {
const creature = makeTestCreature({ speciesId: 'charmander' })
const data: BuddyData = {
version: 2,
party: [creature.id, null, null, null, null, null],
boxes: [],
creatures: [creature],
eggs: [],
dex: [],
bag: { items: [] },
stats: {
totalTurns: 0,
consecutiveDays: 0,
lastActiveDate: '',
totalEggsObtained: 0,
totalEvolutions: 0,
battlesWon: 0,
battlesLost: 0,
},
}
const updated = applyEvolution(data, creature.id, 'charmeleon')
expect(updated.creatures[0]!.speciesId).toBe('charmeleon')
expect(updated.stats.totalEvolutions).toBe(1)
})
})
describe('chooseAIMove', () => {
test('returns a valid move index', () => {
const creature = makeTestCreature()
const init = createBattle([creature], 'squirtle', 50)
const aiPokemon = init.state.opponentPokemon
const idx = chooseAIMove(aiPokemon)
expect(idx).toBeGreaterThanOrEqual(0)
expect(idx).toBeLessThan(aiPokemon.moves.length)
})
test('returns 0 when all moves have 0 PP', () => {
const pokemon = {
...makeTestCreature(),
moves: [
{ id: 'tackle', name: 'Tackle', type: 'Normal', pp: 0, maxPp: 35, disabled: false },
],
}
const idx = chooseAIMove(pokemon as any)
expect(idx).toBe(0) // Struggle fallback
})
test('skips disabled moves', () => {
const pokemon = {
...makeTestCreature(),
moves: [
{ id: 'tackle', name: 'Tackle', type: 'Normal', pp: 35, maxPp: 35, disabled: true },
{ id: 'scratch', name: 'Scratch', type: 'Normal', pp: 35, maxPp: 35, disabled: false },
],
}
const idx = chooseAIMove(pokemon as any)
expect(idx).toBe(1) // Only non-disabled move
})
})
describe('settleBattle - advanced', () => {
test('player win awards XP to creature', () => {
const creature = makeTestCreature({ level: 5 })
const data = makeTestBuddyData([creature])
const result = {
winner: 'player' as const,
turns: 3,
xpGained: 0,
evGained: { hp: 0, attack: 0, defense: 0, spAtk: 0, spDef: 0, speed: 0 },
participantIds: [creature.id],
}
const settlement = settleBattle(data, result, 'squirtle', 20)
expect(settlement.data.creatures[0]!.totalXp).toBeGreaterThan(0)
})
test('player win awards EVs (capped at 252 per stat)', () => {
const creature = makeTestCreature({
level: 5,
ev: { hp: 250, attack: 250, defense: 250, spAtk: 250, spDef: 250, speed: 250 },
})
const data = makeTestBuddyData([creature])
const result = {
winner: 'player' as const,
turns: 3,
xpGained: 0,
evGained: { hp: 0, attack: 0, defense: 0, spAtk: 0, spDef: 0, speed: 0 },
participantIds: [creature.id],
}
const settlement = settleBattle(data, result, 'squirtle', 20)
for (const stat of ['hp', 'attack', 'defense', 'spAtk', 'spDef', 'speed'] as const) {
expect(settlement.data.creatures[0]!.ev[stat]).toBeLessThanOrEqual(252)
}
})
test('player loss does not increment battlesWon', () => {
const creature = makeTestCreature()
const data = makeTestBuddyData([creature])
const result = {
winner: 'opponent' as const,
turns: 3,
xpGained: 0,
evGained: { hp: 0, attack: 0, defense: 0, spAtk: 0, spDef: 0, speed: 0 },
participantIds: [creature.id],
}
const settlement = settleBattle(data, result, 'squirtle', 20)
expect(settlement.data.stats.battlesWon).toBe(0)
})
})