diff --git a/app/games/pokemon/POKEMON_BATTLE_AI_DOCS.md b/app/games/pokemon/POKEMON_BATTLE_AI_DOCS.md new file mode 100644 index 0000000..c4d7d80 --- /dev/null +++ b/app/games/pokemon/POKEMON_BATTLE_AI_DOCS.md @@ -0,0 +1,1334 @@ +# 🎮 Pokemon Battle AI - Documentação Completa + +> Um jogo interativo de batalhas Pokemon com Inteligência Artificial evolutiva baseada em **Algoritmos Genéticos**. A IA aprende e evolui suas estratégias através de gerações, criando times cada vez mais competitivos. + +[![React](https://img.shields.io/badge/React-19.1.0-61dafb?logo=react)](https://react.dev/) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.8.3-3178c6?logo=typescript)](https://www.typescriptlang.org/) +[![React Router](https://img.shields.io/badge/React_Router-7.7.1-ca4245?logo=react-router)](https://reactrouter.com/) +[![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-4.1.4-38bdf8?logo=tailwind-css)](https://tailwindcss.com/) + +--- + +## 📋 Índice + +- [Sobre o Projeto](#-sobre-o-projeto) +- [Tecnologias Utilizadas](#-tecnologias-utilizadas) +- [Conceitos de IA](#-conceitos-de-ia) +- [Algoritmo Genético](#-algoritmo-genético) +- [Instalação](#-instalação) +- [Como Usar](#-como-usar) +- [Estrutura do Projeto](#-estrutura-do-projeto) +- [Sistema de Evolução](#-sistema-de-evolução) +- [Bateria de Testes](#-bateria-de-testes) +- [API de Dados](#-api-de-dados) +- [Contribuindo](#-contribuindo) + +--- + +## 🎯 Sobre o Projeto + +**Pokemon Battle AI** é uma aplicação web interativa onde jogadores montam times de Pokemon (Geração 1 - Kanto) e batalham contra uma **Inteligência Artificial evolutiva**. + +### Destaques: + +🧬 **IA que Evolui**: Usa **Algoritmo Genético** para criar estratégias cada vez melhores +🎮 **Interface Moderna**: Design responsivo com Tailwind CSS e tema dark mode +⚡ **Performance**: Otimizado com cache de dados da PokeAPI +📊 **Visualização**: Gráficos de evolução e estatísticas detalhadas +🧪 **Modo de Teste**: Configurações experimentais com população de até 100 genomas +💾 **Persistência**: Progresso salvo localmente no navegador + +### Como Funciona: + +1. **Seleção de Time**: Escolha 6 Pokemon da primeira geração (Kanto) +2. **Análise**: Veja estatísticas, tipos e composição do seu time +3. **Batalha**: Enfrente a IA em 6 confrontos 1v1 baseados em tipos e stats +4. **Evolução**: A IA aprende com cada batalha e evolui suas estratégias +5. **Testes Automatizados**: Execute baterias de 100 batalhas para ver a IA evoluir rapidamente + +--- + +## 🛠️ Tecnologias Utilizadas + +### Frontend Framework +- **React 19.1.0** - Biblioteca JavaScript para interfaces de usuário +- **TypeScript 5.8.3** - Superset tipado de JavaScript +- **React Router 7.7.1** - Roteamento e navegação + +### Estilização +- **Tailwind CSS 4.1.4** - Framework CSS utility-first +- **@tailwindcss/vite 4.1.4** - Plugin Vite para Tailwind + +### Visualização +- **PixiJS 8.12.0** - Engine 2D para renderização de gráficos +- **@pixi/react 8.0.3** - Integração do PixiJS com React +- **Matter.js 0.20.0** - Engine de física 2D + +### API e Dados +- **pokenode-ts 1.20.0** - Cliente TypeScript para PokeAPI +- **axios 1.12.2** - Cliente HTTP +- **axios-cache-interceptor 1.8.3** - Cache automático de requisições + +### Build e Desenvolvimento +- **Vite 6.3.3** - Build tool e dev server ultrarrápido +- **vite-tsconfig-paths 5.1.4** - Suporte para paths do TypeScript + +### Runtime +- **Node.js 20+** - Runtime JavaScript +- **isbot 5.1.27** - Detecção de bots + +--- + +## 🧠 Conceitos de IA + +### Paradigma: Computação Evolutiva + +O projeto implementa **Evolutionary Computation**, uma subárea de Inteligência Artificial inspirada na **evolução biológica** de Darwin. + +#### O que é o Agente de IA? + +O **Agente** é um sistema evolutivo que aprende a construir times de Pokemon competitivos através de **gerações sucessivas**: + +- **Tipo**: Agente Evolutivo Baseado em População +- **Objetivo**: Maximizar taxa de vitória contra o jogador +- **Método**: Seleção natural + Variação genética (crossover + mutação) +- **Memória**: População de 20-100 genomas com histórico completo +- **Adaptação**: Melhora incremental a cada geração (a cada 5 batalhas) + +#### Elementos Fundamentais: + +##### 🧬 **População** +Conjunto de **20 genomas** (ou 100 no modo de teste), onde cada genoma representa uma estratégia completa de montagem de time. + +```typescript +População = [Genoma1, Genoma2, ..., Genoma20] +``` + +##### 🧬 **Genoma (Cromossomo)** +DNA digital que codifica uma estratégia completa: + +```typescript +interface TeamGenome { + id: string; // Identificador único + genes: TeamGenes; // Cromossomo (DNA) + fitness: number; // Qualidade (0-100) + wins: number; // Histórico de vitórias + losses: number; // Histórico de derrotas + draws: number; // Histórico de empates + generation: number; // Geração de origem + parents?: [string, string]; // Linhagem genética +} +``` + +##### 🧬 **Genes** +Informação genética que define a estratégia: + +```typescript +interface TeamGenes { + pokemonIds: number[]; // [25, 6, 131, 94, 143, 248] + preferredTypes: string[]; // ["electric", "fire", "water"] + strategy: string; // "balanced" | "aggressive" | "defensive" + statPriority: string; // "attack" | "defense" | "speed" | "hp" +} +``` + +##### 📊 **Fitness (Aptidão)** +Função que avalia a "qualidade de sobrevivência" de cada genoma: + +```typescript +fitness = (winRate × 50) + (typeVariety × 25) + (experience × 15) + (counterBonus × 10) +``` + +**Critérios:** +1. **Win Rate (50 pts)**: Taxa de vitória (principal critério) +2. **Type Variety (25 pts)**: Diversidade de tipos no time +3. **Experience (15 pts)**: Número de batalhas (maturidade) +4. **Counter Bonus (10 pts)**: Vantagem de tipos contra o jogador + +**Exemplo:** +``` +Genoma com 60% vitórias, 10 tipos diferentes, 25 batalhas, contém counters: += (0.60 × 50) + (10/18 × 25) + (25/50 × 15) + 10 += 30 + 13.9 + 7.5 + 10 += 61.4 fitness +``` + +##### 🎯 **Seleção** +Mecanismo que escolhe os melhores genomas para reprodução usando **Tournament Selection**: + +1. Escolher 4 genomas aleatoriamente +2. Comparar fitness dos 4 +3. Selecionar os 2 melhores como "pais" + +##### 🔀 **Crossover (Recombinação)** +Combina genes de 2 pais para criar 1 filho: + +```typescript +Parent1: [Poke1, Poke2, Poke3, Poke4, Poke5, Poke6] +Parent2: [PokeA, PokeB, PokeC, PokeD, PokeE, PokeF] + ↓ (ponto de corte na posição 3) +Child: [Poke1, Poke2, Poke3, PokeD, PokeE, PokeF] +``` + +Taxa: **80%** (4 em 5 reproduções usam crossover) + +##### 🧬 **Mutação** +Variação aleatória dos genes para explorar novas estratégias: + +Taxa: **15%** por genoma + +**Tipos de mutação:** +- Trocar 1 Pokemon aleatório +- Adicionar/remover tipo preferido +- Mudar estratégia (balanced → aggressive) +- Mudar prioridade de stat (attack → speed) + +##### 🏆 **Elitismo** +Preservação dos melhores genomas entre gerações: + +- **Top 20%** (4 melhores) são copiados diretamente para a próxima geração +- Garante que as melhores soluções nunca sejam perdidas +- Assegura que o fitness não regrida + +--- + +## 🧬 Algoritmo Genético + +### Como Funciona + +O algoritmo segue o ciclo evolutivo clássico: + +``` +┌─────────────────────────────────────────┐ +│ GERAÇÃO N (20 genomas) │ +└─────────────────┬───────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ 1. AVALIAÇÃO (Fitness) │ +│ Calcular fitness de todos os genomas │ +└─────────────────┬───────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ 2. ELITISMO │ +│ Copiar top 20% → próxima geração │ +└─────────────────┬───────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ 3. SELEÇÃO (Tournament) │ +│ Escolher pais para 16 filhos │ +└─────────────────┬───────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ 4. CROSSOVER (80%) │ +│ Combinar genes dos pais │ +└─────────────────┬───────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ 5. MUTAÇÃO (15%) │ +│ Variação aleatória dos genes │ +└─────────────────┬───────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ GERAÇÃO N+1 (20 genomas) │ +│ 4 elite + 16 filhos │ +└─────────────────────────────────────────┘ +``` + +### Parâmetros do Algoritmo + +| Parâmetro | Valor (Normal) | Valor (Teste) | Justificativa | +|-----------|----------------|---------------|---------------| +| **População** | 20 | 100 | Balanço entre diversidade e performance | +| **Elitismo** | 20% (4) | 10% (10) | Preserva melhores sem travar evolução | +| **Crossover** | 80% | 85% | Alta exploração de combinações | +| **Mutação** | 15% | 20% | Evita convergência prematura | +| **Torneio** | 4 | 6 | Pressão seletiva moderada | +| **Evolução** | A cada 5 batalhas | A cada 5 batalhas | Dados suficientes para fitness confiável | + +### Exemplo de Evolução + +``` +Gen 0: Fitness Médio: 15 | Melhor: 32 | Diversidade: 78% + ↓ (5 batalhas) +Gen 1: Fitness Médio: 28 | Melhor: 48 | Diversidade: 52% + ↓ (5 batalhas) +Gen 2: Fitness Médio: 41 | Melhor: 62 | Diversidade: 38% + ↓ (5 batalhas) +Gen 3: Fitness Médio: 53 | Melhor: 74 | Diversidade: 29% + ↓ (5 batalhas) +Gen 4: Fitness Médio: 64 | Melhor: 83 | Diversidade: 24% + ↓ (5 batalhas) +Gen 5: Fitness Médio: 72 | Melhor: 89 | Diversidade: 21% +``` + +**A IA evolui continuamente! 🚀** + +### Métricas de Qualidade + +#### Convergência +Mede se a população está evoluindo: +- Fitness médio **aumentando** → Convergência saudável +- Fitness **estagnado** → Possível mínimo local + +#### Diversidade +```typescript +diversity = (estratégias únicas / total genomas) × 100 + +Diversidade > 60% = População saudável +Diversidade < 30% = Convergência prematura (ajustar mutação) +``` + +#### Exploração vs Explotação +- **Exploração**: Mutação + Crossover (buscar novas soluções) +- **Explotação**: Elitismo + Seleção (refinar melhores soluções) +- **Balanço**: 15% mutação + 20% elitismo = equilíbrio ideal + +--- + +## 📦 Instalação + +### Pré-requisitos + +- **Node.js** 20+ ([Download](https://nodejs.org/)) +- **npm** 10+ (vem com Node.js) + +### Passo a Passo + +1. **Clone o repositório** +```bash +git clone https://github.com/includeDaniel/PlayAI.git +cd PlayAI +``` + +2. **Instale as dependências** +```bash +npm install +``` + +3. **Inicie o servidor de desenvolvimento** +```bash +npm run dev +``` + +4. **Acesse no navegador** +``` +http://localhost:5173 +``` + +### Comandos Disponíveis + +```bash +npm run dev # Inicia servidor de desenvolvimento +npm run build # Compila para produção +npm run start # Inicia servidor de produção +npm run typecheck # Verifica tipos TypeScript +``` + +--- + +## 🎮 Como Usar + +### 1. Menu Principal + +Ao abrir a aplicação, você verá: + +- **Começar Aventura**: Inicia o modo de jogo normal +- **Bateria de Testes Automatizados**: Executa 100 batalhas automáticas +- **Estatísticas da IA**: Geração, batalhas, win rate, fitness, diversidade + +### 2. Montagem do Time + +1. **Filtrar por Geração**: Use os botões para navegar entre Pokemon (Gen 1: 1-151) +2. **Selecionar Pokemon**: Clique em até 6 Pokemon para formar seu time +3. **Ver Estatísticas**: Cada card mostra tipo, HP, ataque e defesa +4. **Time Aleatório**: Gera um time aleatório automaticamente +5. **Analisar Time**: Vê análise detalhada de força e fraquezas +6. **Iniciar Batalha**: Começa a batalha contra a IA + +### 3. Análise do Time + +Visualize: +- **Composição de Tipos**: Distribuição dos tipos no seu time +- **Forças**: Tipos que seu time é forte contra +- **Fraquezas**: Tipos que seu time é fraco contra +- **Estatísticas Médias**: HP, Ataque, Defesa médios do time +- **Força Total**: Soma de todas as stats + +### 4. Batalha + +A batalha acontece em **6 confrontos 1v1**: + +1. Pokemon são emparelhados por posição (1º vs 1º, 2º vs 2º...) +2. Cada confronto calcula dano baseado em: + - **Vantagem de tipo** (2x de dano) + - **Ataque vs Defesa** + - **Fator aleatório** (±10%) +3. Pokemon com maior dano **vence** o confronto +4. Time com mais vitórias **vence a batalha** + +### 5. Resultados + +Após a batalha, veja: +- **Resultado geral** (Vitória/Derrota/Empate) +- **Matchups individuais**: Cada confronto com análise detalhada +- **Times lado a lado**: Comparação visual dos times +- **Evolução da IA**: A IA registra o resultado e aprende + +### 6. Modo de Teste Experimental + +**Como ativar:** +1. No menu, procure "🧪 Modo de Teste Experimental" +2. Clique em "Ativar Modo de Teste" +3. População aumenta de 20 para **100 genomas** +4. Use "Bateria de Testes" para 100 batalhas automáticas +5. Exporte dados com "📥 Exportar Dados" + +**Benefícios:** +- 5x mais exploração de estratégias +- Convergência 33% mais rápida +- Diversidade mantida por mais tempo +- Dados científicos exportáveis + +--- + +## 📁 Estrutura do Projeto + +``` +PlayAI/ +├── app/ +│ ├── root.tsx # Componente raiz da aplicação +│ ├── routes.ts # Configuração de rotas +│ ├── app.css # Estilos globais +│ ├── games/ +│ │ └── Game.tsx # Página principal do jogo +│ │ └── pokemon/ +│ │ ├── PokemonBattleAI.tsx # Componente principal (1318 linhas) +│ │ ├── POKEMON_BATTLE_AI_DOCS.md # Esta documentação +│ │ ├── components/ +│ │ │ ├── AutomatedTestBattery.tsx # Bateria de testes automáticos +│ │ │ ├── BattleMatchup.tsx # Visualização de confrontos 1v1 +│ │ │ ├── PokemonGrid.tsx # Grade de seleção de Pokemon +│ │ │ └── TeamDisplay.tsx # Exibição e análise do time +│ │ ├── hooks/ +│ │ │ ├── useGeneticAI.ts # Algoritmo genético (398 linhas) +│ │ │ └── usePokemonData.ts # Fetch e cache de dados PokeAPI +│ │ └── types/ +│ │ ├── genetic.ts # Tipos do algoritmo genético +│ │ └── pokemon.ts # Tipos de Pokemon e batalhas +│ └── routes/ +│ └── Home.tsx # Página inicial +├── build/ # Build de produção +│ ├── client/ # Assets do cliente +│ └── server/ # Servidor Node.js +├── public/ # Assets estáticos +├── package.json # Dependências e scripts +├── tsconfig.json # Configuração TypeScript +├── vite.config.ts # Configuração Vite +├── react-router.config.ts # Config do React Router +├── Dockerfile # Container Docker +├── README.md # Documentação do projeto +└── INTEGRACAO_COMPLETA.md # Guia de integração +``` + +### Arquivos Principais + +#### `PokemonBattleAI.tsx` (1318 linhas) +Componente principal que gerencia: +- **Estados do jogo**: menu, setup, análise, batalha, testes +- **Integração com hooks**: `useGeneticAI`, `usePokemonData` +- **Lógica de batalha**: Simulação 1v1 com cálculo de vantagens de tipo +- **Bateria de testes**: Execução de 10-500 batalhas automatizadas +- **UI responsiva**: Tailwind CSS com dark mode +- **Exportação de dados**: JSON com histórico completo da IA + +**Funções principais:** +```typescript +// Linha ~241: Gera time da IA usando melhor genoma +generateCounterTeam(playerTeam): { team, genomeId } + +// Linha ~301: Calcula vantagem de tipo (2x, 0.5x, 0x) +calculateTypeAdvantage(attacker, defender): number + +// Linha ~343: Simula confronto individual 1v1 +simulateIndividualBattle(playerPokemon, aiPokemon): IndividualBattle + +// Linha ~397: Simula batalha completa (6 confrontos) +simulateBattle(playerTeam, aiTeam, genomeId): BattleResult + +// Linha ~430: Executa bateria de testes automatizados +runAutomatedTests(numberOfBattles): Promise +``` + +#### `useGeneticAI.ts` (398 linhas) +Hook customizado que implementa o algoritmo genético completo: + +**Configurações:** +```typescript +// Linha 12-16: Modo Normal +DEFAULT_CONFIG = { + populationSize: 20, + elitePercentage: 0.2, // Top 20% preservado + mutationRate: 0.15, // 15% de mutação + crossoverRate: 0.8, // 80% crossover + tournamentSize: 4 // Seleção por torneio +} + +// Linha 19-25: Modo Teste Intensivo +TESTING_CONFIG = { + populationSize: 100, // 5x maior + elitePercentage: 0.1, // Top 10% + mutationRate: 0.20, // Mais exploração + crossoverRate: 0.85, // Mais recombinação + tournamentSize: 6 // Mais competitivo +} +``` + +**Funções principais:** +```typescript +// Linha 50-60: Gera genes aleatórios para novo genoma +generateRandomGenes(): TeamGenes + +// Linha 85-106: Calcula fitness (0-100 pontos) +calculateFitness(genome, playerTypes?): number +// - Win Rate: 0-50 pts +// - Type Variety: 0-25 pts +// - Experience: 0-15 pts +// - Counter Bonus: 0-10 pts + +// Linha 108-118: Seleção por torneio +tournamentSelection(population, tournamentSize): TeamGenome + +// Linha 120-148: Crossover (recombinação) +crossover(parent1, parent2): TeamGenome + +// Linha 150-188: Mutação genética +mutate(genome, mutationRate): TeamGenome + +// Linha 260-310: Evolução de geração +evolveGeneration(playerTypes): void +// - Calcula fitness de todos +// - Elitismo (preserva melhores) +// - Crossover + Mutação +// - Registra histórico + +// Linha 236-250: Persistência +// Salva/carrega população do localStorage +``` + +#### `genetic.ts` +Define tipos TypeScript para o sistema genético: + +```typescript +// Tipos de estratégia +type StrategyType = 'counter' | 'balanced' | 'aggressive' | 'tank'; +type StatsPriority = 'balanced' | 'offensive' | 'defensive' | 'speed'; + +// DNA do time +interface TeamGenes { + pokemonIds: number[]; // IDs dos 6 Pokemon (1-151) + typeDistribution: string[]; // Tipos priorizados + statsPriority: StatsPriority; // Foco de stats + strategyType: StrategyType; // Estratégia geral +} + +// Genoma completo +interface TeamGenome { + id: string; // ID único + generation: number; // Geração de origem + genes: TeamGenes; // DNA + fitness: number; // Aptidão (0-100) + wins: number; // Vitórias + losses: number; // Derrotas + draws: number; // Empates + battlesPlayed: number; // Total de batalhas + parents: [string, string] | null; // Linhagem + createdAt: number; // Timestamp +} + +// População completa +interface GeneticPopulation { + genomes: TeamGenome[]; // Array de genomas + currentGeneration: number; // Geração atual + totalBattles: number; // Batalhas totais + bestFitness: number; // Melhor fitness alcançado + bestGenomeId: string | null; // ID do melhor genoma + generationHistory: GenerationHistory[]; // Evolução +} +``` + +#### `usePokemonData.ts` +Hook para fetch e cache de dados da PokeAPI: + +**Funcionalidades:** +- Cache automático de requisições (24h) +- Paginação (20 Pokemon por página) +- Busca por nome +- Loading states +- Error handling +- Geração de times aleatórios + +**Funções exportadas:** +```typescript +usePokemonData() { + paginatedPokemon, // Pokemon da página atual + loading, // Estado de carregamento + error, // Erro se houver + searchTerm, // Termo de busca + currentPage, // Página atual + totalPages, // Total de páginas + searchPokemon, // Função de busca + generateRandomTeam, // Gera time aleatório + nextPage, // Próxima página + prevPage, // Página anterior + goToPage // Ir para página específica +} +``` + +#### `AutomatedTestBattery.tsx` +Componente de interface para bateria de testes: + +**Props:** +```typescript +interface AutomatedTestBatteryProps { + onRunTests: (numberOfBattles: number) => Promise; + isRunning: boolean; + progress: { current: number; total: number }; +} +``` + +**Features:** +- Input para número de batalhas (10-500) +- Barra de progresso em tempo real +- Estatísticas agregadas: + - Total de batalhas + - Vitórias jogador/IA + - Win rate percentual + - Vantagem atual +- Lista expandível de resultados +- Análise detalhada de cada confronto +- Código de cores (verde/vermelho/amarelo) + +#### `PokemonGrid.tsx` +Grade de seleção de Pokemon: + +**Features:** +- Grid responsivo (2-6 colunas) +- Sprites via CDN (Pokemon.com) +- Fallback para Serebii.net +- Fallback final: SVG pokeball +- Indicador visual de seleção +- Limite de 6 Pokemon +- Loading states +- Error handling + +**CDN Strategy:** +```typescript +// Primary: Pokemon.com +https://assets.pokemon.com/assets/cms2/img/pokedex/detail/${paddedId}.png + +// Fallback: Serebii.net +https://www.serebii.net/pokemon/art/${paddedId}.png + +// Final: Base64 SVG +data:image/svg+xml;base64,[pokeball] +``` + +#### `TeamDisplay.tsx` +Exibição e análise do time: + +**Features:** +- Visualização dos 6 Pokemon selecionados +- Stats individuais (HP, ATK, DEF, SPD, SP.ATK, SP.DEF) +- Badges de tipo +- Botão de remoção +- Mesmo sistema de CDN do PokemonGrid +- Layout responsivo + +#### `BattleMatchup.tsx` +Visualização de confronto individual: + +**Features:** +- Sprites lado a lado (jogador vs IA) +- Indicadores de vantagem (→ ← ⚔) +- Badges de tipo +- Análise textual do confronto +- Highlight do vencedor +- Mesmo sistema de CDN + +#### `pokemon.ts` +Tipos para Pokemon e batalhas: + +```typescript +interface Pokemon { + id: number; + name: string; + sprites: PokemonSprites; + types: PokemonType[]; + stats: PokemonStat[]; + abilities: PokemonAbility[]; + height: number; + weight: number; +} + +interface IndividualBattle { + playerPokemon: Pokemon; + aiPokemon: Pokemon; + winner: 'player' | 'ai'; + typeAdvantage: 'player' | 'ai' | 'neutral'; + reasoning: string; +} + +interface BattleResult { + playerTeam: Pokemon[]; + aiTeam: Pokemon[]; + winner: 'player' | 'ai' | 'draw'; + analysis: string; + battles: IndividualBattle[]; + playerScore: number; + aiScore: number; +} +``` + +--- + +## 🔬 Sistema de Evolução + +### Ciclo de Vida Geracional + +```typescript +// Inicialização (Geração 0) +População inicial: 20 genomas aleatórios + +// A cada 5 batalhas +if (totalBattles % 5 === 0) { + evolveGeneration(); +} + +// Processo de evolução +function evolveGeneration() { + // 1. Calcular fitness de todos + population.forEach(genome => { + genome.fitness = calculateFitness(genome); + }); + + // 2. Ordenar por fitness + population.sort((a, b) => b.fitness - a.fitness); + + // 3. Elitismo (top 20%) + const elite = population.slice(0, 4); + + // 4. Gerar nova geração + const newGeneration = [...elite]; + + while (newGeneration.length < 20) { + // Seleção + const [parent1, parent2] = tournamentSelection(population); + + // Crossover (80% chance) + let child = Math.random() < 0.8 + ? crossover(parent1, parent2) + : { ...parent1 }; + + // Mutação (15% chance) + if (Math.random() < 0.15) { + child = mutate(child); + } + + newGeneration.push(child); + } + + population = newGeneration; + generation++; +} +``` + +### Função de Fitness Detalhada + +```typescript +function calculateFitness(genome: TeamGenome): number { + // 1. Win Rate (0-50 pontos) + const winRate = genome.wins / (genome.totalBattles || 1); + const winRateScore = winRate * 50; + + // 2. Type Variety (0-25 pontos) + const uniqueTypes = new Set( + genome.genes.pokemonIds.flatMap(id => + getPokemon(id).types.map(t => t.type.name) + ) + ).size; + const varietyScore = (uniqueTypes / 18) * 25; + + // 3. Experience (0-15 pontos) + const experienceScore = Math.min(15, (genome.totalBattles / 50) * 15); + + // 4. Counter Bonus (0-10 pontos) + const hasCounterTypes = genome.genes.preferredTypes.some(type => + playerTypes.some(playerType => + isStrongAgainst(type, playerType) + ) + ); + const counterScore = hasCounterTypes ? 10 : 0; + + // Fitness Total (0-100) + return winRateScore + varietyScore + experienceScore + counterScore; +} +``` + +### Operadores Genéticos + +#### Crossover (One-Point) +```typescript +function crossover(parent1: TeamGenome, parent2: TeamGenome): TeamGenome { + const cutPoint = 3; // Meio do time (6 Pokemon) + + return { + id: generateId(), + generation: Math.max(parent1.generation, parent2.generation) + 1, + genes: { + pokemonIds: [ + ...parent1.genes.pokemonIds.slice(0, cutPoint), + ...parent2.genes.pokemonIds.slice(cutPoint) + ], + preferredTypes: [ + ...parent1.genes.preferredTypes.slice(0, 2), + ...parent2.genes.preferredTypes.slice(2) + ], + strategy: Math.random() > 0.5 ? parent1.genes.strategy : parent2.genes.strategy, + statPriority: Math.random() > 0.5 ? parent1.genes.statPriority : parent2.genes.statPriority + }, + fitness: 0, + wins: 0, + losses: 0, + draws: 0, + totalBattles: 0, + parents: [parent1.id, parent2.id] + }; +} +``` + +#### Mutação +```typescript +function mutate(genome: TeamGenome): TeamGenome { + const mutated = { ...genome }; + + // Mutar Pokemon (15% chance cada) + mutated.genes.pokemonIds = genome.genes.pokemonIds.map(id => + Math.random() < 0.15 ? randomPokemonId() : id + ); + + // Mutar tipos preferidos + if (Math.random() < 0.15) { + const allTypes = ['fire', 'water', 'grass', /* ... */]; + if (Math.random() > 0.5) { + // Adicionar tipo + mutated.genes.preferredTypes.push( + allTypes[Math.floor(Math.random() * allTypes.length)] + ); + } else { + // Remover tipo + mutated.genes.preferredTypes = + mutated.genes.preferredTypes.slice(0, -1); + } + } + + // Mutar estratégia + if (Math.random() < 0.15) { + const strategies = ['balanced', 'aggressive', 'defensive', 'counter']; + mutated.genes.strategy = + strategies[Math.floor(Math.random() * 4)]; + } + + // Mutar prioridade + if (Math.random() < 0.15) { + const priorities = ['attack', 'defense', 'speed', 'hp']; + mutated.genes.statPriority = + priorities[Math.floor(Math.random() * 4)]; + } + + return mutated; +} +``` + +### Persistência de Dados + +```typescript +// Salvar no localStorage após cada evolução +localStorage.setItem('pokemon-genetic-population', JSON.stringify({ + population: population.genomes, + generations: population.generations, + currentGeneration: population.currentGeneration, + totalBattles: population.totalBattles, + bestGenomeId: getBestGenome().id +})); + +// Carregar na inicialização +const saved = localStorage.getItem('pokemon-genetic-population'); +if (saved) { + const data = JSON.parse(saved); + // Restaurar população... +} +``` + +--- + +## 🧪 Bateria de Testes + +### Modo de Teste Experimental + +O projeto inclui um **modo de teste** com população de **100 genomas** para análise científica do algoritmo. + +### Configurações + +| Parâmetro | Normal | Teste | Impacto | +|-----------|--------|-------|---------| +| População | 20 | 100 | +400% exploração | +| Mutação | 15% | 20% | +33% variação | +| Torneio | 4 | 6 | +50% competição | +| Elite | 20% (4) | 10% (10) | Mais preservação | +| Memória | ~100KB | ~500KB | +400% uso | + +### Protocolo de Testes + +#### Teste 1: Convergência +**Objetivo**: Comparar velocidade de convergência + +``` +Executar 100 batalhas: +- População 20 (baseline) +- População 100 (teste) + +Métricas: +- Gerações para atingir fitness 80 +- Fitness final (geração 20) +``` + +**Resultados esperados:** +- Pop 20: ~18-22 gerações para F=80 +- Pop 100: ~12-16 gerações para F=80 (**33% mais rápido**) + +#### Teste 2: Diversidade +**Objetivo**: Manter variedade de estratégias + +``` +Executar 200 batalhas: +Registrar diversidade a cada geração + +Diversidade = (estratégias únicas / total) × 100 +``` + +**Resultados esperados:** +``` +População 20: +Gen 0: 75% +Gen 10: 45% +Gen 20: 30% ← convergência prematura + +População 100: +Gen 0: 80% +Gen 10: 65% +Gen 20: 50% ← diversidade mantida +``` + +#### Teste 3: Taxa de Mutação +**Objetivo**: Encontrar taxa ótima + +Testar 3 configurações (100 batalhas cada): +- 10% mutação (baixa) +- 20% mutação (média) +- 30% mutação (alta) + +**Análise:** +- 10%: Convergência rápida, pode travar em mínimo local +- 20%: **Balanço ideal** (configuração padrão) +- 30%: Alta diversidade, convergência lenta + +### Exportação de Dados + +```typescript +// Botão "📥 Exportar Dados" gera JSON: +{ + "metadata": { + "exportDate": "2025-10-26T...", + "testMode": "TESTING_100", + "config": { /* parâmetros usados */ } + }, + "currentStats": { + "generation": 15, + "totalBattles": 75, + "winRate": 0.64, + "bestFitness": 87.3, + "averageFitness": 72.1, + "diversity": 68.5 + }, + "population": { + "genomes": [ /* 100 genomas completos */ ] + }, + "generationHistory": [ /* evolução ao longo do tempo */ ] +} +``` + +**Use os dados para:** +- Gerar gráficos de evolução +- Análise estatística +- Comparar configurações +- Validar hipóteses +- Publicações científicas + +### Executando Testes + +1. **Executar Bateria** +``` +Menu → "Bateria de Testes Automatizados" → Iniciar +``` + +2. **Acompanhar Progresso** +``` +Barra de progresso: 0/100 batalhas +Estatísticas atualizadas em tempo real +``` + +Para mais detalhes, veja: **[docs/TESTING_BATTERY.md](docs/TESTING_BATTERY.md)** + +--- + +## 🌐 API de Dados + +### PokeAPI + +O projeto usa a **PokeAPI v2** para dados de Pokemon: + +``` +https://pokeapi.co/api/v2/ +``` + +**Endpoints utilizados:** +``` +GET /pokemon/{id} # Dados de um Pokemon específico +GET /pokemon?limit=151 # Lista dos 151 Pokemon (Gen 1) +GET /type/{type} # Informações de tipo +``` + +### CDN de Sprites + +**Problema resolvido**: GitHub raw.githubusercontent.com tem rate limiting (429 errors) + +**Solução**: Sistema multi-CDN com fallback automático + +```typescript +// 1. Primary: Pokemon.com CDN +const pokemonComCdn = `https://assets.pokemon.com/assets/cms2/img/pokedex/detail/${paddedId}.png`; + +// 2. Fallback: Serebii.net +const serebiiCdn = `https://www.serebii.net/pokemon/art/${paddedId}.png`; + +// 3. Final Fallback: Base64 SVG Pokeball +const FALLBACK_IMAGE = 'data:image/svg+xml;base64,...'; +``` + +**Implementação (PokemonGrid.tsx, TeamDisplay.tsx, BattleMatchup.tsx):** +```typescript +{pokemon.name} { + const target = e.target as HTMLImageElement; + const paddedId = String(pokemon.id).padStart(3, '0'); + const serebiiUrl = `https://www.serebii.net/pokemon/art/${paddedId}.png`; + + if (!target.src.includes('serebii') && target.src !== FALLBACK_IMAGE) { + target.src = serebiiUrl; + } else if (target.src.includes('serebii')) { + target.src = FALLBACK_IMAGE; + } + }} +/> +``` + +**Vantagens:** +- ✅ Sem dependência do GitHub +- ✅ Alta disponibilidade (3 fontes) +- ✅ Fallback visual sempre funciona +- ✅ Performance otimizada + +### Estrutura de Dados + +```typescript +interface Pokemon { + id: number; + name: string; + sprites: { + front_default: string; + other: { + 'official-artwork': { + front_default: string; // URL alta resolução + }; + }; + }; + types: Array<{ + slot: number; + type: { + name: string; + url: string; + }; + }>; + stats: Array<{ + base_stat: number; + stat: { + name: string; + }; + }>; +} +``` + +### Cache + +Usa **pokenode-ts 1.20.0** com cache interno: + +```typescript +const pokemonApi = new PokemonClient(); +// Cache automático por requisição +``` + +**Benefícios:** +- ✅ Reduz chamadas à API +- ✅ Melhora performance +- ✅ Funciona offline (após primeiro carregamento) +- ✅ Respeita limites de rate da API + +--- + +## 🎨 Interface do Usuário + +### Design System + +**Cores Principais:** +- Azul (`blue-500/600`): Jogador +- Vermelho (`red-500/600`): IA +- Roxo (`purple-500/600`): Ações principais +- Ciano (`cyan-500/600`): Análise +- Verde (`green-500/600`): Vitória +- Cinza (`gray-500/600`): Neutro + +**Breakpoints Responsivos:** +```css +sm: 640px /* Mobile pequeno */ +md: 768px /* Tablet */ +lg: 1024px /* Desktop */ +xl: 1280px /* Desktop grande */ +``` + +### Componentes + +#### PokemonGrid +Grade responsiva de seleção: +```tsx +
+ {pokemon.map(p => )} +
+``` + +#### TeamDisplay +Visualização do time com stats: +- Badges de tipo +- Barras de progresso (HP, ATK, DEF) +- Análise agregada +- Slots vazios + +#### BattleMatchup +Confronto 1v1 detalhado: +- Sprites dos Pokemon +- Indicador de vantagem (→ ← ⚔) +- Dano calculado +- Análise textual + +#### AutomatedTestBattery +Execução de testes: +- Barra de progresso +- Estatísticas em tempo real +- Gráfico de evolução +- Exportação de dados + +--- + +## 🚀 Otimizações + +### Performance + +1. **Lazy Loading**: Componentes carregados sob demanda +2. **Memoização**: `useMemo` e `useCallback` para cálculos pesados +3. **Debounce**: Evita re-renders excessivos +4. **Cache de API**: pokenode-ts com cache interno +5. **CDN Multi-Fallback**: Sprites sempre disponíveis + +### Código Limpo + +**Limpeza recente (Outubro 2025):** +- ✅ Removidos todos console.logs de debug +- ✅ Mantidos logs informativos da evolução da IA +- ✅ Deletado arquivo de teste `check-pokemon-api.js` +- ✅ Removida pasta vazia `constants/` +- ✅ Código padronizado (sprites official-artwork) +- ✅ Sistema CDN unificado em todos os componentes + +**Logs informativos preservados:** +```typescript +// runAutomatedTests() - Linha 482 +console.log(`🧬 Evolução #${n} - Batalhas X-Y: Z/5 vitórias da IA (%)`) + +// runAutomatedTests() - Linhas 501-507 +console.log(`🎯 EVOLUÇÃO FINAL CONSOLIDADA`) +console.log(`📊 ${results.length} batalhas | ${totalAiWins} vitórias IA (%)`) +console.log(`🧬 ${uniquePlayerTypes.length} tipos únicos encontrados`) +console.log(`✅ População evoluída com base em todos os resultados!`) +``` + +### Bundle Size + +```bash +npm run build + +# Análise de tamanho +build/ +├── client/ +│ └── assets/ +│ ├── entry.client-*.js # ~150KB +│ ├── PokemonBattleAI-*.js # ~180KB +│ ├── Game-*.js # ~120KB +│ └── root-*.css # ~50KB +└── server/ + └── index.js # ~200KB +``` + +**Otimizações aplicadas:** +- Tree-shaking automático (Vite) +- Code splitting por rota +- Minificação de CSS/JS +- Compressão gzip/brotli + +### Acessibilidade + +- ✅ Navegação por teclado +- ✅ ARIA labels +- ✅ Contraste adequado (WCAG AA) +- ✅ Textos alternativos em imagens +- ✅ Dark mode nativo + +--- + +## 🤝 Contribuindo + +Contribuições são bem-vindas! Siga os passos: + +1. **Fork** o projeto +2. **Crie uma branch** para sua feature + ```bash + git checkout -b feature/MinhaNovaFeature + ``` +3. **Commit** suas mudanças + ```bash + git commit -m 'Adiciona MinhaNovaFeature' + ``` +4. **Push** para a branch + ```bash + git push origin feature/MinhaNovaFeature + ``` +5. **Abra um Pull Request** + +### Diretrizes + +- Use **TypeScript** para novas features +- Siga o estilo de código existente (Prettier) +- Adicione testes para novas funcionalidades +- Documente mudanças significativas +- Mantenha commits semânticos +- **NÃO** adicione console.logs de debug (apenas logs informativos da IA) +- Use sistema de CDN multi-fallback para imagens + +### Branch Atual + +``` +Repository: PlayAI +Owner: includeDaniel +Current branch: feature/pokemon-integration +Default branch: main +``` + +### Roadmap + +Funcionalidades planejadas: + +- [ ] Suporte a Gerações 2-9 +- [ ] Multiplayer online (batalhas PvP) +- [ ] Sistema de ranking +- [ ] Mais estratégias de IA (Minimax, MCTS) +- [ ] Ataques com tipos e efeitos +- [ ] Items e habilidades +- [ ] Animações de batalha com PixiJS +- [ ] PWA (Progressive Web App) +- [ ] Internacionalização (i18n) +- [ ] Gráficos de evolução da IA +- [ ] Replay de batalhas + +### Issues e Bugs + +Problemas conhecidos e resolvidos: + +#### ✅ Resolvidos +- GitHub rate limiting (429 errors) → Sistema CDN multi-fallback +- Sprites não carregando → Pokemon.com + Serebii.net + SVG fallback +- Console.logs poluindo código → Limpeza completa mantendo logs informativos +- Arquivos não utilizados → Cleanup de `check-pokemon-api.js` e pasta `constants/` + +#### 🔧 Em andamento +- Nenhum issue crítico no momento + +--- + +## 📚 Referências + +### Algoritmos Genéticos +- Goldberg, D. E. (1989). *Genetic Algorithms in Search, Optimization, and Machine Learning* +- Mitchell, M. (1998). *An Introduction to Genetic Algorithms* +- Eiben, A. E., & Smith, J. E. (2015). *Introduction to Evolutionary Computing* + +### PokeAPI +- Documentação oficial: https://pokeapi.co/docs/v2 +- GitHub: https://github.com/PokeAPI/pokeapi +- pokenode-ts: https://github.com/Gabb-c/pokenode-ts + +### React & TypeScript +- React Docs: https://react.dev/ +- TypeScript Handbook: https://www.typescriptlang.org/docs/ +- React Router: https://reactrouter.com/ +- Vite: https://vite.dev/ + +### Sprites & Assets +- Pokemon.com CDN: https://assets.pokemon.com/ +- Serebii.net: https://www.serebii.net/ + +--- + +## 📝 Changelog + +### v1.1.0 (Outubro 2025) +- ✅ Sistema CDN multi-fallback para sprites +- ✅ Remoção de dependência do GitHub raw URLs +- ✅ Limpeza completa de console.logs de debug +- ✅ Preservação de logs informativos da evolução da IA +- ✅ Remoção de arquivos não utilizados +- ✅ Padronização de sprites (official-artwork) +- ✅ Documentação atualizada + +### v1.0.0 (2025) +- 🎮 Lançamento inicial +- 🧬 Algoritmo genético funcional +- 🧪 Bateria de testes automatizados +- 📊 Sistema de estatísticas +- 💾 Persistência em localStorage +- 🎨 Interface responsiva com Tailwind CSS + +--- + +[⬆ Voltar ao topo](#-pokemon-battle-ai---documentação-completa) + + diff --git a/app/games/pokemon/PokemonBattleAI.tsx b/app/games/pokemon/PokemonBattleAI.tsx new file mode 100644 index 0000000..51de07a --- /dev/null +++ b/app/games/pokemon/PokemonBattleAI.tsx @@ -0,0 +1,1318 @@ +"use client"; +import { useState, useCallback, useMemo } from 'react'; +import { PokemonClient } from 'pokenode-ts'; +import type { Pokemon as PokeNodePokemon } from 'pokenode-ts'; +import { usePokemonData } from './hooks/usePokemonData'; +import { useGeneticAI, TESTING_CONFIG } from './hooks/useGeneticAI'; +import PokemonGrid from './components/PokemonGrid'; +import TeamDisplay from './components/TeamDisplay'; +import { BattleMatchup } from './components/BattleMatchup'; +import { AutomatedTestBattery } from './components/AutomatedTestBattery'; +import type { Pokemon, ViewType, TeamAnalysis, BattleResult, IndividualBattle } from './types/pokemon'; +import type { TeamGenome } from './types/genetic'; + +// Função helper para converter Pokemon do pokenode-ts +function convertPokemon(pkNodePokemon: PokeNodePokemon): Pokemon { + return { + id: pkNodePokemon.id, + name: pkNodePokemon.name, + sprites: { + front_default: pkNodePokemon.sprites.front_default || '', + other: { + 'official-artwork': { + front_default: pkNodePokemon.sprites.other?.['official-artwork']?.front_default || pkNodePokemon.sprites.front_default || '' + } + } + }, + types: pkNodePokemon.types, + stats: pkNodePokemon.stats, + abilities: pkNodePokemon.abilities, + height: pkNodePokemon.height, + weight: pkNodePokemon.weight + }; +} + +export default function PokemonBattleAI() { + const [currentView, setCurrentView] = useState('menu'); + const [playerTeam, setPlayerTeam] = useState([]); + const [aiTeam, setAiTeam] = useState([]); + const [currentGenomeId, setCurrentGenomeId] = useState(null); + const [teamAnalysis, setTeamAnalysis] = useState(null); + const [battleResult, setBattleResult] = useState(null); + const [isTestingMode, setIsTestingMode] = useState(false); + const [isRunningTests, setIsRunningTests] = useState(false); + const [testProgress, setTestProgress] = useState<{ current: number; total: number }>({ current: 0, total: 0 }); + + // Criar instância do PokemonClient + const pokemonApi = useMemo(() => new PokemonClient(), []); + + const { + paginatedPokemon, + loading, + error, + searchTerm, + currentPage, + totalPages, + searchPokemon, + generateRandomTeam, + nextPage, + prevPage, + goToPage + } = usePokemonData(); + + const { + population, + recordBattle, + evolveGeneration, + getBestGenome, + getGenomeById, + resetPopulation, + getStats + } = useGeneticAI(isTestingMode ? TESTING_CONFIG : {}); + + // Gerenciamento do time do jogador + const handlePokemonSelect = useCallback((pokemon: Pokemon) => { + setPlayerTeam(prevTeam => { + const isAlreadySelected = prevTeam.some(p => p.id === pokemon.id); + + if (isAlreadySelected) { + return prevTeam.filter(p => p.id !== pokemon.id); + } else if (prevTeam.length < 6) { + return [...prevTeam, pokemon]; + } + + return prevTeam; + }); + }, []); + + const handleRemovePokemon = useCallback((pokemon: Pokemon) => { + setPlayerTeam(prevTeam => prevTeam.filter(p => p.id !== pokemon.id)); + }, []); + + const handleGenerateRandomTeam = useCallback(async () => { + try { + const randomTeam = await generateRandomTeam(6); + setPlayerTeam(randomTeam); + } catch (error) { + console.error('Erro ao gerar time aleatório:', error); + } + }, [generateRandomTeam]); + + // Análise de tipos e fraquezas + const analyzeTeam = useCallback((team: Pokemon[]): TeamAnalysis => { + const typeEffectiveness: { [key: string]: { weakTo: string[]; strongAgainst: string[] } } = { + fire: { weakTo: ['water', 'ground', 'rock'], strongAgainst: ['grass', 'ice', 'bug', 'steel'] }, + water: { weakTo: ['electric', 'grass'], strongAgainst: ['fire', 'ground', 'rock'] }, + grass: { weakTo: ['fire', 'ice', 'poison', 'flying', 'bug'], strongAgainst: ['water', 'ground', 'rock'] }, + electric: { weakTo: ['ground'], strongAgainst: ['water', 'flying'] }, + ground: { weakTo: ['water', 'grass', 'ice'], strongAgainst: ['fire', 'electric', 'poison', 'rock', 'steel'] }, + rock: { weakTo: ['water', 'grass', 'fighting', 'ground', 'steel'], strongAgainst: ['fire', 'ice', 'flying', 'bug'] }, + psychic: { weakTo: ['bug', 'ghost', 'dark'], strongAgainst: ['fighting', 'poison'] }, + ice: { weakTo: ['fire', 'fighting', 'rock', 'steel'], strongAgainst: ['grass', 'ground', 'flying', 'dragon'] }, + dragon: { weakTo: ['ice', 'dragon', 'fairy'], strongAgainst: ['dragon'] }, + dark: { weakTo: ['fighting', 'bug', 'fairy'], strongAgainst: ['psychic', 'ghost'] }, + fairy: { weakTo: ['poison', 'steel'], strongAgainst: ['fighting', 'dragon', 'dark'] }, + fighting: { weakTo: ['flying', 'psychic', 'fairy'], strongAgainst: ['normal', 'ice', 'rock', 'dark', 'steel'] }, + poison: { weakTo: ['ground', 'psychic'], strongAgainst: ['grass', 'fairy'] }, + flying: { weakTo: ['electric', 'ice', 'rock'], strongAgainst: ['grass', 'fighting', 'bug'] }, + bug: { weakTo: ['fire', 'flying', 'rock'], strongAgainst: ['grass', 'psychic', 'dark'] }, + ghost: { weakTo: ['ghost', 'dark'], strongAgainst: ['psychic', 'ghost'] }, + steel: { weakTo: ['fire', 'fighting', 'ground'], strongAgainst: ['ice', 'rock', 'fairy'] }, + normal: { weakTo: ['fighting'], strongAgainst: [] } + }; + + const teamTypes = team.flatMap(pokemon => pokemon.types.map(t => t.type.name)); + const typeCount = teamTypes.reduce((acc, type) => { + acc[type] = (acc[type] || 0) + 1; + return acc; + }, {} as { [key: string]: number }); + + // Calcular fraquezas comuns + const weaknesses = Object.entries(typeCount) + .flatMap(([type]) => typeEffectiveness[type]?.weakTo || []) + .reduce((acc, weakness) => { + acc[weakness] = (acc[weakness] || 0) + 1; + return acc; + }, {} as { [key: string]: number }); + + // Calcular resistências + const resistances = Object.entries(typeCount) + .flatMap(([type]) => typeEffectiveness[type]?.strongAgainst || []) + .reduce((acc, resistance) => { + acc[resistance] = (acc[resistance] || 0) + 1; + return acc; + }, {} as { [key: string]: number }); + + // Gerar recomendações + const recommendations: string[] = []; + + const majorWeaknesses = Object.entries(weaknesses) + .filter(([, count]) => count >= 3) + .map(([type]) => type); + + if (majorWeaknesses.length > 0) { + recommendations.push(`Cuidado! Seu time é muito vulnerável a ataques do tipo ${majorWeaknesses.join(', ')}.`); + } + + const typeVariety = Object.keys(typeCount).length; + if (typeVariety < 4) { + recommendations.push('Considere diversificar mais os tipos do seu time para maior versatilidade.'); + } + + const avgStats = team.reduce((sum, pokemon) => { + return sum + pokemon.stats.reduce((total, stat) => total + stat.base_stat, 0); + }, 0) / (team.length || 1); + + if (avgStats < 400) { + recommendations.push('Considere adicionar Pokemon com stats mais altos para aumentar a força do time.'); + } + + // Calcular força geral (0-100) + const overallStrength = Math.min(100, Math.round( + (avgStats / 6) * 0.6 + // 60% baseado em stats + (typeVariety / 6) * 20 + // 20% baseado em variedade + (Object.keys(resistances).length / 10) * 20 // 20% baseado em resistências + )); + + return { + weaknesses: Object.keys(weaknesses), + resistances: Object.keys(resistances), + recommendations, + overallStrength + }; + }, []); + + // Exportar dados de teste + const exportTestData = useCallback(() => { + const stats = getStats(); + const testData = { + metadata: { + exportDate: new Date().toISOString(), + testMode: isTestingMode ? 'TESTING_100' : 'NORMAL_20', + config: { + populationSize: stats.populationSize, + elitePercentage: isTestingMode ? 0.1 : 0.2, + mutationRate: isTestingMode ? 0.20 : 0.15, + crossoverRate: isTestingMode ? 0.85 : 0.80, + tournamentSize: isTestingMode ? 6 : 4 + } + }, + currentStats: { + generation: stats.generation, + totalBattles: stats.totalBattles, + totalWins: stats.totalWins, + totalLosses: stats.totalLosses, + totalDraws: stats.totalDraws, + winRate: stats.totalBattles > 0 ? (stats.totalWins / stats.totalBattles) : 0, + bestFitness: stats.bestFitness, + averageFitness: stats.averageFitness, + diversity: stats.diversity + }, + population: { + genomes: population.genomes.map(g => ({ + id: g.id, + fitness: g.fitness, + wins: g.wins, + losses: g.losses, + draws: g.draws, + battlesPlayed: g.battlesPlayed, + generation: g.generation, + strategyType: g.genes.strategyType, + statsPriority: g.genes.statsPriority, + typeDistribution: g.genes.typeDistribution, + pokemonIds: g.genes.pokemonIds + })), + totalGenomes: population.genomes.length + }, + generationHistory: population.generationHistory + }; + + const blob = new Blob([JSON.stringify(testData, null, 2)], { + type: 'application/json' + }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `pokemon-genetic-test-${isTestingMode ? '100pop' : '20pop'}-gen${stats.generation}-${Date.now()}.json`; + a.click(); + URL.revokeObjectURL(url); + }, [population, getStats, isTestingMode]); + + // Gerar time da IA usando algoritmo genético + const generateCounterTeam = useCallback(async (playerTeam: Pokemon[]): Promise<{ team: Pokemon[], genomeId: string }> => { + try { + // Obter o melhor genoma da população + const bestGenome = getBestGenome(); + + if (!bestGenome) { + // Fallback: gerar time aleatório + const randomTeam = await generateRandomTeam(6); + return { team: randomTeam, genomeId: '' }; + } + + // Construir time baseado nos genes do melhor genoma + const counterTeam: Pokemon[] = []; + + for (const pokemonId of bestGenome.genes.pokemonIds) { + try { + const pkNodePokemon = await pokemonApi.getPokemonById(pokemonId); + const pokemon = convertPokemon(pkNodePokemon); + counterTeam.push(pokemon); + } catch (error) { + console.error(`Erro ao buscar Pokemon ${pokemonId}:`, error); + // Tentar um substituto aleatório + const randomId = Math.floor(Math.random() * 151) + 1; + try { + const pkNodePokemon = await pokemonApi.getPokemonById(randomId); + const pokemon = convertPokemon(pkNodePokemon); + counterTeam.push(pokemon); + } catch { + // Ignorar se falhar + } + } + } + + // Preencher slots restantes se necessário + while (counterTeam.length < 6) { + const randomId = Math.floor(Math.random() * 151) + 1; + try { + const pkNodePokemon = await pokemonApi.getPokemonById(randomId); + const pokemon = convertPokemon(pkNodePokemon); + if (!counterTeam.some(p => p.id === pokemon.id)) { + counterTeam.push(pokemon); + } + } catch (error) { + console.error('Erro ao preencher time da IA:', error); + } + } + + return { team: counterTeam, genomeId: bestGenome.id }; + } catch (error) { + console.error('Erro ao gerar time da IA:', error); + const randomTeam = await generateRandomTeam(6); + return { team: randomTeam, genomeId: '' }; + } + }, [getBestGenome, pokemonApi, generateRandomTeam]); + + const calculateTypeAdvantage = useCallback((attacker: Pokemon, defender: Pokemon): number => { + const typeEffectiveness: { [key: string]: { [key: string]: number } } = { + fire: { grass: 2, ice: 2, bug: 2, steel: 2, water: 0.5, fire: 0.5, rock: 0.5, dragon: 0.5 }, + water: { fire: 2, ground: 2, rock: 2, water: 0.5, grass: 0.5, dragon: 0.5 }, + grass: { water: 2, ground: 2, rock: 2, fire: 0.5, grass: 0.5, poison: 0.5, flying: 0.5, bug: 0.5, dragon: 0.5, steel: 0.5 }, + electric: { water: 2, flying: 2, electric: 0.5, grass: 0.5, dragon: 0.5, ground: 0 }, + ice: { grass: 2, ground: 2, flying: 2, dragon: 2, fire: 0.5, water: 0.5, ice: 0.5, steel: 0.5 }, + fighting: { normal: 2, ice: 2, rock: 2, dark: 2, steel: 2, poison: 0.5, flying: 0.5, psychic: 0.5, bug: 0.5, fairy: 0.5, ghost: 0 }, + poison: { grass: 2, fairy: 2, poison: 0.5, ground: 0.5, rock: 0.5, ghost: 0.5, steel: 0 }, + ground: { fire: 2, electric: 2, poison: 2, rock: 2, steel: 2, grass: 0.5, bug: 0.5, flying: 0 }, + flying: { grass: 2, fighting: 2, bug: 2, electric: 0.5, rock: 0.5, steel: 0.5 }, + psychic: { fighting: 2, poison: 2, psychic: 0.5, steel: 0.5, dark: 0 }, + bug: { grass: 2, psychic: 2, dark: 2, fire: 0.5, fighting: 0.5, poison: 0.5, flying: 0.5, ghost: 0.5, steel: 0.5, fairy: 0.5 }, + rock: { fire: 2, ice: 2, flying: 2, bug: 2, fighting: 0.5, ground: 0.5, steel: 0.5 }, + ghost: { psychic: 2, ghost: 2, dark: 0.5, normal: 0 }, + dragon: { dragon: 2, steel: 0.5, fairy: 0 }, + dark: { psychic: 2, ghost: 2, fighting: 0.5, dark: 0.5, fairy: 0.5 }, + steel: { ice: 2, rock: 2, fairy: 2, fire: 0.5, water: 0.5, electric: 0.5, steel: 0.5 }, + fairy: { fighting: 2, dragon: 2, dark: 2, fire: 0.5, poison: 0.5, steel: 0.5 } + }; + + let multiplier = 1; + const attackerTypes = attacker.types.map(t => t.type.name); + const defenderTypes = defender.types.map(t => t.type.name); + + attackerTypes.forEach(atkType => { + defenderTypes.forEach(defType => { + if (typeEffectiveness[atkType]?.[defType]) { + multiplier *= typeEffectiveness[atkType][defType]; + } + }); + }); + + return multiplier; + }, []); + + // Simular batalha individual entre dois Pokemon + const simulateIndividualBattle = useCallback((playerPokemon: Pokemon, aiPokemon: Pokemon): IndividualBattle => { + const playerStats = playerPokemon.stats.reduce((sum, stat) => sum + stat.base_stat, 0); + const aiStats = aiPokemon.stats.reduce((sum, stat) => sum + stat.base_stat, 0); + + const playerAdvantage = calculateTypeAdvantage(playerPokemon, aiPokemon); + const aiAdvantage = calculateTypeAdvantage(aiPokemon, playerPokemon); + + const playerDamage = Math.round(playerStats * playerAdvantage * (0.8 + Math.random() * 0.4)); + const aiDamage = Math.round(aiStats * aiAdvantage * (0.8 + Math.random() * 0.4)); + + const winner = playerDamage > aiDamage ? 'player' : 'ai'; + + let typeAdvantage: 'player' | 'ai' | 'neutral' = 'neutral'; + + const playerHasAdvantage = playerAdvantage >= 2.0; + const aiHasAdvantage = aiAdvantage >= 2.0; + + if (playerHasAdvantage && !aiHasAdvantage) { + typeAdvantage = 'player'; + } else if (aiHasAdvantage && !playerHasAdvantage) { + typeAdvantage = 'ai'; + } else if (playerHasAdvantage && aiHasAdvantage) { + // Ambos têm vantagem, mostrar quem tem maior + typeAdvantage = playerAdvantage > aiAdvantage ? 'player' : 'ai'; + } + // Se nenhum tem vantagem (ambos < 2.0), permanece neutral + + let reasoning = ''; + if (typeAdvantage === 'player') { + reasoning = `${playerPokemon.name} tem vantagem de tipo SUPER EFETIVA (${playerAdvantage.toFixed(1)}x) sobre ${aiPokemon.name}!`; + } else if (typeAdvantage === 'ai') { + reasoning = `${aiPokemon.name} tem vantagem de tipo SUPER EFETIVA (${aiAdvantage.toFixed(1)}x) sobre ${playerPokemon.name}!`; + } else if (playerAdvantage < 1.0 || aiAdvantage < 1.0) { + // Mencionar se há resistência + const resistantPokemon = playerAdvantage < 1.0 ? aiPokemon.name : playerPokemon.name; + const multiplier = playerAdvantage < 1.0 ? playerAdvantage : aiAdvantage; + reasoning = `${resistantPokemon} resiste ao ataque (${multiplier.toFixed(1)}x). Vencedor decidido por stats totais.`; + } else { + reasoning = `Confronto equilibrado! Ambos com dano neutro (1.0x). Vencedor decidido por stats totais.`; + } + + return { + playerPokemon, + aiPokemon, + winner, + playerDamage, + aiDamage, + typeAdvantage, + reasoning + }; + }, [calculateTypeAdvantage]); + + // Simular batalha + const simulateBattle = useCallback((playerTeam: Pokemon[], aiTeam: Pokemon[], genomeId: string | null): BattleResult => { + // Simular todas as batalhas individuais + const battles: IndividualBattle[] = []; + let playerScore = 0; + let aiScore = 0; + + for (let i = 0; i < Math.min(playerTeam.length, aiTeam.length); i++) { + const battle = simulateIndividualBattle(playerTeam[i], aiTeam[i]); + battles.push(battle); + + if (battle.winner === 'player') playerScore++; + else aiScore++; + } + + let winner: 'player' | 'ai' | 'draw'; + let analysis: string; + + if (playerScore === aiScore) { + winner = 'draw'; + analysis = `Empate incrível! ${playerScore} vitórias para cada lado. Ambos os times mostraram grande força e estratégia.`; + } else if (playerScore > aiScore) { + winner = 'player'; + analysis = playerScore - aiScore >= 3 + ? `Vitória dominante! Você venceu ${playerScore} de ${battles.length} confrontos. Seu time demonstrou superioridade clara!` + : `Vitória conquistada! ${playerScore} vitórias contra ${aiScore} da IA. Uma batalha acirrada, mas sua estratégia prevaleceu.`; + } else { + winner = 'ai'; + analysis = aiScore - playerScore >= 3 + ? `A IA dominou com ${aiScore} vitórias! Seu time precisa de ajustes estratégicos.` + : `Derrota por ${aiScore} a ${playerScore}. Foi uma batalha próxima, você está quase lá!`; + } + + // Registrar resultado no sistema genético + if (genomeId) { + const playerTypes = playerTeam.flatMap(p => p.types.map(t => t.type.name)); + const result = winner === 'ai' ? 'win' : winner === 'player' ? 'loss' : 'draw'; + recordBattle(genomeId, result, playerTypes); + } + + return { playerTeam, aiTeam, winner, analysis, battles, playerScore, aiScore }; + }, [simulateIndividualBattle, recordBattle]); + + // Executar bateria de testes automatizados + const runAutomatedTests = useCallback(async (numberOfBattles: number): Promise<{ + battleNumber: number; + result: BattleResult; + timestamp: number; + }[]> => { + setIsRunningTests(true); + setTestProgress({ current: 0, total: numberOfBattles }); + + const results: { + battleNumber: number; + result: BattleResult; + timestamp: number; + }[] = []; + + try { + for (let i = 0; i < numberOfBattles; i++) { + // Gerar time aleatório do jogador + const randomPlayerTeam = await generateRandomTeam(6); + + // Gerar time da IA contra esse time + const { team: aiCounterTeam, genomeId } = await generateCounterTeam(randomPlayerTeam); + + // Simular batalha (já registra o resultado internamente via recordBattle) + const battleResult = simulateBattle(randomPlayerTeam, aiCounterTeam, genomeId); + + // Armazenar resultado + results.push({ + battleNumber: i + 1, + result: battleResult, + timestamp: Date.now() + }); + + // Atualizar progresso + setTestProgress({ current: i + 1, total: numberOfBattles }); + + // Evoluir população a cada 5 batalhas usando dados acumulados + // A evolução usa todos os resultados registrados via recordBattle, não apenas o último time + if ((i + 1) % 5 === 0) { + // Coletar tipos únicos de todos os times de jogadores testados até agora + const allPlayerTypes = results + .slice(Math.max(0, results.length - 5), results.length) // Últimas 5 batalhas + .flatMap(r => r.result.playerTeam.flatMap(p => p.types.map(t => t.type.name))); + + // Remover duplicatas + const uniquePlayerTypes = Array.from(new Set(allPlayerTypes)); + + // Estatísticas antes da evolução + const recentWins = results.slice(-5).filter(r => r.result.winner === 'ai').length; + console.log(`🧬 Evolução #${Math.floor((i + 1) / 5)} - Batalhas ${i - 3}-${i + 1}: ${recentWins}/5 vitórias da IA (${(recentWins/5*100).toFixed(1)}%)`); + + evolveGeneration(uniquePlayerTypes); + } + + // Pequeno delay para não travar a UI + if ((i + 1) % 10 === 0) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + } + + // Evolução final consolidada com TODOS os tipos encontrados nos testes + if (results.length > 0) { + const allPlayerTypes = results + .flatMap(r => r.result.playerTeam.flatMap(p => p.types.map(t => t.type.name))); + const uniquePlayerTypes = Array.from(new Set(allPlayerTypes)); + + // Estatísticas finais + const totalAiWins = results.filter(r => r.result.winner === 'ai').length; + const finalWinRate = (totalAiWins / results.length * 100).toFixed(1); + + console.log(`\n🎯 EVOLUÇÃO FINAL CONSOLIDADA`); + console.log(`📊 ${results.length} batalhas | ${totalAiWins} vitórias IA (${finalWinRate}%)`); + console.log(`🧬 ${uniquePlayerTypes.length} tipos únicos encontrados: ${uniquePlayerTypes.slice(0, 10).join(', ')}${uniquePlayerTypes.length > 10 ? '...' : ''}`); + + evolveGeneration(uniquePlayerTypes); + + console.log(`✅ População evoluída com base em todos os resultados!\n`); + } + } finally { + setIsRunningTests(false); + } + + return results; + }, [generateRandomTeam, generateCounterTeam, simulateBattle, evolveGeneration]); + + // Handlers dos views + const handleStartSetup = () => setCurrentView('setup'); + const handleAnalyzeTeam = () => { + if (playerTeam.length > 0) { + const analysis = analyzeTeam(playerTeam); + setTeamAnalysis(analysis); + setCurrentView('analysis'); + } + }; + + const handleStartBattle = async () => { + if (playerTeam.length > 0) { + const { team: aiCounterTeam, genomeId } = await generateCounterTeam(playerTeam); + setAiTeam(aiCounterTeam); + setCurrentGenomeId(genomeId); + const result = simulateBattle(playerTeam, aiCounterTeam, genomeId); + setBattleResult(result); + setCurrentView('battle'); + + // Evoluir população a cada 5 batalhas + const stats = getStats(); + if (stats.totalBattles % 5 === 0 && stats.totalBattles > 0) { + const playerTypes = playerTeam.flatMap(p => p.types.map(t => t.type.name)); + evolveGeneration(playerTypes); + } + } + }; + + const handleBackToMenu = () => { + setCurrentView('menu'); + setPlayerTeam([]); + setAiTeam([]); + setTeamAnalysis(null); + setBattleResult(null); + }; + + // Views do componente + const renderMenuView = () => { + const stats = getStats(); + const winRate = stats.totalBattles > 0 + ? ((stats.totalWins / stats.totalBattles) * 100).toFixed(1) + : '0.0'; + + return ( +
+
+
+
+ {/* Pokéball Icon SVG */} + + + + + + + + +

+ Pokemon Battle AI +

+ + + + + + + + +
+

+ Monte seu time estratégico e enfrente uma IA com Algoritmo Genético que evolui a cada batalha! +

+
+ + {/* AI Stats Display */} + {stats.totalBattles > 0 && ( +
+

Estatísticas Genéticas da IA

+
+
+

Geração

+

{stats.generation}

+
+
+

Batalhas

+

{stats.totalBattles}

+
+
+

Vitórias da IA

+

{stats.totalWins}

+
+
+

Taxa de Vitória

+

{winRate}%

+
+
+

Fitness Médio

+

{stats.averageFitness.toFixed(1)}

+
+
+
+
+

Melhor Fitness

+
+
+
+
+ {stats.bestFitness.toFixed(1)} +
+
+
+

Diversidade Genética

+
+
+
+
+ {stats.diversity.toFixed(1)} +
+
+
+ +
+ )} + +
+
+

Estratégia

+

+ Escolha Pokemon com tipos complementares e stats equilibrados +

+
+
+

IA com Algoritmo Genético

+

+ Evolução real através de crossover, mutação e seleção natural! +

+
+
+

Batalhas Individuais

+

+ Veja cada confronto 1v1 com análise de tipos +

+
+
+ + + + +
+
+ ); + }; + + const renderSetupView = () => ( +
+
+ {/* Header */} +
+

+ Monte seu Time Pokemon +

+ +
+ + {/* Error Display */} + {error && ( +
+

{error}

+
+ )} + + {/* Loading Indicator */} + {loading && ( +
+
+
+
+

+ Carregando Pokemon... +

+

+ Primeira vez pode levar alguns segundos. Próximas vezes serão instantâneas! +

+
+
+
+ )} + + {/* Team Display */} +
+ + +
+ + + + + +
+
+ + {/* Generation Filter */} +
+

+ Filtrar por Geração: +

+
+ + + + + + + + +
+
+ + {/* Pokemon Grid */} + + + {/* Pagination Controls */} +
+ + +
+ + Página {currentPage} de {totalPages} + + + {/* Quick page jumps */} + {totalPages > 1 && ( +
+ {[1, 2, 3, 4, 5].map(page => { + if (page > totalPages) return null; + const isCurrentPage = page === currentPage; + return ( + + ); + })} + {totalPages > 5 && ...} + {totalPages > 5 && ( + + )} +
+ )} +
+ + +
+ + {/* Pokemon count info */} +
+ Mostrando {paginatedPokemon.length} Pokemon (Total: até 905 Pokemon das gerações 1-8) +
+
+
+ ); + + const renderAnalysisView = () => ( +
+
+
+

+ Análise do Time +

+ +
+ + {teamAnalysis && ( +
+ {/* Força Geral */} +
+

+ Força Geral do Time +

+
+
+
+
+ + {teamAnalysis.overallStrength}% + +
+
+ + {/* Team Display */} + + + {/* Análise Detalhada */} +
+ {/* Fraquezas */} +
+

+ Principais Fraquezas +

+
+ {teamAnalysis.weaknesses.map(weakness => ( + + {weakness} + + ))} +
+
+ + {/* Resistências */} +
+

+ ✅ Resistências +

+
+ {teamAnalysis.resistances.map(resistance => ( + + {resistance} + + ))} +
+
+
+ + {/* Recomendações */} +
+

+ Recomendações Estratégicas +

+
    + {teamAnalysis.recommendations.map((rec, index) => ( +
  • + + {rec} +
  • + ))} +
+
+ +
+ + +
+
+ )} +
+
+ ); + + const renderBattleView = () => ( +
+
+
+

+ Batalha Pokemon +

+ +
+ + {battleResult && ( +
+ {/* Resultado da Batalha */} +
+

+ {battleResult.winner === 'player' ? 'VITÓRIA!' : + battleResult.winner === 'ai' ? 'DERROTA!' : 'EMPATE!'} +

+
+
+

Você

+

{battleResult.playerScore}

+
+
-
+
+

IA

+

{battleResult.aiScore}

+
+
+

+ {battleResult.analysis} +

+
+ + {/* Individual Battles Section */} +
+

+ Confrontos Individuais +

+
+ {battleResult.battles.map((battle, index) => ( + + ))} +
+
+ + {/* Times da Batalha */} +
+
+

Seu Time

+
+ {battleResult.playerTeam.map((pokemon, index) => ( +
+ {/* Número da Posição */} +
+ {index + 1} +
+ + {/* Imagem do Pokemon */} +
+ +
+ + {/* Nome */} +

+ {pokemon.name} +

+ + {/* Tipos */} +
+ {pokemon.types.map((type, typeIndex) => { + const typeColors: { [key: string]: string } = { + normal: 'bg-gray-400', fire: 'bg-red-500', water: 'bg-blue-500', + electric: 'bg-yellow-400', grass: 'bg-green-500', ice: 'bg-blue-200', + fighting: 'bg-red-700', poison: 'bg-purple-500', ground: 'bg-yellow-600', + flying: 'bg-indigo-400', psychic: 'bg-pink-500', bug: 'bg-green-400', + rock: 'bg-yellow-800', ghost: 'bg-purple-700', dragon: 'bg-indigo-700', + dark: 'bg-gray-800', steel: 'bg-gray-500', fairy: 'bg-pink-300' + }; + return ( + + {type.type.name} + + ); + })} +
+ + {/* Stats Resumidos */} +
+ {['hp', 'attack', 'defense'].map((statName) => { + const stat = pokemon.stats.find(s => s.stat.name === statName); + const value = stat?.base_stat || 0; + const label = statName === 'hp' ? 'HP' : + statName === 'attack' ? 'ATK' : 'DEF'; + + return ( +
+ + {label} + +
+
+
+
+ + {value} + +
+
+ ); + })} +
+
+ ))} +
+
+ +
+

Time da IA

+
+ {battleResult.aiTeam.map((pokemon, index) => ( +
+ {/* Número da Posição */} +
+ {index + 1} +
+ + {/* Imagem do Pokemon */} +
+ +
+ + {/* Nome */} +

+ {pokemon.name} +

+ + {/* Tipos */} +
+ {pokemon.types.map((type, typeIndex) => { + const typeColors: { [key: string]: string } = { + normal: 'bg-gray-400', fire: 'bg-red-500', water: 'bg-blue-500', + electric: 'bg-yellow-400', grass: 'bg-green-500', ice: 'bg-blue-200', + fighting: 'bg-red-700', poison: 'bg-purple-500', ground: 'bg-yellow-600', + flying: 'bg-indigo-400', psychic: 'bg-pink-500', bug: 'bg-green-400', + rock: 'bg-yellow-800', ghost: 'bg-purple-700', dragon: 'bg-indigo-700', + dark: 'bg-gray-800', steel: 'bg-gray-500', fairy: 'bg-pink-300' + }; + return ( + + {type.type.name} + + ); + })} +
+ + {/* Stats Resumidos */} +
+ {['hp', 'attack', 'defense'].map((statName) => { + const stat = pokemon.stats.find(s => s.stat.name === statName); + const value = stat?.base_stat || 0; + const label = statName === 'hp' ? 'HP' : + statName === 'attack' ? 'ATK' : 'DEF'; + + return ( +
+ + {label} + +
+
+
+
+ + {value} + +
+
+ ); + })} +
+
+ ))} +
+
+
+ +
+ +
+
+ )} +
+
+ ); + + // Render principal baseado na view atual + const renderAutomatedTestsView = () => ( +
+
+

+ 🧪 Testes Automatizados +

+ +
+ + +
+ ); + + switch (currentView) { + case 'setup': return renderSetupView(); + case 'analysis': return renderAnalysisView(); + case 'battle': return renderBattleView(); + case 'automated-tests': return renderAutomatedTestsView(); + default: return renderMenuView(); + } +} \ No newline at end of file diff --git a/app/games/pokemon/components/AutomatedTestBattery.tsx b/app/games/pokemon/components/AutomatedTestBattery.tsx new file mode 100644 index 0000000..87af841 --- /dev/null +++ b/app/games/pokemon/components/AutomatedTestBattery.tsx @@ -0,0 +1,274 @@ +import { useState } from 'react'; +import type { BattleResult } from '../types/pokemon'; + +interface TestResult { + battleNumber: number; + result: BattleResult; + timestamp: number; +} + +interface AutomatedTestBatteryProps { + onRunTests: (numberOfBattles: number) => Promise; + isRunning: boolean; + progress: { current: number; total: number }; +} + +export function AutomatedTestBattery({ onRunTests, isRunning, progress }: AutomatedTestBatteryProps) { + const [testResults, setTestResults] = useState([]); + const [expandedBattle, setExpandedBattle] = useState(null); + const [numberOfBattles, setNumberOfBattles] = useState(100); + + const handleRunTests = async () => { + const results = await onRunTests(numberOfBattles); + setTestResults(results); + }; + + const toggleBattle = (battleNumber: number) => { + setExpandedBattle(expandedBattle === battleNumber ? null : battleNumber); + }; + + const getStats = () => { + if (testResults.length === 0) return null; + + const playerWins = testResults.filter(r => r.result.winner === 'player').length; + const aiWins = testResults.filter(r => r.result.winner === 'ai').length; + const draws = testResults.filter(r => r.result.winner === 'draw').length; + + return { + total: testResults.length, + playerWins, + aiWins, + draws, + playerWinRate: ((playerWins / testResults.length) * 100).toFixed(1), + aiWinRate: ((aiWins / testResults.length) * 100).toFixed(1) + }; + }; + + const stats = getStats(); + + return ( +
+
+

+ Bateria de Testes Automatizados +

+

+ Execute múltiplas batalhas automáticas para testar a evolução da IA genética +

+ +
+
+ + setNumberOfBattles(parseInt(e.target.value) || 100)} + disabled={isRunning} + className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg + bg-white dark:bg-gray-700 text-gray-900 dark:text-white + disabled:opacity-50 disabled:cursor-not-allowed" + /> +
+ + + + {testResults.length > 0 && ( + + )} +
+ + {/* Progress Bar */} + {isRunning && ( +
+
+ Progresso: + {progress.current} / {progress.total} batalhas +
+
+
0 ? (progress.current / progress.total) * 100 : 0}%` }} + /> +
+
+ )} +
+ + {/* Statistics Summary */} + {stats && ( +
+

+ Estatísticas Gerais +

+
+
+

Total de Batalhas

+

{stats.total}

+
+
+

Vitórias Jogador

+

{stats.playerWins}

+

{stats.playerWinRate}%

+
+
+

Vitórias IA

+

{stats.aiWins}

+

{stats.aiWinRate}%

+
+
+

Empates

+

{stats.draws}

+
+
+

Vantagem IA

+

+ {stats.aiWins > stats.playerWins ? '+' : '-'} + {Math.abs(stats.aiWins - stats.playerWins)} +

+
+
+
+ )} + + {/* Battle Results List */} + {testResults.length > 0 && ( +
+

+ Resultados Detalhados ({testResults.length} batalhas) +

+
+ {testResults.map((test) => ( +
+ {/* Battle Header */} + + + {/* Battle Details (Expanded) */} + {expandedBattle === test.battleNumber && ( +
+

+ {test.result.analysis} +

+ + {/* Individual Matchups */} +
+

+ Confrontos Individuais: +

+ {test.result.battles.map((battle, idx) => ( +
+
+
+

+ {battle.playerPokemon.name} +

+
+ {battle.playerPokemon.types.map((type) => ( + + {type.type.name} + + ))} +
+
+ +
+ + VS + +
+ +
+

+ {battle.aiPokemon.name} +

+
+ {battle.aiPokemon.types.map((type) => ( + + {type.type.name} + + ))} +
+
+
+ +

+ {battle.reasoning} +

+
+ ))} +
+
+ )} +
+ ))} +
+
+ )} +
+ ); +} diff --git a/app/games/pokemon/components/BattleMatchup.tsx b/app/games/pokemon/components/BattleMatchup.tsx new file mode 100644 index 0000000..a64cb53 --- /dev/null +++ b/app/games/pokemon/components/BattleMatchup.tsx @@ -0,0 +1,131 @@ +import type { IndividualBattle } from '../types/pokemon'; + +interface BattleMatchupProps { + battle: IndividualBattle; + index: number; +} + +const TYPE_COLORS: { [key: string]: string } = { + normal: '#A8A878', + fire: '#F08030', + water: '#6890F0', + electric: '#F8D030', + grass: '#78C850', + ice: '#98D8D8', + fighting: '#C03028', + poison: '#A040A0', + ground: '#E0C068', + flying: '#A890F0', + psychic: '#F85888', + bug: '#A8B820', + rock: '#B8A038', + ghost: '#705898', + dragon: '#7038F8', + dark: '#705848', + steel: '#B8B8D0', + fairy: '#EE99AC' +}; + +export function BattleMatchup({ battle, index }: BattleMatchupProps) { + const { playerPokemon, aiPokemon, winner, playerDamage, aiDamage, typeAdvantage, reasoning } = battle; + + const getAdvantageIcon = () => { + if (typeAdvantage === 'player') return '→'; + if (typeAdvantage === 'ai') return '←'; + return '⚔'; + }; + + const getAdvantageColor = () => { + if (typeAdvantage === 'player') return 'text-green-500'; + if (typeAdvantage === 'ai') return 'text-red-500'; + return 'text-yellow-500'; + }; + + return ( +
+
+

Batalha {index + 1}

+ + {getAdvantageIcon()} + +
+ +
+ {/* Player Pokemon */} +
+ {playerPokemon.name} { + const target = e.target as HTMLImageElement; + const paddedId = String(playerPokemon.id).padStart(3, '0'); + const serebiiUrl = `https://www.serebii.net/pokemon/art/${paddedId}.png`; + + if (!target.src.includes('serebii')) { + target.src = serebiiUrl; + } + }} + /> +

{playerPokemon.name}

+
+ {playerPokemon.types.map(type => ( + + {type.type.name} + + ))} +
+

Dano: {playerDamage}

+
+ + {/* VS Divider */} +
+

VS

+
+ + {/* AI Pokemon */} +
+ {aiPokemon.name} { + const target = e.target as HTMLImageElement; + const paddedId = String(aiPokemon.id).padStart(3, '0'); + const serebiiUrl = `https://www.serebii.net/pokemon/art/${paddedId}.png`; + + if (!target.src.includes('serebii')) { + target.src = serebiiUrl; + } + }} + /> +

{aiPokemon.name}

+
+ {aiPokemon.types.map(type => ( + + {type.type.name} + + ))} +
+

Dano: {aiDamage}

+
+
+ + {/* Reasoning */} +
+

+ Análise: + {reasoning} +

+
+
+ ); +} diff --git a/app/games/pokemon/components/PokemonGrid.tsx b/app/games/pokemon/components/PokemonGrid.tsx new file mode 100644 index 0000000..123ac7d --- /dev/null +++ b/app/games/pokemon/components/PokemonGrid.tsx @@ -0,0 +1,193 @@ +import type { Pokemon } from '../types/pokemon'; + +interface PokemonGridProps { + pokemon: Pokemon[]; + onPokemonSelect: (pokemon: Pokemon) => void; + selectedPokemon: Pokemon[]; + loading?: boolean; + searchTerm?: string; + onSearchChange?: (term: string) => void; + maxSelections?: number; +} + +const typeColors: { [key: string]: string } = { + normal: 'bg-gray-400', + fire: 'bg-red-500', + water: 'bg-blue-500', + electric: 'bg-yellow-400', + grass: 'bg-green-500', + ice: 'bg-blue-200', + fighting: 'bg-red-700', + poison: 'bg-purple-500', + ground: 'bg-yellow-600', + flying: 'bg-indigo-400', + psychic: 'bg-pink-500', + bug: 'bg-green-400', + rock: 'bg-yellow-800', + ghost: 'bg-purple-700', + dragon: 'bg-indigo-700', + dark: 'bg-gray-800', + steel: 'bg-gray-500', + fairy: 'bg-pink-300' +}; + +// Imagem fallback em base64 (pokeball simples) +const FALLBACK_IMAGE = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgZmlsbD0iI0VFRSIgc3Ryb2tlPSIjMzMzIiBzdHJva2Utd2lkdGg9IjMiLz48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSIxNSIgZmlsbD0iI0ZGRiIgc3Ryb2tlPSIjMzMzIiBzdHJva2Utd2lkdGg9IjMiLz48L3N2Zz4='; + +export default function PokemonGrid({ + pokemon, + onPokemonSelect, + selectedPokemon = [], + loading = false, + searchTerm = '', + onSearchChange, + maxSelections = 6 +}: PokemonGridProps) { + const isSelected = (pokemonItem: Pokemon) => + selectedPokemon.some(p => p.id === pokemonItem.id); + + const canSelect = (pokemonItem: Pokemon) => + isSelected(pokemonItem) || selectedPokemon.length < maxSelections; + + return ( +
+ {/* Barra de Pesquisa */} + {onSearchChange && ( +
+
+ onSearchChange(e.target.value)} + placeholder="Buscar Pokemon por nome ou tipo..." + className="w-full px-4 py-3 pl-10 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg + text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 + focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" + /> +
+ + + +
+
+
+ )} + + {/* Indicador de Loading */} + {loading && ( +
+
+ Carregando Pokemon... +
+ )} + + {/* Grid de Pokemon */} +
+ {pokemon.map((pokemonItem) => ( +
canSelect(pokemonItem) && onPokemonSelect(pokemonItem)} + className={` + relative bg-white dark:bg-gray-800 rounded-lg shadow-md border-2 p-4 cursor-pointer + transition-all duration-200 hover:shadow-lg transform hover:-translate-y-1 + ${isSelected(pokemonItem) + ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' + : 'border-gray-200 dark:border-gray-700 hover:border-blue-300' + } + ${!canSelect(pokemonItem) ? 'opacity-50 cursor-not-allowed' : ''} + `} + > + {/* Badge de Selecionado */} + {isSelected(pokemonItem) && ( +
+ ✓ +
+ )} + + {/* Imagem do Pokemon */} +
+ {pokemonItem.name} { + const target = e.target as HTMLImageElement; + const paddedId = String(pokemonItem.id).padStart(3, '0'); + const serebiiUrl = `https://www.serebii.net/pokemon/art/${paddedId}.png`; + + if (!target.src.includes('serebii') && target.src !== FALLBACK_IMAGE) { + target.src = serebiiUrl; + } else if (target.src.includes('serebii')) { + target.src = FALLBACK_IMAGE; + } + }} + /> +
+ + {/* Nome do Pokemon */} +

+ #{pokemonItem.id.toString().padStart(3, '0')} {pokemonItem.name} +

+ + {/* Tipos */} +
+ {pokemonItem.types.map((type, index) => ( + + {type.type.name} + + ))} +
+ + {/* Stats Principais */} +
+ {pokemonItem.stats.slice(0, 3).map((stat, index) => { + const statName = stat.stat.name === 'hp' ? 'HP' : + stat.stat.name === 'attack' ? 'ATK' : 'DEF'; + return ( +
+ + {statName} + +
+
+
+
+ + {stat.base_stat} + +
+
+ ); + })} +
+ + {/* Hover Effect */} +
+
+ ))} +
+ + {/* Mensagem de Pokemon não encontrado */} + {!loading && pokemon.length === 0 && searchTerm && ( +
+
+ + + +

Nenhum Pokemon encontrado

+

Tente buscar com outros termos

+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/app/games/pokemon/components/TeamDisplay.tsx b/app/games/pokemon/components/TeamDisplay.tsx new file mode 100644 index 0000000..42f659a --- /dev/null +++ b/app/games/pokemon/components/TeamDisplay.tsx @@ -0,0 +1,259 @@ +import type { Pokemon } from '../types/pokemon'; + +interface TeamDisplayProps { + team: Pokemon[]; + onRemovePokemon?: (pokemon: Pokemon) => void; + title?: string; + showRemoveButton?: boolean; + maxSize?: number; + className?: string; +} + +const typeColors: { [key: string]: string } = { + normal: 'bg-gray-400', + fire: 'bg-red-500', + water: 'bg-blue-500', + electric: 'bg-yellow-400', + grass: 'bg-green-500', + ice: 'bg-blue-200', + fighting: 'bg-red-700', + poison: 'bg-purple-500', + ground: 'bg-yellow-600', + flying: 'bg-indigo-400', + psychic: 'bg-pink-500', + bug: 'bg-green-400', + rock: 'bg-yellow-800', + ghost: 'bg-purple-700', + dragon: 'bg-indigo-700', + dark: 'bg-gray-800', + steel: 'bg-gray-500', + fairy: 'bg-pink-300' +}; + +// Imagem fallback em base64 (pokeball simples) +const FALLBACK_IMAGE = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgZmlsbD0iI0VFRSIgc3Ryb2tlPSIjMzMzIiBzdHJva2Utd2lkdGg9IjMiLz48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSIxNSIgZmlsbD0iI0ZGRiIgc3Ryb2tlPSIjMzMzIiBzdHJva2Utd2lkdGg9IjMiLz48L3N2Zz4='; + +export default function TeamDisplay({ + team, + onRemovePokemon, + title = "Seu Time", + showRemoveButton = true, + maxSize = 6, + className = "" +}: TeamDisplayProps) { + + const emptySlots = Array.from({ length: Math.max(0, maxSize - team.length) }); + + const getPokemonStats = (pokemon: Pokemon) => { + const stats = pokemon.stats.reduce((acc, stat) => { + acc[stat.stat.name] = stat.base_stat; + return acc; + }, {} as { [key: string]: number }); + + return { + hp: stats.hp || 0, + attack: stats.attack || 0, + defense: stats.defense || 0, + total: Object.values(stats).reduce((sum, val) => sum + val, 0) + }; + }; + + return ( +
+ {/* Cabeçalho */} +
+

+ {title} +

+
+ + {team.length}/{maxSize} Pokemon selecionados + + {team.length > 0 && ( + + Força Total: + {team.reduce((sum, pokemon) => sum + getPokemonStats(pokemon).total, 0)} + + + )} +
+
+ + {/* Grid do Time */} +
+ {/* Pokemon Selecionados */} + {team.map((pokemon, index) => ( +
+ {/* Botão de Remover */} + {showRemoveButton && onRemovePokemon && ( + + )} + + {/* Número da Posição */} +
+ {index + 1} +
+ + {/* Imagem do Pokemon */} +
+ {pokemon.name} { + const target = e.target as HTMLImageElement; + const paddedId = String(pokemon.id).padStart(3, '0'); + const serebiiUrl = `https://www.serebii.net/pokemon/art/${paddedId}.png`; + + if (!target.src.includes('serebii') && target.src !== FALLBACK_IMAGE) { + target.src = serebiiUrl; + } else if (target.src.includes('serebii')) { + target.src = FALLBACK_IMAGE; + } + }} + /> +
+ + {/* Nome */} +

+ {pokemon.name} +

+ + {/* Tipos */} +
+ {pokemon.types.map((type, typeIndex) => ( + + {type.type.name} + + ))} +
+ + {/* Stats Resumidos */} +
+ {['hp', 'attack', 'defense'].map((statName) => { + const stat = pokemon.stats.find(s => s.stat.name === statName); + const value = stat?.base_stat || 0; + const label = statName === 'hp' ? 'HP' : + statName === 'attack' ? 'ATK' : 'DEF'; + + return ( +
+ + {label} + +
+
+
+
+ + {value} + +
+
+ ); + })} +
+
+ ))} + + {/* Slots Vazios */} + {emptySlots.map((_, index) => ( +
+ {/* Número da Posição */} +
+ {team.length + index + 1} +
+ +
+
+ + + +
+

+ Slot vazio +

+
+
+ ))} +
+ + {/* Resumo do Time (se tiver Pokemon) */} + {team.length > 0 && ( +
+

Análise do Time

+ + {/* Tipos do Time */} +
+

Tipos presentes:

+
+ {Array.from(new Set(team.flatMap(p => p.types.map(t => t.type.name)))).map(type => ( + + {type} + + ))} +
+
+ + {/* Stats Médias */} +
+

Stats médias do time:

+
+ {['hp', 'attack', 'defense'].map(statName => { + const average = team.length > 0 + ? Math.round(team.reduce((sum, pokemon) => { + const stat = pokemon.stats.find(s => s.stat.name === statName); + return sum + (stat?.base_stat || 0); + }, 0) / team.length) + : 0; + + const label = statName === 'hp' ? 'HP Médio' : + statName === 'attack' ? 'Ataque Médio' : 'Defesa Média'; + + return ( +
+
{label}
+
+ {average} +
+
+ ); + })} +
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/app/games/pokemon/hooks/useGeneticAI.ts b/app/games/pokemon/hooks/useGeneticAI.ts new file mode 100644 index 0000000..884e874 --- /dev/null +++ b/app/games/pokemon/hooks/useGeneticAI.ts @@ -0,0 +1,410 @@ +import { useState, useEffect, useCallback } from 'react'; +import type { + TeamGenome, + GeneticPopulation, + GeneticConfig, + TeamGenes, + StrategyType, + StatsPriority +} from '../types/genetic'; + +const STORAGE_KEY = 'pokemon-genetic-population'; + +// Configuração padrão +const DEFAULT_CONFIG: GeneticConfig = { + populationSize: 20, + elitePercentage: 0.2, + mutationRate: 0.15, + crossoverRate: 0.8, + tournamentSize: 4 +}; + +// Configuração para testes intensivos +export const TESTING_CONFIG: GeneticConfig = { + populationSize: 100, // 5x maior população + elitePercentage: 0.1, // Mantém top 10 genomas + mutationRate: 0.20, // Mais exploração + crossoverRate: 0.85, // Mais recombinação + tournamentSize: 6 // Seleção mais competitiva +}; + +// Tipos de Pokemon disponíveis +const ALL_TYPES = [ + 'normal', 'fire', 'water', 'electric', 'grass', 'ice', + 'fighting', 'poison', 'ground', 'flying', 'psychic', 'bug', + 'rock', 'ghost', 'dragon', 'dark', 'steel', 'fairy' +]; + +// Gerar ID único +function generateId(): string { + return `genome_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +} + +// Gerar genes aleatórios +function generateRandomGenes(): TeamGenes { + const pokemonIds: number[] = []; + while (pokemonIds.length < 6) { + const id = Math.floor(Math.random() * 151) + 1; + if (!pokemonIds.includes(id)) { + pokemonIds.push(id); + } + } + + const typeCount = 3 + Math.floor(Math.random() * 4); // 3-6 tipos + const typeDistribution = Array.from({ length: typeCount }, () => + ALL_TYPES[Math.floor(Math.random() * ALL_TYPES.length)] + ); + + const strategies: StrategyType[] = ['counter', 'balanced', 'aggressive', 'tank']; + const priorities: StatsPriority[] = ['balanced', 'offensive', 'defensive', 'speed']; + + return { + pokemonIds, + typeDistribution, + statsPriority: priorities[Math.floor(Math.random() * priorities.length)], + strategyType: strategies[Math.floor(Math.random() * strategies.length)] + }; +} + +// Criar genoma inicial +function createGenome(generation: number = 0, parents: [string, string] | null = null): TeamGenome { + return { + id: generateId(), + generation, + genes: generateRandomGenes(), + fitness: 0, + wins: 0, + losses: 0, + draws: 0, + battlesPlayed: 0, + parents, + createdAt: Date.now() + }; +} + +// Calcular fitness de um genoma +function calculateFitness(genome: TeamGenome, playerTypes?: string[]): number { + if (genome.battlesPlayed === 0) return 0; + + // 1. Win Rate (0-50 pontos) + const winRate = genome.wins / genome.battlesPlayed; + const winScore = winRate * 50; + + // 2. Variedade de Tipos (0-25 pontos) + const uniqueTypes = new Set(genome.genes.typeDistribution).size; + const varietyScore = (uniqueTypes / 18) * 25; + + // 3. Experiência (0-15 pontos) + const experienceScore = Math.min(15, (genome.battlesPlayed / 50) * 15); + + // 4. Bonus de Contra-Estratégia (0-10 pontos) + let counterBonus = 0; + if (playerTypes && playerTypes.length > 0) { + const countersPlayer = genome.genes.typeDistribution.some(type => + playerTypes.includes(type) + ); + counterBonus = countersPlayer ? 10 : 0; + } + + return Math.min(100, winScore + varietyScore + experienceScore + counterBonus); +} + +// Seleção por torneio +function tournamentSelection(population: TeamGenome[], tournamentSize: number): TeamGenome { + const tournament = []; + for (let i = 0; i < tournamentSize; i++) { + const randomIndex = Math.floor(Math.random() * population.length); + tournament.push(population[randomIndex]); + } + + tournament.sort((a, b) => b.fitness - a.fitness); + return tournament[0]; +} + +// Crossover (cruzamento) +function crossover(parent1: TeamGenome, parent2: TeamGenome): TeamGenome { + const genes: TeamGenes = { + // Combinar Pokemon IDs (3 de cada pai) + pokemonIds: [ + ...parent1.genes.pokemonIds.slice(0, 3), + ...parent2.genes.pokemonIds.slice(3, 6) + ], + + // Combinar tipos (mix dos dois pais) + typeDistribution: [ + ...parent1.genes.typeDistribution.slice(0, Math.floor(parent1.genes.typeDistribution.length / 2)), + ...parent2.genes.typeDistribution.slice(Math.floor(parent2.genes.typeDistribution.length / 2)) + ], + + // Herdar aleatoriamente + statsPriority: Math.random() > 0.5 ? parent1.genes.statsPriority : parent2.genes.statsPriority, + strategyType: Math.random() > 0.5 ? parent1.genes.strategyType : parent2.genes.strategyType + }; + + return { + id: generateId(), + generation: Math.max(parent1.generation, parent2.generation) + 1, + genes, + fitness: 0, + wins: 0, + losses: 0, + draws: 0, + battlesPlayed: 0, + parents: [parent1.id, parent2.id], + createdAt: Date.now() + }; +} + +// Mutação +function mutate(genome: TeamGenome, mutationRate: number): TeamGenome { + const mutated = { ...genome, genes: { ...genome.genes } }; + + // Mutação de Pokemon (trocar 1 Pokemon) + if (Math.random() < mutationRate) { + const index = Math.floor(Math.random() * 6); + let newId = Math.floor(Math.random() * 151) + 1; + // Garantir que não duplica + while (mutated.genes.pokemonIds.includes(newId)) { + newId = Math.floor(Math.random() * 151) + 1; + } + mutated.genes.pokemonIds[index] = newId; + } + + // Mutação de Tipos + if (Math.random() < mutationRate) { + const action = Math.random(); + if (action < 0.5 && mutated.genes.typeDistribution.length < 10) { + // Adicionar tipo + mutated.genes.typeDistribution.push( + ALL_TYPES[Math.floor(Math.random() * ALL_TYPES.length)] + ); + } else if (mutated.genes.typeDistribution.length > 2) { + // Remover tipo + mutated.genes.typeDistribution.splice( + Math.floor(Math.random() * mutated.genes.typeDistribution.length), 1 + ); + } + } + + // Mutação de Estratégia + if (Math.random() < mutationRate) { + const strategies: StrategyType[] = ['counter', 'balanced', 'aggressive', 'tank']; + mutated.genes.strategyType = strategies[Math.floor(Math.random() * 4)]; + } + + // Mutação de Prioridade de Stats + if (Math.random() < mutationRate) { + const priorities: StatsPriority[] = ['balanced', 'offensive', 'defensive', 'speed']; + mutated.genes.statsPriority = priorities[Math.floor(Math.random() * 4)]; + } + + return mutated; +} + +// Calcular diversidade genética +function calculateDiversity(population: TeamGenome[]): number { + const uniqueStrategies = new Set(population.map(g => g.genes.strategyType)).size; + const uniquePriorities = new Set(population.map(g => g.genes.statsPriority)).size; + + // Diversidade baseada em variação de estratégias e prioridades + return ((uniqueStrategies / 4) * 0.5 + (uniquePriorities / 4) * 0.5) * 100; +} + +export function useGeneticAI(config: Partial = {}) { + const fullConfig = { ...DEFAULT_CONFIG, ...config }; + + const [population, setPopulation] = useState({ + genomes: [], + currentGeneration: 0, + totalBattles: 0, + bestFitness: 0, + bestGenomeId: null, + generationHistory: [] + }); + + // Carregar população do localStorage + useEffect(() => { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + try { + const parsed = JSON.parse(stored); + setPopulation(parsed); + } catch (error) { + console.error('Erro ao carregar população genética:', error); + initializePopulation(); + } + } else { + initializePopulation(); + } + }, []); + + // Salvar população no localStorage + useEffect(() => { + if (population.genomes.length > 0) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(population)); + } + }, [population]); + + // Inicializar população + const initializePopulation = useCallback(() => { + const genomes = Array.from({ length: fullConfig.populationSize }, () => + createGenome(0) + ); + + setPopulation({ + genomes, + currentGeneration: 0, + totalBattles: 0, + bestFitness: 0, + bestGenomeId: null, + generationHistory: [] + }); + }, [fullConfig.populationSize]); + + // Registrar resultado de batalha + const recordBattle = useCallback((genomeId: string, result: 'win' | 'loss' | 'draw', playerTypes: string[] = []) => { + setPopulation(prev => { + const updated = { ...prev }; + const genome = updated.genomes.find(g => g.id === genomeId); + + if (!genome) return prev; + + genome.battlesPlayed++; + if (result === 'win') genome.wins++; + else if (result === 'loss') genome.losses++; + else genome.draws++; + + // Recalcular fitness + genome.fitness = calculateFitness(genome, playerTypes); + + // Atualizar melhor fitness + if (genome.fitness > updated.bestFitness) { + updated.bestFitness = genome.fitness; + updated.bestGenomeId = genome.id; + } + + updated.totalBattles++; + + return updated; + }); + }, []); + + // Evoluir para próxima geração + const evolveGeneration = useCallback((playerTypes: string[] = []) => { + setPopulation(prev => { + // Recalcular fitness de todos + const genomesWithFitness = prev.genomes.map(genome => ({ + ...genome, + fitness: calculateFitness(genome, playerTypes) + })); + + // Ordenar por fitness + genomesWithFitness.sort((a, b) => b.fitness - a.fitness); + + // Elitismo - manter os melhores + const eliteCount = Math.ceil(fullConfig.populationSize * fullConfig.elitePercentage); + const elite = genomesWithFitness.slice(0, eliteCount); + + // Criar nova geração + const newGenomes = [...elite]; + + while (newGenomes.length < fullConfig.populationSize) { + // Seleção + const parent1 = tournamentSelection(genomesWithFitness, fullConfig.tournamentSize); + const parent2 = tournamentSelection(genomesWithFitness, fullConfig.tournamentSize); + + // Crossover + let child: TeamGenome; + if (Math.random() < fullConfig.crossoverRate) { + child = crossover(parent1, parent2); + } else { + // Se não houver crossover, clone um dos pais + child = { ...parent1, id: generateId(), parents: [parent1.id, parent2.id] }; + } + + // Mutação + child = mutate(child, fullConfig.mutationRate); + + newGenomes.push(child); + } + + // Calcular estatísticas da geração + const avgFitness = genomesWithFitness.reduce((sum, g) => sum + g.fitness, 0) / genomesWithFitness.length; + const diversity = calculateDiversity(genomesWithFitness); + + const newGeneration = prev.currentGeneration + 1; + + return { + genomes: newGenomes, + currentGeneration: newGeneration, + totalBattles: prev.totalBattles, + bestFitness: Math.max(prev.bestFitness, genomesWithFitness[0].fitness), + bestGenomeId: genomesWithFitness[0].id, + generationHistory: [ + ...prev.generationHistory, + { + generation: newGeneration, + averageFitness: avgFitness, + bestFitness: genomesWithFitness[0].fitness, + diversity + } + ] + }; + }); + }, [fullConfig]); + + // Obter melhor genoma + const getBestGenome = useCallback((): TeamGenome | null => { + if (population.genomes.length === 0) return null; + + const sorted = [...population.genomes].sort((a, b) => + calculateFitness(b) - calculateFitness(a) + ); + + return sorted[0]; + }, [population.genomes]); + + // Obter genoma por ID + const getGenomeById = useCallback((id: string): TeamGenome | null => { + return population.genomes.find(g => g.id === id) || null; + }, [population.genomes]); + + // Resetar população + const resetPopulation = useCallback(() => { + localStorage.removeItem(STORAGE_KEY); + initializePopulation(); + }, [initializePopulation]); + + // Obter estatísticas + const getStats = useCallback(() => { + const totalWins = population.genomes.reduce((sum, g) => sum + g.wins, 0); + const totalLosses = population.genomes.reduce((sum, g) => sum + g.losses, 0); + const totalDraws = population.genomes.reduce((sum, g) => sum + g.draws, 0); + const avgFitness = population.genomes.length > 0 + ? population.genomes.reduce((sum, g) => sum + g.fitness, 0) / population.genomes.length + : 0; + + return { + generation: population.currentGeneration, + populationSize: population.genomes.length, + totalBattles: population.totalBattles, + totalWins, + totalLosses, + totalDraws, + bestFitness: population.bestFitness, + averageFitness: avgFitness, + diversity: calculateDiversity(population.genomes) + }; + }, [population]); + + return { + population, + recordBattle, + evolveGeneration, + getBestGenome, + getGenomeById, + resetPopulation, + getStats, + initializePopulation + }; +} diff --git a/app/games/pokemon/hooks/usePokemonData.ts b/app/games/pokemon/hooks/usePokemonData.ts new file mode 100644 index 0000000..dea9ed4 --- /dev/null +++ b/app/games/pokemon/hooks/usePokemonData.ts @@ -0,0 +1,282 @@ +import { useState, useEffect, useCallback, useMemo } from 'react'; +import { PokemonClient } from 'pokenode-ts'; +import type { Pokemon as PokeNodePokemon } from 'pokenode-ts'; +import type { Pokemon } from '../types/pokemon'; + +// Constantes +const MAX_POKEMON = 905; // Até a 8ª geração +const POKEMON_PER_PAGE = 48; // 6x8 grid +const POKEMON_CACHE_KEY = 'pokemon-data-cache'; +const CACHE_VERSION = 'v1'; // Incrementar quando mudar estrutura de dados + +// Gerações Pokemon (ranges de ID) +export const GENERATIONS = [ + { gen: 1, name: 'Kanto', start: 1, end: 151 }, + { gen: 2, name: 'Johto', start: 152, end: 251 }, + { gen: 3, name: 'Hoenn', start: 252, end: 386 }, + { gen: 4, name: 'Sinnoh', start: 387, end: 493 }, + { gen: 5, name: 'Unova', start: 494, end: 649 }, + { gen: 6, name: 'Kalos', start: 650, end: 721 }, + { gen: 7, name: 'Alola', start: 722, end: 809 }, + { gen: 8, name: 'Galar', start: 810, end: 905 } +]; + +// Função para obter geração de um Pokemon +export function getPokemonGeneration(id: number): { gen: number; name: string } { + const generation = GENERATIONS.find(g => id >= g.start && id <= g.end); + return generation ? { gen: generation.gen, name: generation.name } : { gen: 1, name: 'Kanto' }; +} + +// Função para converter Pokemon do pokenode-ts para nosso tipo +function convertPokemon(pkNodePokemon: PokeNodePokemon): Pokemon { + const pokemonId = pkNodePokemon.id; + const paddedId = String(pokemonId).padStart(3, '0'); + + // URLs de CDNs alternativos (SEM GitHub para evitar rate limit) + const pokemonComCdn = `https://assets.pokemon.com/assets/cms2/img/pokedex/detail/${paddedId}.png`; + + return { + id: pokemonId, + name: pkNodePokemon.name, + sprites: { + // Usar CDN Pokemon.com (não tem rate limit) + front_default: pokemonComCdn, + other: { + 'official-artwork': { + // Prioridade: Pokemon.com > Serebii > API original + front_default: pokemonComCdn + } + } + }, + types: pkNodePokemon.types, + stats: pkNodePokemon.stats, + abilities: pkNodePokemon.abilities, + height: pkNodePokemon.height, + weight: pkNodePokemon.weight + }; +} + +export function usePokemonData() { + const [allPokemon, setAllPokemon] = useState([]); + const [filteredPokemon, setFilteredPokemon] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + + // Criar instância do PokemonClient com cache + const api = useMemo(() => new PokemonClient(), []); + + // Carregar do cache localStorage + const loadFromCache = useCallback((): Pokemon[] | null => { + try { + const cached = localStorage.getItem(POKEMON_CACHE_KEY); + if (!cached) return null; + + const data = JSON.parse(cached); + if (data.version !== CACHE_VERSION) { + localStorage.removeItem(POKEMON_CACHE_KEY); + return null; + } + + console.log(`✅ ${data.pokemon.length} Pokemon carregados do cache!`); + return data.pokemon; + } catch (error) { + console.error('Erro ao carregar cache:', error); + return null; + } + }, []); + + // Salvar no cache localStorage + const saveToCache = useCallback((pokemon: Pokemon[]) => { + try { + const data = { + version: CACHE_VERSION, + pokemon, + timestamp: Date.now() + }; + localStorage.setItem(POKEMON_CACHE_KEY, JSON.stringify(data)); + console.log(`💾 ${pokemon.length} Pokemon salvos no cache!`); + } catch (error) { + console.error('Erro ao salvar cache:', error); + } + }, []); + + // Calcular páginas baseado nos Pokemon filtrados + useEffect(() => { + const total = Math.ceil(filteredPokemon.length / POKEMON_PER_PAGE); + setTotalPages(total); + }, [filteredPokemon]); + + // Pokemon da página atual + const paginatedPokemon = useMemo(() => { + const startIndex = (currentPage - 1) * POKEMON_PER_PAGE; + const endIndex = startIndex + POKEMON_PER_PAGE; + return filteredPokemon.slice(startIndex, endIndex); + }, [filteredPokemon, currentPage]); + + const loadInitialPokemon = useCallback(async () => { + // Tentar carregar do cache primeiro + const cachedPokemon = loadFromCache(); + if (cachedPokemon && cachedPokemon.length > 0) { + setAllPokemon(cachedPokemon); + setFilteredPokemon(cachedPokemon); + setCurrentPage(1); + return; + } + + // Se não tiver cache, carregar da API + setLoading(true); + setError(null); + + try { + // Carregar TODOS os Pokemon de uma vez (API tem cache) + // Fazer em lotes para evitar sobrecarga + const BATCH_SIZE = 100; + const allPokemonData: Pokemon[] = []; + + for (let batch = 0; batch < Math.ceil(MAX_POKEMON / BATCH_SIZE); batch++) { + const start = batch * BATCH_SIZE + 1; + const end = Math.min((batch + 1) * BATCH_SIZE, MAX_POKEMON); + + const batchPromises = Array.from({ length: end - start + 1 }, (_, i) => + api.getPokemonById(start + i) + .then(convertPokemon) + .catch(err => { + console.error(`Erro ao carregar Pokemon ${start + i}:`, err); + return null; + }) + ); + + const batchResults = (await Promise.all(batchPromises)).filter(Boolean) as Pokemon[]; + allPokemonData.push(...batchResults); + + // Atualizar progresso + console.log(`Carregados ${allPokemonData.length} de ${MAX_POKEMON} Pokemon...`); + } + + setAllPokemon(allPokemonData); + setFilteredPokemon(allPokemonData); + setCurrentPage(1); + saveToCache(allPokemonData); // Salvar no cache + console.log(`✅ Total de ${allPokemonData.length} Pokemon carregados!`); + } catch (error) { + console.error('Erro ao carregar Pokemon:', error); + setError('Erro ao carregar dados dos Pokemon. Tente novamente.'); + } finally { + setLoading(false); + } + }, [api, loadFromCache, saveToCache]); + + const fetchPokemon = useCallback(async (nameOrId: string | number): Promise => { + try { + const pokemon = await api.getPokemonByName(nameOrId.toString().toLowerCase()); + return convertPokemon(pokemon); + } catch (error) { + console.error(`Erro ao buscar ${nameOrId}:`, error); + return null; + } + }, [api]); + + const generateRandomTeam = useCallback(async (size: number = 6): Promise => { + const team: Pokemon[] = []; + + // Se já temos todos os Pokemon carregados, selecionar aleatoriamente deles + if (allPokemon.length >= MAX_POKEMON * 0.9) { // Pelo menos 90% carregados + const shuffled = [...allPokemon].sort(() => Math.random() - 0.5); + return shuffled.slice(0, size); + } + + // Fallback: buscar na API + const maxAttempts = 20; + for (let i = 0; i < size && team.length < maxAttempts; i++) { + const randomId = Math.floor(Math.random() * MAX_POKEMON) + 1; + const pokemon = await fetchPokemon(randomId.toString()); + + if (pokemon && !team.find(p => p.id === pokemon.id)) { + team.push(pokemon); + } else { + i--; + } + } + + return team; + }, [allPokemon, fetchPokemon]); + + const searchPokemon = useCallback((term: string) => { + setSearchTerm(term); + setCurrentPage(1); // Voltar para primeira página ao buscar + + if (!term.trim()) { + setFilteredPokemon(allPokemon); + return; + } + + const filtered = allPokemon.filter(pokemon => + pokemon.name.toLowerCase().includes(term.toLowerCase()) || + pokemon.types.some(type => type.type.name.toLowerCase().includes(term.toLowerCase())) + ); + + setFilteredPokemon(filtered); + }, [allPokemon]); + + const getPokemonById = useCallback(async (id: number): Promise => { + // Verificar se já está carregado + const existing = allPokemon.find(p => p.id === id); + if (existing) return existing; + + // Buscar na API se não estiver carregado + return await fetchPokemon(id.toString()); + }, [allPokemon, fetchPokemon]); + + // Simplificado - todos os Pokemon já estão carregados + const loadMorePokemon = useCallback(async () => { + // Não faz nada, todos já estão carregados + console.log('Todos os Pokemon já estão carregados!'); + }, []); + + const goToPage = useCallback((page: number) => { + if (page < 1 || page > totalPages) return; + setCurrentPage(page); + }, [totalPages]); + + const nextPage = useCallback(() => { + if (currentPage < totalPages) { + goToPage(currentPage + 1); + } + }, [currentPage, totalPages, goToPage]); + + const prevPage = useCallback(() => { + if (currentPage > 1) { + goToPage(currentPage - 1); + } + }, [currentPage, goToPage]); + + // Carregar Pokemon iniciais na montagem do componente + useEffect(() => { + if (allPokemon.length === 0) { + loadInitialPokemon(); + } + }, [loadInitialPokemon, allPokemon.length]); + + return { + allPokemon, + filteredPokemon, + paginatedPokemon, + loading, + error, + searchTerm, + currentPage, + totalPages, + loadInitialPokemon, + fetchPokemon, + generateRandomTeam, + searchPokemon, + getPokemonById, + loadMorePokemon, + goToPage, + nextPage, + prevPage + }; +} \ No newline at end of file diff --git a/app/games/pokemon/types/genetic.ts b/app/games/pokemon/types/genetic.ts new file mode 100644 index 0000000..e10e17b --- /dev/null +++ b/app/games/pokemon/types/genetic.ts @@ -0,0 +1,46 @@ +// Tipos para o Algoritmo Genético + +export type StrategyType = 'counter' | 'balanced' | 'aggressive' | 'tank'; +export type StatsPriority = 'balanced' | 'offensive' | 'defensive' | 'speed'; + +export interface TeamGenes { + pokemonIds: number[]; // IDs dos 6 Pokemon (1-151) + typeDistribution: string[]; // Tipos priorizados na seleção + statsPriority: StatsPriority; // Prioridade de stats + strategyType: StrategyType; // Tipo de estratégia +} + +export interface TeamGenome { + id: string; + generation: number; + genes: TeamGenes; + fitness: number; // Pontuação de sucesso (0-100) + wins: number; + losses: number; + draws: number; + battlesPlayed: number; + parents: [string, string] | null; // IDs dos genomas "pais" + createdAt: number; // timestamp +} + +export interface GeneticPopulation { + genomes: TeamGenome[]; + currentGeneration: number; + totalBattles: number; + bestFitness: number; + bestGenomeId: string | null; + generationHistory: { + generation: number; + averageFitness: number; + bestFitness: number; + diversity: number; + }[]; +} + +export interface GeneticConfig { + populationSize: number; // Tamanho da população (padrão: 20) + elitePercentage: number; // % mantida entre gerações (padrão: 0.2) + mutationRate: number; // Taxa de mutação (padrão: 0.15) + crossoverRate: number; // Taxa de crossover (padrão: 0.8) + tournamentSize: number; // Tamanho do torneio de seleção (padrão: 4) +} diff --git a/app/games/pokemon/types/pokemon.ts b/app/games/pokemon/types/pokemon.ts new file mode 100644 index 0000000..0315c1b --- /dev/null +++ b/app/games/pokemon/types/pokemon.ts @@ -0,0 +1,60 @@ +// Interfaces TypeScript para o Pokemon Battle AI +export interface Pokemon { + id: number; + name: string; + sprites: { + front_default: string; + other: { + 'official-artwork': { + front_default: string; + }; + }; + }; + types: Array<{ + type: { + name: string; + }; + }>; + stats: Array<{ + base_stat: number; + stat: { + name: string; + }; + }>; + abilities: Array<{ + ability: { + name: string; + }; + }>; + height: number; + weight: number; +} + +export interface TeamAnalysis { + weaknesses: string[]; + resistances: string[]; + recommendations: string[]; + overallStrength: number; +} + +export interface IndividualBattle { + playerPokemon: Pokemon; + aiPokemon: Pokemon; + winner: 'player' | 'ai'; + playerDamage: number; + aiDamage: number; + typeAdvantage: 'player' | 'ai' | 'neutral'; + reasoning: string; +} + +export interface BattleResult { + playerTeam: Pokemon[]; + aiTeam: Pokemon[]; + winner: 'player' | 'ai' | 'draw'; + analysis: string; + battles: IndividualBattle[]; + playerScore: number; + aiScore: number; +} + +export type ViewType = 'menu' | 'setup' | 'analysis' | 'battle' | 'automated-tests'; diff --git a/app/routes.ts b/app/routes.ts index 03d26bd..0f5af7e 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -3,6 +3,7 @@ import { type RouteConfig, index, route } from "@react-router/dev/routes"; export default [ index("routes/Home.tsx"), route("pythonFlappyBird", "games/PythonFlappyBird.tsx"), - route("game", "games/Game.tsx"), + route("/games/pokemon", "games/pokemon/PokemonBattleAI.tsx"), + route("/games/physics", "games/Game.tsx"), route("/games/pong-game-js", "games/pong-game-js/page.tsx") ] satisfies RouteConfig; diff --git a/app/routes/Home.tsx b/app/routes/Home.tsx index 006d873..fa3cc62 100644 --- a/app/routes/Home.tsx +++ b/app/routes/Home.tsx @@ -1,10 +1,19 @@ -import { Link } from "react-router-dom"; +import type { Route } from "./+types/Home"; +import { Link } from "react-router"; import "./Home.css"; +export function meta({}: Route.MetaArgs) { + return [ + { title: "GameHub - Jogos com Inteligência Artificial" }, + { name: "description", content: "Combinando Jogos com Inteligência Artificial" }, + ]; +} + type GameLink = { name: string; description: string; image: string; + icon?: string; // Emoji fallback quando não houver imagem url: string; }; @@ -16,11 +25,18 @@ const games: GameLink[] = [ image: "/PythonFlappyBird/FlappyBirdIcon.png", url: "/pythonFlappyBird", }, + { + name: "Pokemon Battle AI", + description: "Monte seu time e enfrente uma IA que evolui suas estratégias!", + image: "/Pokemon/PokemonIcon.svg", + url: "/games/pokemon", + }, { name: "Physics Sandbox", description: "Experimente a física com bolas e colisões!", image: "", - url: "/game", + icon: "🌟", + url: "/games/physics", }, { name: "Pong AI", @@ -43,7 +59,11 @@ export default function Home() { {games.map((game) => (
- {game.name} + {game.image ? ( + {game.name} + ) : ( +
{game.icon || "🎮"}
+ )}

{game.name}

{game.description}

@@ -58,4 +78,4 @@ export default function Home() {
); -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 4f5b916..ff2c57d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,15 +10,17 @@ "@pixi/react": "^8.0.3", "@react-router/node": "^7.7.1", "@react-router/serve": "^7.7.1", + "axios": "^1.12.2", + "axios-cache-interceptor": "^1.8.3", "highlight.js": "^11.11.1", "isbot": "^5.1.27", "marked-react": "^3.0.2", "matter-js": "^0.20.0", "pixi.js": "^8.12.0", + "pokenode-ts": "^1.20.0", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-router": "^7.9.4", - "react-router-dom": "^7.9.4" + "react-router": "^7.7.1" }, "devDependencies": { "@react-router/dev": "^7.7.1", @@ -1866,6 +1868,66 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.5.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.5.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.7", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.16", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz", @@ -2306,6 +2368,59 @@ "node": ">= 10.13.0" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.0.tgz", + "integrity": "sha512-zt40Pz4zcRXra9CVV31KeyofwiNvAbJ5B6YPz9pMJ+yOSLikvPT4Yi5LjfgjRa9CawVYBaD1JQzIVcIvBejKeA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios-cache-interceptor": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-1.8.3.tgz", + "integrity": "sha512-ifuSBoCEkVaiugg1UTjVuTdK+SjSOJ35pdv2OrzhRT3wDMr52QiayQxUqs7jd7GDsfPOjMcw3T3ek0TysbyZZw==", + "license": "MIT", + "dependencies": { + "cache-parser": "1.2.5", + "fast-defer": "1.1.8", + "object-code": "1.3.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/arthurfiorette/axios-cache-interceptor?sponsor=1" + }, + "peerDependencies": { + "axios": "^1" + } + }, "node_modules/b4a": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", @@ -3088,6 +3203,12 @@ "node": ">=8" } }, + "node_modules/cache-parser": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/cache-parser/-/cache-parser-1.2.5.tgz", + "integrity": "sha512-Md/4VhAHByQ9frQ15WD6LrMNiVw9AEl/J7vWIXw+sxT6fSOpbtt6LHTp76vy8+bOESPBO94117Hm2bIjlI7XjA==", + "license": "MIT" + }, "node_modules/cacheable-request": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", @@ -3121,6 +3242,25 @@ "node": ">=0.10.0" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -3479,6 +3619,18 @@ "text-hex": "0.0.x" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", @@ -4038,6 +4190,33 @@ } } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4412,6 +4591,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es5-ext": { "version": "0.10.64", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", @@ -4568,6 +4762,34 @@ "node": ">=0.8.0" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esniff/node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -5033,6 +5255,12 @@ "node": ">= 0.10" } }, + "node_modules/fast-defer": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/fast-defer/-/fast-defer-1.1.8.tgz", + "integrity": "sha512-lEJeOH5VL5R09j6AA0D4Uvq7AgsHw0dAImQQ+F3iSyHZuAxyQfWobsagGpTcOPvJr3urmKRHrs+Gs9hV+/Qm/Q==", + "license": "MIT" + }, "node_modules/fast-equals": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.2.tgz", @@ -5263,6 +5491,42 @@ "node": ">= 10.13.0" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -5299,6 +5563,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -6305,6 +6585,19 @@ "node": ">= 0.10" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbol-support-x": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", @@ -6338,6 +6631,21 @@ "node": "*" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -6808,6 +7116,19 @@ "node": ">=8" } }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -7036,6 +7357,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unc-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", @@ -8279,6 +8616,12 @@ "node": ">=0.10.0" } }, + "node_modules/object-code": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/object-code/-/object-code-1.3.3.tgz", + "integrity": "sha512-/Ds4Xd5xzrtUOJ+xJQ57iAy0BZsZltOHssnDgcZ8DOhgh41q1YJCnTPnWdWSLkNGNnxYzhYChjc5dgC9mEERCA==", + "license": "MIT" + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -8645,6 +8988,13 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT", + "optional": true + }, "node_modules/periscopic": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-4.0.2.tgz", @@ -8746,6 +9096,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pokenode-ts": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/pokenode-ts/-/pokenode-ts-1.20.0.tgz", + "integrity": "sha512-6MekrbiQc9nmaZJ5xpyhRSEMFo4xEsMuB7RR3EqfPvuXo/3StnH1p4brfIiIWDCcZvu7t9a0vjodiR4TnRdLEw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/Gabb-c/pokenode-ts?sponsor=1" + }, + "peerDependencies": { + "axios": "^1.4.0", + "axios-cache-interceptor": "^1.2.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -8880,6 +9256,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -9042,21 +9424,6 @@ } } }, - "node_modules/react-router-dom": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz", - "integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==", - "dependencies": { - "react-router": "7.9.4" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, "node_modules/react-router/node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", @@ -9508,6 +9875,24 @@ "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", "license": "MIT" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -10344,6 +10729,12 @@ "optional": true, "peer": true }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, "node_modules/type-fest": { "version": "3.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", @@ -11006,6 +11397,28 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "optional": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", diff --git a/package.json b/package.json index 24a3cbb..8bd9935 100644 --- a/package.json +++ b/package.json @@ -13,15 +13,17 @@ "@pixi/react": "^8.0.3", "@react-router/node": "^7.7.1", "@react-router/serve": "^7.7.1", + "axios": "^1.12.2", + "axios-cache-interceptor": "^1.8.3", "highlight.js": "^11.11.1", "isbot": "^5.1.27", "marked-react": "^3.0.2", "matter-js": "^0.20.0", "pixi.js": "^8.12.0", + "pokenode-ts": "^1.20.0", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-router": "^7.9.4", - "react-router-dom": "^7.9.4" + "react-router": "^7.7.1" }, "devDependencies": { "@react-router/dev": "^7.7.1", diff --git a/public/Pokemon/PokemonIcon.svg b/public/Pokemon/PokemonIcon.svg new file mode 100644 index 0000000..a830351 --- /dev/null +++ b/public/Pokemon/PokemonIcon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + +