mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
test: 新增数据层测试 + 引擎修复
- 新增 pkmn.test.ts: stat 映射测试 - 新增 species.test.ts: 物种数据测试 - 新增 xpTable.test.ts: XP 公式测试 - 新增 evMapping.test.ts: EV 映射测试 - 新增 names.test.ts: 多语言名称测试 - 新增 fallback.test.ts: 精灵 fallback 测试 - 修复 engine.ts 类型 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
39
packages/pokemon/src/__tests__/evMapping.test.ts
Normal file
39
packages/pokemon/src/__tests__/evMapping.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { getEVForTool, DEFAULT_EV_MAPPING, MAX_EV_PER_STAT, MAX_EV_TOTAL } from '../data/evMapping'
|
||||
|
||||
describe('getEVForTool', () => {
|
||||
test('returns EV mapping for known tools', () => {
|
||||
const bashEV = getEVForTool('Bash')
|
||||
expect(bashEV).toBeDefined()
|
||||
expect(bashEV!.attack).toBe(2)
|
||||
expect(bashEV!.speed).toBe(1)
|
||||
})
|
||||
|
||||
test('returns undefined for unknown tools', () => {
|
||||
expect(getEVForTool('UnknownTool')).toBeUndefined()
|
||||
})
|
||||
|
||||
test('all mapped tools have correct stat shape', () => {
|
||||
for (const [, ev] of Object.entries(DEFAULT_EV_MAPPING)) {
|
||||
expect(ev.hp).toBeDefined()
|
||||
expect(ev.attack).toBeDefined()
|
||||
expect(ev.defense).toBeDefined()
|
||||
expect(ev.spAtk).toBeDefined()
|
||||
expect(ev.spDef).toBeDefined()
|
||||
expect(ev.speed).toBeDefined()
|
||||
// EVs should sum to > 0
|
||||
const total = ev.hp + ev.attack + ev.defense + ev.spAtk + ev.spDef + ev.speed
|
||||
expect(total).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('EV constants', () => {
|
||||
test('MAX_EV_PER_STAT is 252', () => {
|
||||
expect(MAX_EV_PER_STAT).toBe(252)
|
||||
})
|
||||
|
||||
test('MAX_EV_TOTAL is 510', () => {
|
||||
expect(MAX_EV_TOTAL).toBe(510)
|
||||
})
|
||||
})
|
||||
28
packages/pokemon/src/__tests__/fallback.test.ts
Normal file
28
packages/pokemon/src/__tests__/fallback.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { getFallbackSprite } from '../sprites/fallback'
|
||||
import { ALL_SPECIES_IDS } from '../types'
|
||||
|
||||
describe('getFallbackSprite', () => {
|
||||
test('returns 5 lines for every species', () => {
|
||||
for (const id of ALL_SPECIES_IDS) {
|
||||
const sprite = getFallbackSprite(id)
|
||||
expect(sprite.length).toBe(5)
|
||||
}
|
||||
})
|
||||
|
||||
test('returns pikachu fallback for unknown species', () => {
|
||||
const sprite = getFallbackSprite('unknown' as any)
|
||||
expect(sprite).toEqual(getFallbackSprite('pikachu'))
|
||||
})
|
||||
|
||||
test('each line has consistent width', () => {
|
||||
for (const id of ALL_SPECIES_IDS) {
|
||||
const sprite = getFallbackSprite(id)
|
||||
const widths = sprite.map(line => line.length)
|
||||
// All lines should be roughly the same width
|
||||
const maxWidth = Math.max(...widths)
|
||||
const minWidth = Math.min(...widths)
|
||||
expect(maxWidth - minWidth).toBeLessThanOrEqual(2)
|
||||
}
|
||||
})
|
||||
})
|
||||
44
packages/pokemon/src/__tests__/names.test.ts
Normal file
44
packages/pokemon/src/__tests__/names.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { SPECIES_NAMES, SPECIES_I18N, SPECIES_PERSONALITY } from '../data/names'
|
||||
import { ALL_SPECIES_IDS } from '../types'
|
||||
|
||||
describe('SPECIES_NAMES', () => {
|
||||
test('has name for every species', () => {
|
||||
for (const id of ALL_SPECIES_IDS) {
|
||||
expect(SPECIES_NAMES[id]).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
test('Charmander name is correct', () => {
|
||||
expect(SPECIES_NAMES.charmander).toBe('Charmander')
|
||||
})
|
||||
})
|
||||
|
||||
describe('SPECIES_I18N', () => {
|
||||
test('has i18n for every species', () => {
|
||||
for (const id of ALL_SPECIES_IDS) {
|
||||
expect(SPECIES_I18N[id]).toBeTruthy()
|
||||
expect(SPECIES_I18N[id]!.en).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
test('has Chinese translations', () => {
|
||||
expect(SPECIES_I18N.pikachu!.zh).toBe('皮卡丘')
|
||||
expect(SPECIES_I18N.squirtle!.zh).toBe('杰尼龟')
|
||||
})
|
||||
})
|
||||
|
||||
describe('SPECIES_PERSONALITY', () => {
|
||||
test('has personality for every species', () => {
|
||||
for (const id of ALL_SPECIES_IDS) {
|
||||
expect(SPECIES_PERSONALITY[id]).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
test('personality is non-empty string', () => {
|
||||
for (const id of ALL_SPECIES_IDS) {
|
||||
expect(typeof SPECIES_PERSONALITY[id]).toBe('string')
|
||||
expect(SPECIES_PERSONALITY[id]!.length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
56
packages/pokemon/src/__tests__/pkmn.test.ts
Normal file
56
packages/pokemon/src/__tests__/pkmn.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { FROM_DEX_STAT, TO_DEX_STAT, mapBaseStats, mapGenderRatio, getPrimaryAbility } from '../data/pkmn'
|
||||
|
||||
describe('FROM_DEX_STAT', () => {
|
||||
test('maps all 6 stats', () => {
|
||||
expect(FROM_DEX_STAT.hp).toBe('hp')
|
||||
expect(FROM_DEX_STAT.atk).toBe('attack')
|
||||
expect(FROM_DEX_STAT.def).toBe('defense')
|
||||
expect(FROM_DEX_STAT.spa).toBe('spAtk')
|
||||
expect(FROM_DEX_STAT.spd).toBe('spDef')
|
||||
expect(FROM_DEX_STAT.spe).toBe('speed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('TO_DEX_STAT', () => {
|
||||
test('reverse maps all 6 stats', () => {
|
||||
expect(TO_DEX_STAT.hp).toBe('hp')
|
||||
expect(TO_DEX_STAT.attack).toBe('atk')
|
||||
expect(TO_DEX_STAT.defense).toBe('def')
|
||||
expect(TO_DEX_STAT.spAtk).toBe('spa')
|
||||
expect(TO_DEX_STAT.spDef).toBe('spd')
|
||||
expect(TO_DEX_STAT.speed).toBe('spe')
|
||||
})
|
||||
})
|
||||
|
||||
describe('mapBaseStats', () => {
|
||||
test('converts Dex stat format to our format', () => {
|
||||
const result = mapBaseStats({ hp: 45, atk: 49, def: 49, spa: 65, spd: 65, spe: 45 })
|
||||
expect(result).toEqual({
|
||||
hp: 45, attack: 49, defense: 49,
|
||||
spAtk: 65, spDef: 65, speed: 45,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('mapGenderRatio', () => {
|
||||
test('returns -1 for genderless', () => {
|
||||
expect(mapGenderRatio(undefined)).toBe(-1)
|
||||
expect(mapGenderRatio('N')).toBe(-1)
|
||||
})
|
||||
|
||||
test('calculates female ratio', () => {
|
||||
expect(mapGenderRatio({ M: 0.875, F: 0.125 })).toBe(1) // 12.5% F → 1
|
||||
expect(mapGenderRatio({ M: 0.5, F: 0.5 })).toBe(4) // 50% F → 4
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPrimaryAbility', () => {
|
||||
test('returns first ability', () => {
|
||||
expect(getPrimaryAbility({ '0': 'Overgrow', '1': 'Chlorophyll' })).toBe('overgrow')
|
||||
})
|
||||
|
||||
test('returns empty string for missing ability', () => {
|
||||
expect(getPrimaryAbility({})).toBe('')
|
||||
})
|
||||
})
|
||||
69
packages/pokemon/src/__tests__/species.test.ts
Normal file
69
packages/pokemon/src/__tests__/species.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { getSpeciesData, getAllSpeciesData, DEX_TO_SPECIES } from '../data/species'
|
||||
import { ALL_SPECIES_IDS } from '../types'
|
||||
import type { SpeciesId } from '../types'
|
||||
|
||||
describe('getSpeciesData', () => {
|
||||
test('returns valid data for charmander', () => {
|
||||
const data = getSpeciesData('charmander')
|
||||
expect(data.id).toBe('charmander')
|
||||
expect(data.name).toBe('Charmander')
|
||||
expect(data.dexNumber).toBe(4)
|
||||
expect(data.growthRate).toBe('medium-slow')
|
||||
expect(data.captureRate).toBe(45)
|
||||
expect(data.flavorText).toBeTruthy()
|
||||
})
|
||||
|
||||
test('returns valid data for pikachu', () => {
|
||||
const data = getSpeciesData('pikachu')
|
||||
expect(data.id).toBe('pikachu')
|
||||
expect(data.dexNumber).toBe(25)
|
||||
expect(data.growthRate).toBe('medium-fast')
|
||||
})
|
||||
|
||||
test('has baseStats with all 6 stats', () => {
|
||||
const data = getSpeciesData('bulbasaur')
|
||||
expect(data.baseStats).toHaveProperty('hp')
|
||||
expect(data.baseStats).toHaveProperty('attack')
|
||||
expect(data.baseStats).toHaveProperty('defense')
|
||||
expect(data.baseStats).toHaveProperty('spAtk')
|
||||
expect(data.baseStats).toHaveProperty('spDef')
|
||||
expect(data.baseStats).toHaveProperty('speed')
|
||||
})
|
||||
|
||||
test('has types array', () => {
|
||||
const data = getSpeciesData('squirtle')
|
||||
expect(data.types.length).toBeGreaterThan(0)
|
||||
expect(data.types[0]).toBe('water')
|
||||
})
|
||||
|
||||
test('has evolutionChain for species with evolutions', () => {
|
||||
const data = getSpeciesData('charmander')
|
||||
expect(data.evolutionChain).toBeDefined()
|
||||
expect(data.evolutionChain?.[0]?.into).toBe('charmeleon')
|
||||
})
|
||||
|
||||
test('has no evolutionChain for final evolutions', () => {
|
||||
const data = getSpeciesData('charizard')
|
||||
expect(data.evolutionChain).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAllSpeciesData', () => {
|
||||
test('returns data for all species', () => {
|
||||
const all = getAllSpeciesData()
|
||||
for (const id of ALL_SPECIES_IDS) {
|
||||
expect(all[id]).toBeDefined()
|
||||
expect(all[id]!.id).toBe(id)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('DEX_TO_SPECIES', () => {
|
||||
test('maps dex numbers correctly', () => {
|
||||
expect(DEX_TO_SPECIES[1]).toBe('bulbasaur')
|
||||
expect(DEX_TO_SPECIES[4]).toBe('charmander')
|
||||
expect(DEX_TO_SPECIES[7]).toBe('squirtle')
|
||||
expect(DEX_TO_SPECIES[25]).toBe('pikachu')
|
||||
})
|
||||
})
|
||||
64
packages/pokemon/src/__tests__/xpTable.test.ts
Normal file
64
packages/pokemon/src/__tests__/xpTable.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { xpForLevel, levelFromXp, xpToNextLevel } from '../data/xpTable'
|
||||
|
||||
describe('xpForLevel', () => {
|
||||
test('returns 0 for level 1', () => {
|
||||
expect(xpForLevel(1, 'medium-fast')).toBe(0)
|
||||
})
|
||||
|
||||
test('returns 0 for level 0', () => {
|
||||
expect(xpForLevel(0, 'medium-fast')).toBe(0)
|
||||
})
|
||||
|
||||
test('medium-fast: level 5 = 125 XP', () => {
|
||||
expect(xpForLevel(5, 'medium-fast')).toBe(125)
|
||||
})
|
||||
|
||||
test('medium-fast: level 10 = 1000 XP', () => {
|
||||
expect(xpForLevel(10, 'medium-fast')).toBe(1000)
|
||||
})
|
||||
|
||||
test('slow: level 5 = 156 XP', () => {
|
||||
expect(xpForLevel(5, 'slow')).toBe(156)
|
||||
})
|
||||
|
||||
test('fast: level 5 = 100 XP', () => {
|
||||
expect(xpForLevel(5, 'fast')).toBe(100)
|
||||
})
|
||||
})
|
||||
|
||||
describe('levelFromXp', () => {
|
||||
test('returns 1 for 0 XP', () => {
|
||||
expect(levelFromXp(0, 'medium-fast')).toBe(1)
|
||||
})
|
||||
|
||||
test('returns 5 for 125 XP medium-fast', () => {
|
||||
expect(levelFromXp(125, 'medium-fast')).toBe(5)
|
||||
})
|
||||
|
||||
test('caps at 100', () => {
|
||||
expect(levelFromXp(999999999, 'medium-fast')).toBe(100)
|
||||
})
|
||||
|
||||
test('roundtrip: xpForLevel then levelFromXp', () => {
|
||||
for (let lv = 1; lv <= 100; lv += 10) {
|
||||
const xp = xpForLevel(lv, 'medium-fast')
|
||||
expect(levelFromXp(xp, 'medium-fast')).toBe(lv)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('xpToNextLevel', () => {
|
||||
test('returns 0 at level 100', () => {
|
||||
expect(xpToNextLevel(100, 0, 'medium-fast')).toBe(0)
|
||||
})
|
||||
|
||||
test('returns difference to next level', () => {
|
||||
// Level 5 medium-fast: xpForLevel(5)=125, xpForLevel(6)=216
|
||||
expect(xpToNextLevel(5, 125, 'medium-fast')).toBe(216 - 125)
|
||||
})
|
||||
|
||||
test('returns full next level XP from 0', () => {
|
||||
expect(xpToNextLevel(1, 0, 'medium-fast')).toBe(8) // 2^3=8
|
||||
})
|
||||
})
|
||||
@@ -3,7 +3,7 @@ import { Dex } from '@pkmn/sim'
|
||||
import type { Creature, SpeciesId } from '../types'
|
||||
import { TO_DEX_STAT, FROM_DEX_STAT } from '../data/pkmn'
|
||||
import { STAT_NAMES } from '../types'
|
||||
import type { BattleState, BattlePokemon, BattleEvent, BattleResult, PlayerAction, MoveOption, StatusCondition } from './types'
|
||||
import type { BattleState, BattlePokemon, BattleEvent, PlayerAction, StatusCondition } from './types'
|
||||
import { chooseAIMove } from './ai'
|
||||
|
||||
// ─── Adapter: Creature → Showdown Set ───
|
||||
@@ -45,13 +45,23 @@ function wildPokemonToSetString(speciesId: SpeciesId, level: number): string {
|
||||
return [species.name, `Level: ${level}`, `Ability: ${ability}`, ...moves.map(m => `- ${m}`)].join('\n')
|
||||
}
|
||||
|
||||
function getSpeciesMoves(speciesId: string, level: number): string[] {
|
||||
// Use @pkmn/sim move pool - get natural level-up moves
|
||||
// This is a simplified approach for the sim
|
||||
function getSpeciesMoves(speciesId: string, _level: number): string[] {
|
||||
// In @pkmn/sim, Dex.species doesn't expose learnsets directly.
|
||||
// Use common moves that exist in the sim's data for basic battles.
|
||||
// The actual move pool is resolved by the Battle engine during construction.
|
||||
const species = Dex.species.get(speciesId)
|
||||
if (!species) return ['Tackle']
|
||||
// For sim battles, just return basic moves
|
||||
return ['Tackle', 'Splash']
|
||||
// Use type-appropriate basic moves as fallback
|
||||
const type = species.types[0]?.toLowerCase() ?? 'normal'
|
||||
const basicMoves: Record<string, string[]> = {
|
||||
normal: ['Tackle', 'Scratch'],
|
||||
fire: ['Ember', 'FireSpin'],
|
||||
water: ['WaterGun', 'Bubble'],
|
||||
grass: ['VineWhip', 'RazorLeaf'],
|
||||
electric: ['ThunderShock', 'Spark'],
|
||||
poison: ['PoisonSting', 'Smog'],
|
||||
}
|
||||
return basicMoves[type] ?? ['Tackle', 'Scratch']
|
||||
}
|
||||
|
||||
// ─── State Projection ───
|
||||
|
||||
Reference in New Issue
Block a user