diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e7a7209
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,490 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from `dotnet new gitignore`
+
+# dotenv files
+.env
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# Tye
+.tye/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# SQLite
+*.db
+*.db3
+*.sqlite
+*.sqlite3
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
+.idea
+
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# Mac bundle stuff
+*.dmg
+*.app
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# Vim temporary swap files
+*.swp
diff --git a/MySolution.sln b/MySolution.sln
new file mode 100644
index 0000000..ff632e1
--- /dev/null
+++ b/MySolution.sln
@@ -0,0 +1,40 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api", "src\Api\Api.csproj", "{4A967206-0565-4E15-BE8D-DC76C8470FA4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "src\Core\Core.csproj", "{CD553728-FD2F-45D1-8826-137A14A06321}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "src\Infrastructure\Infrastructure.csproj", "{003ABA45-4054-40A1-AD23-B1D195637534}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application", "src\Application\Application.csproj", "{FB477A56-5919-4F07-9BA6-5E9C6E8ACD8F}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {4A967206-0565-4E15-BE8D-DC76C8470FA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4A967206-0565-4E15-BE8D-DC76C8470FA4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4A967206-0565-4E15-BE8D-DC76C8470FA4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4A967206-0565-4E15-BE8D-DC76C8470FA4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CD553728-FD2F-45D1-8826-137A14A06321}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CD553728-FD2F-45D1-8826-137A14A06321}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CD553728-FD2F-45D1-8826-137A14A06321}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CD553728-FD2F-45D1-8826-137A14A06321}.Release|Any CPU.Build.0 = Release|Any CPU
+ {003ABA45-4054-40A1-AD23-B1D195637534}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {003ABA45-4054-40A1-AD23-B1D195637534}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {003ABA45-4054-40A1-AD23-B1D195637534}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {003ABA45-4054-40A1-AD23-B1D195637534}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FB477A56-5919-4F07-9BA6-5E9C6E8ACD8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FB477A56-5919-4F07-9BA6-5E9C6E8ACD8F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FB477A56-5919-4F07-9BA6-5E9C6E8ACD8F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FB477A56-5919-4F07-9BA6-5E9C6E8ACD8F}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
\ No newline at end of file
diff --git a/app/__init__.py b/app/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/app/__pycache__/__init__.cpython-312.pyc b/app/__pycache__/__init__.cpython-312.pyc
deleted file mode 100644
index 941e56c..0000000
Binary files a/app/__pycache__/__init__.cpython-312.pyc and /dev/null differ
diff --git a/app/__pycache__/crud.cpython-312.pyc b/app/__pycache__/crud.cpython-312.pyc
deleted file mode 100644
index 238508c..0000000
Binary files a/app/__pycache__/crud.cpython-312.pyc and /dev/null differ
diff --git a/app/__pycache__/database.cpython-312.pyc b/app/__pycache__/database.cpython-312.pyc
deleted file mode 100644
index 2bb5adf..0000000
Binary files a/app/__pycache__/database.cpython-312.pyc and /dev/null differ
diff --git a/app/__pycache__/initial_data.cpython-312.pyc b/app/__pycache__/initial_data.cpython-312.pyc
deleted file mode 100644
index f9c5761..0000000
Binary files a/app/__pycache__/initial_data.cpython-312.pyc and /dev/null differ
diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc
deleted file mode 100644
index 82b7403..0000000
Binary files a/app/__pycache__/main.cpython-312.pyc and /dev/null differ
diff --git a/app/__pycache__/models.cpython-312.pyc b/app/__pycache__/models.cpython-312.pyc
deleted file mode 100644
index 8721acf..0000000
Binary files a/app/__pycache__/models.cpython-312.pyc and /dev/null differ
diff --git a/app/__pycache__/schemas.cpython-312.pyc b/app/__pycache__/schemas.cpython-312.pyc
deleted file mode 100644
index 68955f8..0000000
Binary files a/app/__pycache__/schemas.cpython-312.pyc and /dev/null differ
diff --git a/app/create_tables.py b/app/create_tables.py
deleted file mode 100644
index c09dd6c..0000000
--- a/app/create_tables.py
+++ /dev/null
@@ -1,114 +0,0 @@
-# create_tables.py
-import asyncio
-import inspect
-from typing import List, Type
-from sqlalchemy import inspect as sql_inspect
-from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker
-from sqlalchemy.orm import DeclarativeBase
-from app.database import engine, Base
-from app import models # Asegúrate que todos los modelos estén importados aquí
-
-async def get_all_models() -> List[Type[DeclarativeBase]]:
- """
- Detecta automáticamente todos los modelos SQLAlchemy en el módulo models.
- Devuelve una lista de clases de modelo válidas.
- """
- model_classes = []
-
- print("\n🔍 Buscando modelos en:", models.__file__)
-
- for name, obj in inspect.getmembers(models):
- try:
- if (inspect.isclass(obj) and
- issubclass(obj, Base) and
- obj != Base and
- hasattr(obj, '__tablename__')):
- print(f"✅ Modelo válido detectado: {name} (tabla: {obj.__tablename__})")
- model_classes.append(obj)
- except TypeError:
- continue
-
- if not model_classes:
- print("⚠️ ¡No se encontraron modelos válidos! Verifica que:")
- print("- Cada modelo herede de Base (de app.database)")
- print("- Cada modelo tenga definido __tablename__")
- print("- Los modelos estén importados en app/models/__init__.py")
-
- return model_classes
-
-async def verify_database_connection(engine: AsyncEngine):
- """Verifica que la conexión a la base de datos funciona"""
- try:
- async with engine.connect() as conn:
- await conn.execute("SELECT 1")
- print("✓ Conexión a la base de datos verificada")
- return True
- except Exception as e:
- print(f"✖ Error de conexión a la base de datos: {str(e)}")
- return False
-
-async def create_all_tables(engine: AsyncEngine):
- """Crea todas las tablas definidas en los modelos"""
- async with engine.begin() as conn:
- print("\n🛠 Creando todas las tablas...")
- await conn.run_sync(Base.metadata.create_all)
- print("✓ Todas las tablas creadas exitosamente")
-
-async def drop_all_tables(engine: AsyncEngine):
- """Elimina todas las tablas (¡CUIDADO! Pérdida de datos)"""
- async with engine.begin() as conn:
- print("\n⚠️ ELIMINANDO TODAS LAS TABLAS...")
- await conn.run_sync(Base.metadata.drop_all)
- print("✓ Todas las tablas eliminadas")
-
-async def show_existing_tables(engine: AsyncEngine):
- """Muestra las tablas existentes en la base de datos"""
- async with engine.connect() as conn:
- inspector = sql_inspect(conn)
- tables = await conn.run_sync(lambda sync_conn: inspector.get_table_names())
- print("\n📊 Tablas existentes en la base de datos:")
- for table in tables:
- print(f" - {table}")
- return tables
-
-async def main():
- print("\n=== 🚀 Script de Creación de Tablas SQLAlchemy ===")
-
- # 1. Verificar conexión a la base de datos
- if not await verify_database_connection(engine):
- return
-
- # 2. Obtener todos los modelos
- all_models = await get_all_models()
- if not all_models:
- return
-
- # 3. Mostrar tablas existentes
- existing_tables = await show_existing_tables(engine)
-
- # 4. Menú de opciones
- print("\n🔧 Opciones disponibles:")
- print("1. Crear tablas faltantes (sin afectar existentes)")
- print("2. Recrear TODAS las tablas (¡elimina datos existentes!)")
- print("3. Solo mostrar información (no hacer cambios)")
-
- choice = input("\nSeleccione una opción (1-3): ").strip()
-
- if choice == "1":
- await create_all_tables(engine)
- elif choice == "2":
- confirm = input("⚠️ ¿ESTÁ SEGURO? Esto eliminará TODAS las tablas y datos. (s/N): ")
- if confirm.lower() == 's':
- await drop_all_tables(engine)
- await create_all_tables(engine)
- else:
- print("Operación cancelada")
- else:
- print("Solo mostrando información (no se hicieron cambios)")
-
- # Mostrar estado final
- await show_existing_tables(engine)
- print("\n✔ Proceso completado")
-
-if __name__ == "__main__":
- asyncio.run(main())
\ No newline at end of file
diff --git a/app/crud.py b/app/crud.py
deleted file mode 100644
index 8ced448..0000000
--- a/app/crud.py
+++ /dev/null
@@ -1,610 +0,0 @@
-# Importaciones de SQLAlchemy para operaciones asíncronas
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy.future import select
-from sqlalchemy import func, or_
-from sqlalchemy.orm import selectinload, joinedload
-from datetime import datetime, timezone
-from typing import List, Optional
-
-# Importaciones de SQLAlchemy para operaciones síncronas
-from sqlalchemy.orm import Session
-from app.models import Admin
-from app.schemas import AdminCreate
-from app.routers.auth import get_password_hash
-
-# Importaciones de nuestros módulos internos
-from . import schemas # Esquemas Pydantic para validación de datos
-from . import models # Modelos de la base de datos
-
-# Funciones CRUD para administradores
-async def get_admin_by_username(db: AsyncSession, username: str):
- result = await db.execute(select(Admin).where(Admin.username == username))
- return result.scalars().first()
-
-async def create_admin(db: AsyncSession, admin_data: dict):
- db_admin = Admin(**admin_data)
- db.add(db_admin)
- await db.commit()
- await db.refresh(db_admin)
- return db_admin
-
-
-# Función auxiliar para parsear movimientos de Pokémon
-def parse_moves(moves):
- """
- Convierte una cadena de movimientos en formato "{move1,move2,...}" a una lista.
-
- Args:
- moves: Puede ser una cadena con formato especial o una lista.
-
- Returns:
- Lista de movimientos limpios.
- """
- if isinstance(moves, str):
- cleaned = moves.strip("{}").replace('\"', '"').split(',')
- return [m.strip().strip('"') for m in cleaned if m.strip()]
- return moves
-
-## ------------------------- CRUD para Pokémon ------------------------- ##
-
-async def get_pokemon(db: AsyncSession, pokemon_id: int):
- """
- Obtiene un Pokémon por su ID.
-
- Args:
- db: Sesión de base de datos asíncrona.
- pokemon_id: ID del Pokémon a buscar.
-
- Returns:
- El Pokémon encontrado o None si no existe.
- """
- result = await db.execute(
- select(models.Pokemon)
- .where(models.Pokemon.id == pokemon_id)
- )
- return result.scalar_one_or_none()
-
-async def get_pokemons(
- db: AsyncSession,
- name: Optional[str] = None
-):
- """
- Obtiene una lista paginada de Pokémon con opción de filtrado por nombre.
-
- Args:
- db: Sesión de base de datos.
- skip: Número de registros a saltar (paginación).
- limit: Máximo número de registros a devolver.
- name: Filtro opcional por nombre (búsqueda parcial case insensitive).
-
- Returns:
- Lista de Pokémon.
- """
- query = select(models.Pokemon)
-
- if name:
- query = query.where(
- func.lower(models.Pokemon.name).contains(func.lower(name)))
- result = await db.execute(query)
- return result.scalars().all()
-
-async def get_pokemon_by_name(db: AsyncSession, name: str):
- """Busca un Pokémon por nombre exacto (case insensitive)"""
- result = await db.execute(
- select(models.Pokemon)
- .where(func.lower(models.Pokemon.name) == func.lower(name))
- )
- return result.scalars().first()
-
-async def get_pokemons_by_names(db: AsyncSession, names: List[str]):
- """Busca Pokémon por lista de nombres exactos"""
- result = await db.execute(
- select(models.Pokemon)
- .where(models.Pokemon.name.in_(names))
- )
- return result.scalars().all()
-
-async def search_pokemons_by_name(
- db: AsyncSession,
- name: str,
-) -> List[models.Pokemon]:
- """
- Búsqueda avanzada por nombre con:
- - Coincidencias parciales en cualquier parte del nombre
- - Case insensitive
- - Ordenamiento por mejor coincidencia
-
- Args:
- db: Sesión de base de datos.
- name: Término de búsqueda.
- skip: Registros a saltar.
- limit: Máximo de resultados.
-
- Returns:
- Lista de Pokémon ordenados por relevancia.
- """
- query = (
- select(models.Pokemon)
- .where(
- or_(
- func.lower(models.Pokemon.name).contains(func.lower(name)),
- models.Pokemon.name.ilike(f"%{name}%")
- )
- )
- .order_by(
- # Primero los que empiezan con el término de búsqueda
- func.lower(models.Pokemon.name).startswith(func.lower(name)).desc(),
- # Luego por longitud del nombre (más corto primero)
- func.length(models.Pokemon.name)
- )
-
- )
-
- result = await db.execute(query)
- return result.scalars().all()
-
-async def create_pokemon(db: AsyncSession, pokemon: schemas.PokemonCreate):
- """
- Crea un nuevo Pokémon en la base de datos.
-
- Args:
- db: Sesión de base de datos.
- pokemon: Datos del Pokémon a crear.
-
- Returns:
- El Pokémon creado con su ID asignado.
- """
- data = pokemon.dict()
- data["moves"] = parse_moves(data.get("moves"))
- db_pokemon = models.Pokemon(**data)
- db.add(db_pokemon)
- await db.commit()
- await db.refresh(db_pokemon)
- return db_pokemon
-
-async def update_pokemon(
- db: AsyncSession,
- pokemon_id: int,
- pokemon: schemas.PokemonCreate
-):
- """
- Actualiza los datos de un Pokémon existente.
-
- Args:
- db: Sesión de base de datos.
- pokemon_id: ID del Pokémon a actualizar.
- pokemon: Nuevos datos del Pokémon.
-
- Returns:
- El Pokémon actualizado o None si no existe.
- """
- db_pokemon = await get_pokemon(db, pokemon_id)
- if db_pokemon:
- for key, value in pokemon.dict().items():
- if key == "moves":
- value = parse_moves(value)
- setattr(db_pokemon, key, value)
- await db.commit()
- await db.refresh(db_pokemon)
- return db_pokemon
-
-async def delete_pokemon(db: AsyncSession, pokemon_id: int):
- """
- Elimina un Pokémon de la base de datos.
-
- Args:
- db: Sesión de base de datos.
- pokemon_id: ID del Pokémon a eliminar.
-
- Returns:
- El Pokémon eliminado o None si no existe.
- """
- db_pokemon = await get_pokemon(db, pokemon_id)
- if db_pokemon:
- await db.delete(db_pokemon)
- await db.commit()
- return db_pokemon
-
-async def flexible_pokemon_search(
- db: AsyncSession,
- search_term: str,
- skip: int = 0,
- limit: int = 10
-) -> List[models.Pokemon]:
- """
- Búsqueda flexible que intenta encontrar coincidencias incluso con pequeños errores.
- Primero intenta búsqueda exacta, luego parcial si no hay resultados.
-
- Args:
- db: Sesión de base de datos.
- search_term: Término de búsqueda.
- skip: Registros a saltar.
- limit: Máximo de resultados.
-
- Returns:
- Lista de Pokémon que coinciden con el término.
- """
- # Primero intenta búsqueda exacta
- exact_match = await db.execute(
- select(models.Pokemon)
- .where(func.lower(models.Pokemon.name) == func.lower(search_term))
- )
- exact_match = exact_match.scalars().first()
-
- if exact_match:
- return [exact_match]
-
- # Si no hay coincidencia exacta, busca coincidencias parciales
- return await search_pokemons_by_name(db, search_term, skip, limit)
-
-## ------------------------- CRUD para Entrenadores ------------------------- ##
-
-async def get_trainer(db: AsyncSession, trainer_id: int):
- """
- Obtiene un entrenador por su ID.
-
- Args:
- db: Sesión de base de datos.
- trainer_id: ID del entrenador.
-
- Returns:
- El entrenador encontrado o None si no existe.
- """
- result = await db.execute(
- select(models.Trainer)
- .where(models.Trainer.id == trainer_id)
- )
- return result.scalar_one_or_none()
-
-async def get_trainers(db: AsyncSession, skip: int = 0, limit: int = 10):
- """
- Obtiene una lista paginada de entrenadores.
-
- Args:
- db: Sesión de base de datos.
- skip: Registros a saltar.
- limit: Máximo de resultados.
-
- Returns:
- Lista de entrenadores.
- """
- result = await db.execute(
- select(models.Trainer)
- .offset(skip)
- .limit(limit)
- )
- return result.scalars().all()
-
-async def create_trainer(db: AsyncSession, trainer: schemas.TrainerCreate):
- """
- Crea un nuevo entrenador.
-
- Args:
- db: Sesión de base de datos.
- trainer: Datos del entrenador.
-
- Returns:
- El entrenador creado con su ID asignado.
- """
- db_trainer = models.Trainer(**trainer.dict())
- db.add(db_trainer)
- await db.commit()
- await db.refresh(db_trainer)
- return db_trainer
-
-async def update_trainer(
- db: AsyncSession,
- trainer_id: int,
- trainer: schemas.TrainerCreate
-):
- """
- Actualiza los datos de un entrenador.
-
- Args:
- db: Sesión de base de datos.
- trainer_id: ID del entrenador.
- trainer: Nuevos datos del entrenador.
-
- Returns:
- El entrenador actualizado o None si no existe.
- """
- db_trainer = await get_trainer(db, trainer_id)
- if db_trainer:
- for key, value in trainer.dict().items():
- setattr(db_trainer, key, value)
- await db.commit()
- await db.refresh(db_trainer)
- return db_trainer
-
-async def delete_trainer(db: AsyncSession, trainer_id: int):
- """
- Elimina un entrenador.
-
- Args:
- db: Sesión de base de datos.
- trainer_id: ID del entrenador.
-
- Returns:
- El entrenador eliminado o None si no existe.
- """
- db_trainer = await get_trainer(db, trainer_id)
- if db_trainer:
- await db.delete(db_trainer)
- await db.commit()
- return db_trainer
-
-## ------------------------- Relación Pokémon-Entrenadores ------------------------- ##
-
-async def add_pokemon_to_trainer(
- db: AsyncSession,
- trainer_pokemon: schemas.TrainerPokemonCreate
-):
- """
- Agrega un Pokémon a la colección de un entrenador.
-
- Args:
- db: Sesión de base de datos.
- trainer_pokemon: Datos de la relación.
-
- Returns:
- La relación creada.
- """
- db_trainer_pokemon = models.TrainerPokemon(**trainer_pokemon.dict())
- db.add(db_trainer_pokemon)
- await db.commit()
- await db.refresh(db_trainer_pokemon)
- return db_trainer_pokemon
-
-async def get_trainer_pokemons(db: AsyncSession, trainer_id: int):
- """
- Obtiene todos los Pokémon de un entrenador específico.
-
- Args:
- db: Sesión de base de datos.
- trainer_id: ID del entrenador.
-
- Returns:
- Lista de Pokémon del entrenador.
- """
- result = await db.execute(
- select(models.TrainerPokemon)
- .where(models.TrainerPokemon.trainer_id == trainer_id)
- .options(selectinload(models.TrainerPokemon.pokemon))
- )
- return result.scalars().all()
-
-async def remove_pokemon_from_trainer(
- db: AsyncSession,
- trainer_id: int,
- pokemon_id: int
-):
- """
- Elimina un Pokémon de la colección de un entrenador.
-
- Args:
- db: Sesión de base de datos.
- trainer_id: ID del entrenador.
- pokemon_id: ID del Pokémon a remover.
-
- Returns:
- La relación eliminada o None si no existía.
- """
- result = await db.execute(
- select(models.TrainerPokemon)
- .where(
- models.TrainerPokemon.trainer_id == trainer_id,
- models.TrainerPokemon.pokemon_id == pokemon_id
- )
- )
- db_trainer_pokemon = result.scalar_one_or_none()
- if db_trainer_pokemon:
- await db.delete(db_trainer_pokemon)
- await db.commit()
- return db_trainer_pokemon
-
-## ------------------------- Funciones de Batalla ------------------------- ##
-
-async def update_pokemon_hp_remaining(
- db: AsyncSession,
- pokemon_id: int,
- battle_id: int,
- hp_remaining: int
-):
- """
- Actualiza los HP restantes de un Pokémon en una batalla específica.
-
- Args:
- db: Sesión de base de datos.
- pokemon_id: ID del Pokémon.
- battle_id: ID de la batalla.
- hp_remaining: Nuevo valor de HP.
-
- Returns:
- El registro actualizado o None si no existe.
- """
- result = await db.execute(
- select(models.BattlePokemon)
- .where(
- models.BattlePokemon.pokemon_id == pokemon_id,
- models.BattlePokemon.battle_id == battle_id
- )
- )
- db_battle_pokemon = result.scalar_one_or_none()
-
- if db_battle_pokemon:
- db_battle_pokemon.hp_remaining = hp_remaining
- await db.commit()
- await db.refresh(db_battle_pokemon)
- return db_battle_pokemon
- return None
-
-## ------------------------- CRUD para Batallas ------------------------- ##
-
-async def create_battle(db: AsyncSession, battle: schemas.BattleCreate):
- """
- Crea un nuevo registro de batalla.
-
- Args:
- db: Sesión de base de datos.
- battle: Datos de la batalla.
-
- Returns:
- La batalla creada.
-
- Raises:
- ValueError: Si algún entrenador no existe.
- """
- opponent = await get_trainer(db, battle.opponent_id)
- trainer = await get_trainer(db, battle.trainer_id)
-
- if not opponent or not trainer:
- raise ValueError("Entrenador no encontrado")
-
- db_battle = models.Battle(
- trainer_id=battle.trainer_id,
- opponent_name=opponent.name,
- winner=None,
- date=datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
- )
-
- db.add(db_battle)
- await db.commit()
- await db.refresh(db_battle)
- return db_battle
-
-async def get_battle(db: AsyncSession, battle_id: int):
- """
- Obtiene una batalla por su ID con información del entrenador.
-
- Args:
- db: Sesión de base de datos.
- battle_id: ID de la batalla.
-
- Returns:
- La batalla encontrada con datos extendidos o None.
- """
- result = await db.execute(
- select(models.Battle)
- .where(models.Battle.id == battle_id)
- .options(joinedload(models.Battle.trainer))
- )
- battle = result.scalar_one_or_none()
-
- if battle and hasattr(battle, 'trainer'):
- battle.trainer_name = battle.trainer.name
-
- return battle
-
-async def get_battles(db: AsyncSession, skip: int = 0, limit: int = 10):
- """
- Obtiene una lista paginada de batallas con información extendida.
-
- Args:
- db: Sesión de base de datos.
- skip: Registros a saltar.
- limit: Máximo de resultados.
-
- Returns:
- Lista de batallas con datos extendidos.
- """
- result = await db.execute(
- select(models.Battle)
- .options(joinedload(models.Battle.trainer))
- .offset(skip)
- .limit(limit)
- )
- battles = result.scalars().all()
-
- for battle in battles:
- if not hasattr(battle, 'trainer_name') and hasattr(battle, 'trainer'):
- battle.trainer_name = battle.trainer.name
-
- return battles
-
-async def update_battle(
- db: AsyncSession,
- battle_id: int,
- battle_update: schemas.BattleUpdate
-):
- """
- Actualiza los datos de una batalla (como el ganador).
-
- Args:
- db: Sesión de base de datos.
- battle_id: ID de la batalla.
- battle_update: Datos a actualizar.
-
- Returns:
- La batalla actualizada o None si no existe.
- """
- db_battle = await get_battle(db, battle_id)
- if db_battle:
- for key, value in battle_update.dict(exclude_unset=True).items():
- setattr(db_battle, key, value)
- await db.commit()
- await db.refresh(db_battle)
- return db_battle
-
-async def get_battle_with_pokemons(db: AsyncSession, battle_id: int):
- """
- Obtiene una batalla con todos los Pokémon participantes.
-
- Args:
- db: Sesión de base de datos.
- battle_id: ID de la batalla.
-
- Returns:
- La batalla con datos extendidos y lista de Pokémon o None.
- """
- result = await db.execute(
- select(models.Battle)
- .where(models.Battle.id == battle_id)
- .options(
- joinedload(models.Battle.trainer),
- selectinload(models.Battle.pokemons).selectinload(models.BattlePokemon.pokemon)
- )
- )
- battle = result.scalar_one_or_none()
-
- if battle and hasattr(battle, 'trainer'):
- battle.trainer_name = battle.trainer.name
-
- return battle
-
-async def add_pokemon_to_battle(
- db: AsyncSession,
- battle_pokemon: schemas.BattlePokemonCreate
-):
- """
- Agrega un Pokémon a una batalla específica.
-
- Args:
- db: Sesión de base de datos.
- battle_pokemon: Datos de la relación.
-
- Returns:
- La relación creada.
- """
- db_battle_pokemon = models.BattlePokemon(**battle_pokemon.dict())
- db.add(db_battle_pokemon)
- await db.commit()
- await db.refresh(db_battle_pokemon)
- return db_battle_pokemon
-
-async def get_battle_pokemons(db: AsyncSession, battle_id: int):
- """
- Obtiene todos los Pokémon participantes en una batalla.
-
- Args:
- db: Sesión de base de datos.
- battle_id: ID de la batalla.
-
- Returns:
- Lista de Pokémon en la batalla.
- """
- result = await db.execute(
- select(models.BattlePokemon)
- .where(models.BattlePokemon.battle_id == battle_id)
- .options(selectinload(models.BattlePokemon.pokemon))
- )
- return result.scalars().all()
\ No newline at end of file
diff --git a/app/database.py b/app/database.py
deleted file mode 100644
index ed4169f..0000000
--- a/app/database.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
-from sqlalchemy.orm import declarative_base
-import os
-
-DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://isaac:20861681@localhost:5432/pykedex")
-
-engine = create_async_engine(
- DATABASE_URL,
- pool_pre_ping=True, # Verifica conexiones antes de usarlas
- echo=True # Muestra logs SQL (útil para desarrollo)
-)
-
-AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
-Base = declarative_base()
-
-async def get_db():
- """
- Asynchronous generator that provides a database session for use within a context.
-
- Yields:
- An active asynchronous SQLAlchemy session. The session is automatically closed
- after use, even if an exception occurs.
- """
- async with AsyncSessionLocal() as db:
- try:
- yield db
- finally:
- await db.close()
\ No newline at end of file
diff --git a/app/initial_data.py b/app/initial_data.py
deleted file mode 100644
index cad7131..0000000
--- a/app/initial_data.py
+++ /dev/null
@@ -1,32 +0,0 @@
-from sqlalchemy.ext.asyncio import AsyncSession
-from app.crud import create_admin, get_admin_by_username
-from app.schemas import AdminCreate
-from app.database import AsyncSessionLocal
-from app.routers.auth import get_password_hash
-
-async def create_initial_admin():
- async with AsyncSessionLocal() as db:
- try:
- # Verificar si ya existe el admin inicial
- existing_admin = await get_admin_by_username(db, "romez")
- if existing_admin:
- print("✔ El administrador inicial ya existe")
- return False
-
- # Crear el admin inicial
- admin_data = {
- "username": "romez",
- "email": "romez@example.com",
- "hashed_password": get_password_hash("20861681"),
- "is_superadmin": True,
- "is_active": True
- }
-
- await create_admin(db, admin_data)
- await db.commit()
- print("✔ Administrador inicial creado exitosamente")
- return True
- except Exception as e:
- await db.rollback()
- print(f"✖ Error al crear administrador inicial: {str(e)}")
- raise
\ No newline at end of file
diff --git a/app/main.py b/app/main.py
deleted file mode 100644
index cc85e7a..0000000
--- a/app/main.py
+++ /dev/null
@@ -1,220 +0,0 @@
-# app/main.py
-from fastapi import FastAPI, Request, Depends, HTTPException
-from fastapi.middleware.cors import CORSMiddleware
-from fastapi.responses import JSONResponse
-from slowapi import Limiter
-from slowapi.util import get_remote_address
-from slowapi.errors import RateLimitExceeded
-from slowapi.middleware import SlowAPIMiddleware
-from fastapi.openapi.utils import get_openapi
-from fastapi.security import HTTPBearer
-from sqlalchemy import inspect
-from sqlalchemy.ext.asyncio import AsyncEngine
-
-# Importaciones de tu aplicación
-from app.database import engine, Base
-from app.routers import pokemon, trainer, battle, auth, admin
-from app.initial_data import create_initial_admin
-from app.models import *
-
-# --------------------------------------------------
-# CONFIGURACIÓN DEL RATE LIMITER
-# --------------------------------------------------
-limiter = Limiter(key_func=get_remote_address)
-
-# --------------------------------------------------
-# CONFIGURACIÓN DE LA APLICACIÓN
-# --------------------------------------------------
-bearer_scheme = HTTPBearer()
-
-app = FastAPI(
- title="PyKedex API",
- description="API para el sistema de gestión de Pokémon y batallas",
- version="1.0.0",
- contact={
- "name": "Equipo de Desarrollo DruidCode By ROMEZ",
- "email": "isaac.rod33@gmail.com"
- },
- license_info={
- "name": "MIT",
- }
-)
-
-# --------------------------------------------------
-# FUNCIONES DE INICIALIZACIÓN
-# --------------------------------------------------
-async def check_tables_exist(engine: AsyncEngine) -> bool:
- """
- Asynchronously checks if all required tables exist in the database.
-
- Args:
- engine: The SQLAlchemy asynchronous engine connected to the target database.
-
- Returns:
- True if all required tables ('admins', 'pokemons', 'trainers', 'trainer_pokemons', 'battles') are present; otherwise, False.
- """
- async with engine.connect() as conn:
- # Usamos run_sync para ejecutar la inspección de forma síncrona
- existing_tables = await conn.run_sync(
- lambda sync_conn: inspect(sync_conn).get_table_names()
- )
- required_tables = {"admins", "pokemons", "trainers", "trainer_pokemons", "battles"}
- return all(table in existing_tables for table in required_tables)
-
-async def initialize_database():
- """
- Asynchronously initializes the database by creating required tables if they do not exist.
-
- Raises:
- Exception: If an error occurs during database initialization.
- """
- try:
- tables_exist = await check_tables_exist(engine)
- if not tables_exist:
- print("⚠ Tablas no encontradas, creando estructura de base de datos...")
- async with engine.begin() as conn:
- await conn.run_sync(Base.metadata.create_all)
- print("✔ Base de datos inicializada correctamente")
- else:
- print("✔ Tablas ya existen en la base de datos")
- except Exception as e:
- print(f"✖ Error al inicializar la base de datos: {e}")
- raise
-
-# --------------------------------------------------
-# EVENTOS DE LA APLICACIÓN
-# --------------------------------------------------
-@app.on_event("startup")
-async def startup_event():
- """
- Runs application startup tasks to ensure database schema and initial admin user exist.
-
- This function is triggered on application startup. It initializes the database schema if necessary and ensures that an initial admin user is present.
- """
- await initialize_database()
- await create_initial_admin()
- print("✔ Verificado/Creado administrador inicial")
-
-# --------------------------------------------------
-# MIDDLEWARES
-# --------------------------------------------------
-app.add_middleware(SlowAPIMiddleware)
-app.state.limiter = limiter
-
-app.add_middleware(
- CORSMiddleware,
- allow_origins=["*"],
- allow_credentials=True,
- allow_methods=["*"],
- allow_headers=["*"],
-)
-
-# --------------------------------------------------
-# MANEJADORES DE ERRORES
-# --------------------------------------------------
-@app.exception_handler(RateLimitExceeded)
-async def rate_limit_exceeded_handler(request: Request, exc: RateLimitExceeded):
- """
- Handles rate limit exceedance by returning a 429 response with an explanatory message.
-
- Returns:
- JSONResponse indicating the client has exceeded the allowed request rate, including a
- 'Retry-After' header set to 60 seconds.
- """
- return JSONResponse(
- status_code=429,
- content={
- "message": "Ha superado el límite de 10 solicitudes por minuto. Por favor espere.",
- "success": False,
- "error": "RateLimitExceeded"
- },
- headers={
- "Retry-After": str(60)
- }
- )
-
-@app.exception_handler(HTTPException)
-async def http_exception_handler(request: Request, exc: HTTPException):
- """
- Handles HTTPException errors by returning a structured JSON response with error details.
-
- Returns:
- JSONResponse: A response containing the error message, success flag, and error type.
- """
- return JSONResponse(
- status_code=exc.status_code,
- content={
- "message": exc.detail,
- "success": False,
- "error": type(exc).__name__
- }
- )
-
-# --------------------------------------------------
-# RUTAS PRINCIPALES
-# --------------------------------------------------
-@app.get("/", tags=["Inicio"])
-async def root():
- """
- Returns a welcome message and basic information about the PyKedex API.
-
- The response includes the API version, documentation path, and main available routes.
- """
- return {
- "message": "¡Bienvenido a PyKedex!",
- "documentación": "/docs",
- "versión": "1.0.0",
- "rutas_disponibles": {
- "pokemons": "/api/v1/pokemons",
- "entrenadores": "/api/v1/entrenadores",
- "batallas": "/api/v1/batallas"
- }
- }
-
-# --------------------------------------------------
-# INCLUSIÓN DE ROUTERS
-# --------------------------------------------------
-# Configuración de rate limits para cada router
-routers_config = [
- (admin.router, "10/minute", "admin", "/api/v1/admin", "admin"),
- (auth.router, "10/minute", "auth", "/api/v1/auth", "auth"),
- (pokemon.router, "10/minute", "pokemon", "/api/v1/pokemons", "Pokémon"),
- (trainer.router, "10/minute", "trainer", "/api/v1/entrenadores", "Entrenadores"),
- (battle.router, "10/minute", "battle", "/api/v1/batallas", "Batallas"),
-]
-
-for router, rate_limit, scope, prefix, tags in routers_config:
- router.dependencies = [Depends(limiter.shared_limit(rate_limit, scope=scope))]
- app.include_router(router, prefix=prefix, tags=[tags])
-
-# --------------------------------------------------
-# CONFIGURACIÓN OPENAPI
-# --------------------------------------------------
-def custom_openapi():
- """
- Generates and caches a custom OpenAPI schema with JWT Bearer authentication.
-
- Adds a "BearerAuth" security scheme to the OpenAPI components and returns the schema, caching it for future use.
- """
- if app.openapi_schema:
- return app.openapi_schema
-
- openapi_schema = get_openapi(
- title=app.title,
- version=app.version,
- description=app.description,
- routes=app.routes,
- )
-
- openapi_schema["components"]["securitySchemes"] = {
- "BearerAuth": {
- "type": "http",
- "scheme": "bearer",
- "bearerFormat": "JWT"
- }
- }
-
- app.openapi_schema = openapi_schema
- return app.openapi_schema
-
-app.openapi = custom_openapi
\ No newline at end of file
diff --git a/app/models.py b/app/models.py
deleted file mode 100644
index c2d570d..0000000
--- a/app/models.py
+++ /dev/null
@@ -1,136 +0,0 @@
-from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, ARRAY
-from sqlalchemy.orm import relationship
-from datetime import datetime, timezone
-from sqlalchemy import DateTime
-from .database import Base
-
-
-class Admin(Base):
- __tablename__ = "admins"
-
- id = Column(Integer, primary_key=True, index=True)
- username = Column(String, unique=True, index=True)
- hashed_password = Column(String)
- email = Column(String, unique=True, index=True)
- is_active = Column(Boolean, default=True)
- is_superadmin = Column(Boolean, default=False)
-
-class Pokemon(Base):
- """
- Modelo que representa un Pokémon en la base de datos.
- Contiene todos los atributos y estadísticas de un Pokémon.
- """
- __tablename__ = "pokemons"
-
- id = Column(Integer, primary_key=True, index=True) # ID único
- name = Column(String(100), nullable=False) # Nombre (requerido)
- element = Column(String(50)) # Tipo(s) elemental (ej: "Fuego/Volador")
- hp = Column(Integer) # Puntos de salud base
- attack = Column(Integer) # Ataque físico
- defense = Column(Integer) # Defensa física
- special_attack = Column(Integer) # Ataque especial
- special_defense = Column(Integer) # Defensa especial
- speed = Column(Integer) # Velocidad
- moves = Column(ARRAY(String)) # Lista de movimientos disponibles
- current_hp = Column(Integer) # HP actual (para combates en curso)
- level = Column(Integer, default=1) # Nivel del Pokémon (nuevo campo)
-
- # Relación muchos-a-muchos con entrenadores (a través de TrainerPokemon)
- trainer_pokemons = relationship("TrainerPokemon", back_populates="pokemon")
-
- # Relación con las batallas en las que participó
- battles = relationship("BattlePokemon", back_populates="pokemon")
-
-class Trainer(Base):
- """
- Modelo que representa un Entrenador Pokémon.
- Contiene la información básica del entrenador.
- """
- __tablename__ = "trainers"
-
- id = Column(Integer, primary_key=True, index=True) # ID único
- name = Column(String(100), nullable=False) # Nombre (requerido)
- email = Column(String(100), nullable=False, unique=True) # Email (único)
- level = Column(Integer, default=1) # Nivel (default 1)
-
- # Relación muchos-a-muchos con Pokémon (a través de TrainerPokemon)
- pokemons = relationship("TrainerPokemon", back_populates="trainer")
-
- # Relación con las batallas que ha participado
- battles = relationship("Battle", back_populates="trainer")
-
-class TrainerPokemon(Base):
- """
- Modelo de relación muchos-a-muchos entre Entrenadores y Pokémon.
- Representa qué Pokémon pertenecen a qué entrenadores.
- """
- __tablename__ = "trainer_pokemons"
-
- # Clave primaria compuesta
- trainer_id = Column(
- Integer,
- ForeignKey("trainers.id", ondelete="CASCADE"), # Eliminación en cascada
- primary_key=True
- )
- pokemon_id = Column(
- Integer,
- ForeignKey("pokemons.id", ondelete="CASCADE"), # Eliminación en cascada
- primary_key=True
- )
- is_shiny = Column(Boolean, default=False) # Indica si es una variante shiny
-
- # Relaciones con Pokémon y Entrenador
- pokemon = relationship("Pokemon", back_populates="trainer_pokemons")
- trainer = relationship("Trainer", back_populates="pokemons")
-
-class ShinyPokemon(Base):
- """
- Modelo para registrar Pokémon shiny especiales.
- Puede usarse para llevar registro de shiny encontrados.
- """
- __tablename__ = "shiny_pokemons"
-
- id = Column(Integer, primary_key=True, index=True) # ID único
- pokemon_id = Column(
- Integer,
- ForeignKey("pokemons.id", ondelete="CASCADE") # Eliminación en cascada
- )
-
-class Battle(Base):
- """
- Modelo que representa una batalla Pokémon.
- Registra el resultado y participantes de cada combate.
- """
- __tablename__ = "battles"
-
- id = Column(Integer, primary_key=True, index=True) # ID único
- trainer_id = Column(Integer, ForeignKey("trainers.id")) # Entrenador que inició
- opponent_name = Column(String(100), nullable=False) # Nombre del oponente
- winner = Column(String(100)) # Nombre del ganador (puede ser null para empates)
- date = Column(
- String,
- default=datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S') # Fecha auto-generada
- )
-
- # Relación con el entrenador que inició la batalla
- trainer = relationship("Trainer", back_populates="battles")
-
- # Relación con los Pokémon que participaron
- pokemons = relationship("BattlePokemon", back_populates="battle")
-
-class BattlePokemon(Base):
- """
- Modelo de relación muchos-a-muchos entre Batallas y Pokémon.
- Registra qué Pokémon participaron en cada batalla y su estado.
- """
- __tablename__ = "battle_pokemons"
-
- id = Column(Integer, primary_key=True, index=True) # ID único
- battle_id = Column(Integer, ForeignKey("battles.id")) # ID de la batalla
- pokemon_id = Column(Integer, ForeignKey("pokemons.id")) # ID del Pokémon
- hp_remaining = Column(Integer) # HP restante al final de la batalla
- participated = Column(Boolean, default=False) # Si participó efectivamente
-
- # Relaciones con Batalla y Pokémon
- battle = relationship("Battle", back_populates="pokemons")
- pokemon = relationship("Pokemon", back_populates="battles")
\ No newline at end of file
diff --git a/app/routers/__init__.py b/app/routers/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/app/routers/__pycache__/__init__.cpython-312.pyc b/app/routers/__pycache__/__init__.cpython-312.pyc
deleted file mode 100644
index 5bbd0e8..0000000
Binary files a/app/routers/__pycache__/__init__.cpython-312.pyc and /dev/null differ
diff --git a/app/routers/__pycache__/admin.cpython-312.pyc b/app/routers/__pycache__/admin.cpython-312.pyc
deleted file mode 100644
index 48fa889..0000000
Binary files a/app/routers/__pycache__/admin.cpython-312.pyc and /dev/null differ
diff --git a/app/routers/__pycache__/auth.cpython-312.pyc b/app/routers/__pycache__/auth.cpython-312.pyc
deleted file mode 100644
index a2f7d66..0000000
Binary files a/app/routers/__pycache__/auth.cpython-312.pyc and /dev/null differ
diff --git a/app/routers/__pycache__/battle.cpython-312.pyc b/app/routers/__pycache__/battle.cpython-312.pyc
deleted file mode 100644
index dd4dab5..0000000
Binary files a/app/routers/__pycache__/battle.cpython-312.pyc and /dev/null differ
diff --git a/app/routers/__pycache__/pokemon.cpython-312.pyc b/app/routers/__pycache__/pokemon.cpython-312.pyc
deleted file mode 100644
index d57d7c0..0000000
Binary files a/app/routers/__pycache__/pokemon.cpython-312.pyc and /dev/null differ
diff --git a/app/routers/__pycache__/trainer.cpython-312.pyc b/app/routers/__pycache__/trainer.cpython-312.pyc
deleted file mode 100644
index bc2c0a4..0000000
Binary files a/app/routers/__pycache__/trainer.cpython-312.pyc and /dev/null differ
diff --git a/app/routers/admin.py b/app/routers/admin.py
deleted file mode 100644
index 1f10caa..0000000
--- a/app/routers/admin.py
+++ /dev/null
@@ -1,66 +0,0 @@
-from fastapi import APIRouter, Depends, HTTPException, status
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy.exc import IntegrityError
-from app.schemas import AdminCreate, Admin
-from app.crud import create_admin, get_admin_by_username
-from app.routers.auth import get_current_superadmin, get_password_hash
-from app.database import get_db
-
-router = APIRouter(tags=["admin"])
-
-@router.post("/", status_code=status.HTTP_201_CREATED)
-async def crear_nuevo_admin(
- admin: AdminCreate,
- db: AsyncSession = Depends(get_db),
- current_admin: Admin = Depends(get_current_superadmin)
-):
- """
- Crea un nuevo administrador (requiere privilegios de superadmin)
- """
- # Verificar si el username ya existe
- existing_admin = await get_admin_by_username(db, admin.username)
- if existing_admin:
- raise HTTPException(
- status_code=status.HTTP_400_BAD_REQUEST,
- detail="El nombre de usuario ya está registrado"
- )
-
- try:
- hashed_password = get_password_hash(admin.password)
- db_admin_data = {
- "username": admin.username,
- "email": admin.email,
- "hashed_password": hashed_password,
- "is_superadmin": False,
- "is_active": True
- }
- db_admin = await create_admin(db, db_admin_data)
-
- # Convertir el objeto SQLAlchemy a diccionario correctamente
- admin_data = {
- "id": db_admin.id,
- "username": db_admin.username,
- "email": db_admin.email,
- "message": "creado exitosamente"
- }
-
- return admin_data
-
- except IntegrityError as e:
- await db.rollback()
- if "ix_admins_email" in str(e):
- raise HTTPException(
- status_code=status.HTTP_400_BAD_REQUEST,
- detail="El email ya está registrado"
- )
- raise HTTPException(
- status_code=status.HTTP_400_BAD_REQUEST,
- detail="El nombre de usuario ya está registrado"
- )
-
- except Exception as e:
- await db.rollback()
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Error interno del servidor: {str(e)}"
- )
\ No newline at end of file
diff --git a/app/routers/auth.py b/app/routers/auth.py
deleted file mode 100644
index 41f696f..0000000
--- a/app/routers/auth.py
+++ /dev/null
@@ -1,120 +0,0 @@
-from datetime import datetime, timedelta
-from typing import Annotated, Optional
-
-from fastapi import APIRouter, Depends, HTTPException, status
-from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
-from jose import JWTError, jwt
-from passlib.context import CryptContext
-from pydantic import BaseModel
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from app.models import Admin
-from app.database import get_db
-
-# Configuración (debes mover esto a variables de entorno)
-SECRET_KEY = "PykedexSecretKey"
-ALGORITHM = "HS256"
-ACCESS_TOKEN_EXPIRE_MINUTES = 30 # Duración del token en minutos
-
-router = APIRouter(tags=["auth"])
-
-# Modelos
-class Token(BaseModel):
- access_token: str
- token_type: str
-
-class LoginForm(BaseModel):
- username: str
- password: str
-
-# Utilidades
-pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
-bearer_scheme = HTTPBearer()
-
-# Función para verificar contraseña
-def verify_password(plain_password: str, hashed_password: str):
- return pwd_context.verify(plain_password, hashed_password)
-
-# Función para generar hash de contraseña
-def get_password_hash(password: str):
- return pwd_context.hash(password)
-
-# Función para crear token de acceso
-def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
- to_encode = data.copy()
- if expires_delta:
- expire = datetime.utcnow() + expires_delta
- else:
- expire = datetime.utcnow() + timedelta(minutes=15)
- to_encode.update({"exp": expire})
- encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
- return encoded_jwt
-
-# Función para autenticar admin
-async def authenticate_admin(
- username: str,
- password: str,
- db: AsyncSession = Depends(get_db)
-):
- from app.crud import get_admin_by_username
- admin = await get_admin_by_username(db, username)
- if not admin:
- return False
- if not verify_password(password, admin.hashed_password):
- return False
- return admin
-
-# Endpoint para obtener token
-@router.post("/token", response_model=Token)
-async def login_for_access_token(
- form_data: LoginForm,
- db: AsyncSession = Depends(get_db)
-):
- admin = await authenticate_admin(form_data.username, form_data.password, db)
- if not admin:
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Incorrect username or password",
- headers={"WWW-Authenticate": "Bearer"},
- )
- access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
- access_token = create_access_token(
- data={"sub": admin.username}, expires_delta=access_token_expires
- )
- return {"access_token": access_token, "token_type": "bearer"}
-
-# Dependencia para obtener el admin actual
-async def get_current_admin(
- credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
- db: AsyncSession = Depends(get_db)
-):
- credentials_exception = HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Could not validate credentials",
- headers={"WWW-Authenticate": "Bearer"},
- )
- try:
- token = credentials.credentials
- payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
- username: str = payload.get("sub")
- if username is None:
- raise credentials_exception
-
- from app.crud import get_admin_by_username
- admin = await get_admin_by_username(db, username=username)
- if admin is None:
- raise credentials_exception
- return admin
- except JWTError:
- raise credentials_exception
-
-# Dependencia para obtener admin superusuario
-async def get_current_superadmin(
- current_admin: Admin = Depends(get_current_admin)
-):
- if not current_admin.is_superadmin:
- raise HTTPException(
- status_code=status.HTTP_403_FORBIDDEN,
- detail="The user doesn't have enough privileges"
- )
- return current_admin
\ No newline at end of file
diff --git a/app/routers/battle.py b/app/routers/battle.py
deleted file mode 100644
index 95e737b..0000000
--- a/app/routers/battle.py
+++ /dev/null
@@ -1,938 +0,0 @@
-from datetime import datetime
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from typing import List, Optional
-import random
-from .. import schemas, crud, models
-from ..database import get_db
-
-router = APIRouter(
- tags=["Batallas"] # Agrupación para la documentación Swagger/OpenAPI
-)
-
-# --------------------------------------------------
-# SISTEMA DE TIPOS POKÉMON (EFECTIVIDADES) Y FRASES
-# --------------------------------------------------
-
-TYPE_ADVANTAGES = {
- # Efectividades (2x de daño)
- "Planta": {"Agua": 2.0, "Roca": 2.0, "Tierra": 2.0},
- "Fuego": {"Planta": 2.0, "Bicho": 2.0, "Hielo": 2.0, "Acero": 2.0},
- "Agua": {"Fuego": 2.0, "Roca": 2.0, "Tierra": 2.0},
- "Eléctrico": {"Agua": 2.0, "Volador": 2.0},
- "Hielo": {"Planta": 2.0, "Tierra": 2.0, "Volador": 2.0, "Dragón": 2.0},
- "Lucha": {"Normal": 2.0, "Hielo": 2.0, "Roca": 2.0, "Siniestro": 2.0, "Acero": 2.0},
- "Veneno": {"Planta": 2.0, "Hada": 2.0},
- "Tierra": {"Fuego": 2.0, "Eléctrico": 2.0, "Veneno": 2.0, "Roca": 2.0, "Acero": 2.0},
- "Volador": {"Planta": 2.0, "Lucha": 2.0, "Bicho": 2.0},
- "Psíquico": {"Lucha": 2.0, "Veneno": 2.0},
- "Bicho": {"Planta": 2.0, "Psíquico": 2.0, "Siniestro": 2.0},
- "Roca": {"Fuego": 2.0, "Hielo": 2.0, "Volador": 2.0, "Bicho": 2.0},
- "Fantasma": {"Psíquico": 2.0, "Fantasma": 2.0},
- "Dragón": {"Dragón": 2.0},
- "Siniestro": {"Psíquico": 2.0, "Fantasma": 2.0},
- "Acero": {"Hielo": 2.0, "Roca": 2.0, "Hada": 2.0},
- "Hada": {"Lucha": 2.0, "Dragón": 2.0, "Siniestro": 2.0},
-
- # Debilidades (0.5x de daño)
- "Planta": {"Fuego": 0.5, "Volador": 0.5, "Bicho": 0.5, "Hielo": 0.5, "Veneno": 0.5},
- "Fuego": {"Agua": 0.5, "Roca": 0.5, "Tierra": 0.5},
- "Agua": {"Eléctrico": 0.5, "Planta": 0.5},
- "Eléctrico": {"Tierra": 0.5},
- "Hielo": {"Fuego": 0.5, "Lucha": 0.5, "Roca": 0.5, "Acero": 0.5},
- "Lucha": {"Volador": 0.5, "Psíquico": 0.5, "Hada": 0.5},
- "Veneno": {"Tierra": 0.5, "Psíquico": 0.5},
- "Tierra": {"Agua": 0.5, "Planta": 0.5, "Hielo": 0.5},
- "Volador": {"Eléctrico": 0.5, "Hielo": 0.5, "Roca": 0.5},
- "Psíquico": {"Bicho": 0.5, "Fantasma": 0.5, "Siniestro": 0.5},
- "Bicho": {"Fuego": 0.5, "Volador": 0.5, "Roca": 0.5},
- "Roca": {"Agua": 0.5, "Planta": 0.5, "Lucha": 0.5, "Tierra": 0.5, "Acero": 0.5},
- "Fantasma": {"Fantasma": 0.5, "Siniestro": 0.5},
- "Dragón": {"Acero": 0.5},
- "Siniestro": {"Lucha": 0.5, "Bicho": 0.5, "Hada": 0.5},
- "Acero": {"Fuego": 0.5, "Lucha": 0.5, "Tierra": 0.5},
- "Hada": {"Veneno": 0.5, "Acero": 0.5}
-}
-
-BATTLE_COMMENTS = [
- "¡El combate está muy reñido! Ambos Pokémon dan lo mejor de sí.",
- "¡Qué intensidad! Ningún Pokémon quiere ceder terreno.",
- "La batalla se prolonga... ¿Quién tendrá más resistencia?",
- "¡Increíble intercambio de golpes! Esto es digno de un campeonato.",
- "La estrategia de ambos entrenadores es impresionante.",
- "¡El público está al borde de sus asientos con esta batalla!",
- "Ninguno de los dos quiere perder, ¡esto es épico!",
- "¡Qué demostración de habilidad por ambos lados!",
- "La tensión es palpable en este enfrentamiento.",
- "¡Están igualados! Cualquier cosa puede pasar."
-]
-
-TRAINER_DIALOGUES = {
- "winning": [
- "¡Así se hace! Sigue así, {pokemon}!",
- "¡Perfecto! ¡Ese ataque fue directo!",
- "¡Vamos {pokemon}, tú puedes!",
- "¡Esa es la estrategia que practicamos!",
- "¡Excelente ejecución, {pokemon}!",
- "¡Justo como lo planeamos!",
- "¡No le des tregua, {pokemon}!"
- ],
- "losing": [
- "¡Aguanta {pokemon}, no te rindas!",
- "¡Contraataca ahora, {pokemon}!",
- "¡Cuidado con ese movimiento!",
- "¡No es momento de flaquear!",
- "¡Concéntrate, {pokemon}!",
- "¡Puedes superarlo, {pokemon}!",
- "¡No te dejes intimidar!"
- ],
- "critical": [
- "¡Sí! ¡Un golpe crítico!",
- "¡Directo al blanco! ¡Buen trabajo {pokemon}!",
- "¡Ese entrenamiento está dando frutos!",
- "¡Justo en el punto débil!",
- "¡Impacto perfecto, {pokemon}!"
- ],
- "resisted": [
- "¡Bien esquivado {pokemon}!",
- "¡Casi te alcanza! ¡Sigue así!",
- "¡Ese ataque no te afectará fácilmente!",
- "¡Buena defensa, {pokemon}!",
- "¡No tan rápido, rival!"
- ],
- "start": [
- "¡{pokemon}, yo te elijo!",
- "¡Es tu momento, {pokemon}!",
- "¡Confío en ti, {pokemon}!",
- "¡Hagámoslo, {pokemon}!",
- "¡A dar lo mejor, {pokemon}!"
- ],
- "x4_damage": [
- "¡Un golpe devastador! ¡Es muy efectivo!",
- "¡Impacto crítico! ¡El tipo es perfecto!",
- "¡{pokemon} es extremadamente vulnerable a este ataque!",
- "¡Golpe maestro! ¡El daño es enorme!",
- "¡{pokemon} no puede soportar este tipo de ataque!"
- ]
-}
-
-def get_type_multiplier(attacker_type: str, defender_type: str) -> float:
- """Calcula el multiplicador de daño basado en los tipos, incluyendo 4x de daño"""
- multiplier = 1.0
- attacker_types = attacker_type.split("/")
- defender_types = defender_type.split("/")
-
- for atk_type in attacker_types:
- for def_type in defender_types:
- if atk_type in TYPE_ADVANTAGES and def_type in TYPE_ADVANTAGES[atk_type]:
- multiplier *= TYPE_ADVANTAGES[atk_type][def_type]
-
- # Mensaje especial para daño 4x
- if multiplier >= 4.0:
- return multiplier
- elif multiplier <= 0.25:
- return multiplier
- else:
- return multiplier
-
-# --------------------------------------------------
-# MECÁNICAS DE COMBATE MEJORADAS CON NIVELES
-# --------------------------------------------------
-
-def get_random_attack(pokemon: schemas.Pokemon) -> str:
- """Obtiene un ataque aleatorio de los movimientos del Pokémon con 30% de probabilidad de ataque especial"""
- if pokemon.moves and len(pokemon.moves) > 0:
- if random.random() < 0.3 and hasattr(pokemon, 'special_attack'):
- return f"{random.choice(pokemon.moves)} (Especial)"
- return random.choice(pokemon.moves)
- return random.choice(["Placaje", "Arañazo", "Gruñido"])
-
-def calculate_damage(
- attacker: schemas.Pokemon,
- defender: schemas.Pokemon,
- attack_used: str,
- attacker_level: int = 1,
- defender_level: int = 1
-) -> tuple:
- """
- Calcula el daño de un ataque considerando:
- - Ataque/Defensa base o Ataque Especial/Defensa Especial
- - Ventaja de tipo (incluyendo 4x de daño)
- - Nivel del Pokémon
- - Aleatoriedad
- Retorna: (daño, es_crítico, es_especial, es_resistido)
- """
- # Determinar si es un ataque especial
- is_special = "especial" in attack_used.lower()
-
- # Daño base con variación aleatoria
- if is_special and hasattr(attacker, 'special_attack'):
- base_damage = random.randint(5, min(attacker.special_attack, 100) or 20)
- defense_stat = defender.special_defense if hasattr(defender, 'special_defense') else (defender.defense or 10)
- else:
- base_damage = random.randint(5, min(attacker.attack, 100) or 15)
- defense_stat = defender.defense or 10
-
- # Bonus por nivel del Pokémon (1-2% por nivel)
- attacker_level_bonus = 1 + (attacker_level * 0.02)
- base_damage = int(base_damage * attacker_level_bonus)
-
- # Reducción por defensa y nivel del defensor (1-1.5% por nivel)
- defense_level_reduction = max(1, defense_stat / (10 * (1 + defender_level * 0.015)))
-
- # Multiplicador por tipo (puede ser 4x o 0.25x)
- type_multiplier = get_type_multiplier(attacker.element or "Normal", defender.element or "Normal")
-
- # Daño final
- damage = max(1, int((base_damage * type_multiplier) / defense_level_reduction))
-
- # Bonus adicional por ataque especial
- if is_special:
- damage = int(damage * 1.3) # 30% más de daño para ataques especiales
-
- # Posibilidad de golpe crítico (10% base + 0.1% por nivel del atacante)
- critical_chance = 0.1 + (attacker_level * 0.001)
- is_critical = random.random() < critical_chance
- if is_critical:
- damage = int(damage * 1.5)
-
- # Probabilidad de resistencia (0.1% por nivel del defensor)
- resist_chance = defender_level * 0.001
- if random.random() < resist_chance:
- damage = max(1, int(damage * 0.7)) # Reduce el daño en 30%
- return damage, is_critical, is_special, True # Último parámetro indica resistencia
-
- return damage, is_critical, is_special, False
-
-def determine_first_attacker(pokemon1: schemas.Pokemon, pokemon2: schemas.Pokemon) -> tuple:
- """
- Determina qué Pokémon ataca primero basado en la velocidad y nivel.
- Retorna: (attacker, defender, is_pokemon1_first)
- """
- # Velocidad base + 1% por nivel
- speed1 = (pokemon1.speed if hasattr(pokemon1, 'speed') and pokemon1.speed else 50) * (1 + (pokemon1.level if hasattr(pokemon1, 'level') else 1) * 0.01)
- speed2 = (pokemon2.speed if hasattr(pokemon2, 'speed') and pokemon2.speed else 50) * (1 + (pokemon2.level if hasattr(pokemon2, 'level') else 1) * 0.01)
-
- if speed1 == speed2:
- # Empate en velocidad, se decide al azar
- if random.random() < 0.5:
- return pokemon1, pokemon2, True
- else:
- return pokemon2, pokemon1, False
- elif speed1 > speed2:
- return pokemon1, pokemon2, True
- else:
- return pokemon2, pokemon1, False
-
-def calculate_level_up(pokemon: schemas.Pokemon, battle_duration: int, is_winner: bool) -> int:
- """
- Calcula cuántos niveles sube un Pokémon después de una batalla
- - battle_duration: Número de turnos que duró la batalla
- - is_winner: Si el Pokémon ganó la batalla
- """
- if pokemon.level >= 100: # Nivel máximo
- return 0
-
- base_exp = 10
- duration_bonus = min(battle_duration * 0.2, 20) # Máximo 20 de bonus por duración
- winner_bonus = 15 if is_winner else 0
-
- total_exp = base_exp + duration_bonus + winner_bonus
- levels_gained = min(int(total_exp / 20), 2) # Máximo 2 niveles por batalla
-
- return levels_gained
-
-# --------------------------------------------------
-# SIMULACIÓN DE BATALLA INDIVIDUAL (MEJORADA)
-# --------------------------------------------------
-
-async def simulate_single_battle(
- db: AsyncSession,
- trainer: schemas.Trainer,
- opponent: schemas.Trainer,
- trainer_pokemon: schemas.Pokemon,
- opponent_pokemon: schemas.Pokemon,
- previous_trainer_hp: Optional[int] = None
-) -> dict:
- """
- Simula una sola batalla entre dos Pokémon con comentarios y diálogos mejorados.
- """
- # Inicialización de HP
- max_trainer_hp = trainer_pokemon.hp or 100
- max_opponent_hp = opponent_pokemon.hp or 100
- trainer_hp = previous_trainer_hp if previous_trainer_hp is not None else (trainer_pokemon.current_hp if trainer_pokemon.current_hp is not None else max_trainer_hp)
- opponent_hp = opponent_pokemon.current_hp if opponent_pokemon.current_hp is not None else max_opponent_hp
-
- # Obtener niveles de los Pokémon
- trainer_pokemon_level = trainer_pokemon.level if hasattr(trainer_pokemon, 'level') else 1
- opponent_pokemon_level = opponent_pokemon.level if hasattr(opponent_pokemon, 'level') else 1
-
- # Registro de batalla
- battle_log = []
- last_trainer_attack = ""
- last_opponent_attack = ""
- turn_count = 0
-
- # Diálogo inicial aleatorio
- trainer_dialogue = random.choice(TRAINER_DIALOGUES["start"]).format(pokemon=trainer_pokemon.name)
- opponent_dialogue = random.choice(TRAINER_DIALOGUES["start"]).format(pokemon=opponent_pokemon.name)
- battle_log.append(f"🗣️ {trainer.name}: {trainer_dialogue}")
- battle_log.append(f"🗣️ {opponent.name}: {opponent_dialogue}")
-
- # Determinar quién ataca primero
- first_attacker, first_defender, is_trainer_first = determine_first_attacker(
- trainer_pokemon, opponent_pokemon
- )
-
- # Mostrar velocidades
- speed1 = (trainer_pokemon.speed if hasattr(trainer_pokemon, 'speed') else 50)
- speed2 = (opponent_pokemon.speed if hasattr(opponent_pokemon, 'speed') else 50)
-
- battle_log.append(
- f"⚡ Velocidades: {trainer_pokemon.name} ({speed1}) vs {opponent_pokemon.name} ({speed2})"
- )
-
- battle_log.append(
- f"⚔️ ¡Comienza la batalla entre {trainer_pokemon.name} (Nv. {trainer_pokemon_level}, HP: {trainer_hp}/{max_trainer_hp}), "
- f"vs {opponent_pokemon.name} (Nv. {opponent_pokemon_level}, HP: {opponent_hp}/{max_opponent_hp})!"
- )
-
- if is_trainer_first:
- battle_log.append(f"⚡ ¡{trainer_pokemon.name} es más rápido y ataca primero!")
- else:
- battle_log.append(f"⚡ ¡{opponent_pokemon.name} es más rápido y ataca primero!")
-
- # Sistema de turnos
- while True:
- turn_count += 1
-
- # Comentario aleatorio cada 5 turnos
- if turn_count % 5 == 0 and turn_count > 0:
- battle_log.append(f"💬 {random.choice(BATTLE_COMMENTS)}")
-
- # Verificar si la batalla ha terminado
- if trainer_hp <= 0 or opponent_hp <= 0:
- break
-
- # Turno del primer atacante
- if is_trainer_first:
- attacker_name = trainer.name
- defender_name = opponent.name
- attacker_pokemon = trainer_pokemon
- defender_pokemon = opponent_pokemon
- attacker_level = trainer_pokemon_level
- defender_level = opponent_pokemon_level
- else:
- attacker_name = opponent.name
- defender_name = trainer.name
- attacker_pokemon = opponent_pokemon
- defender_pokemon = trainer_pokemon
- attacker_level = opponent_pokemon_level
- defender_level = trainer_pokemon_level
-
- # Diálogo aleatorio del entrenador (30% de probabilidad)
- if random.random() < 0.3:
- if (is_trainer_first and trainer_hp > opponent_hp) or (not is_trainer_first and opponent_hp > trainer_hp):
- dialogue_type = "winning"
- else:
- dialogue_type = "losing"
-
- dialogue = random.choice(TRAINER_DIALOGUES[dialogue_type]).format(pokemon=attacker_pokemon.name)
- battle_log.append(f"🗣️ {attacker_name}: {dialogue}")
-
- # Ataque
- attack_used = get_random_attack(attacker_pokemon)
- damage, is_critical, is_special, resisted = calculate_damage(
- attacker_pokemon,
- defender_pokemon,
- attack_used,
- attacker_level,
- defender_level
- )
-
- # Registrar el último ataque
- if is_trainer_first:
- last_trainer_attack = attack_used
- else:
- last_opponent_attack = attack_used
-
- # Multiplicador de tipo para mensajes especiales
- type_multiplier = get_type_multiplier(
- attacker_pokemon.element or "Normal",
- defender_pokemon.element or "Normal"
- )
-
- # Diálogo especial para daño 4x (50% de probabilidad)
- if type_multiplier >= 4.0 and random.random() < 0.5:
- dialogue = random.choice(TRAINER_DIALOGUES["x4_damage"]).format(pokemon=defender_pokemon.name)
- battle_log.append(f"🗣️ {attacker_name}: {dialogue}")
-
- # Diálogo para golpe crítico o resistencia (50% de probabilidad)
- if (is_critical or resisted) and random.random() < 0.5:
- dialogue_type = "critical" if is_critical else "resisted"
- dialogue = random.choice(TRAINER_DIALOGUES[dialogue_type]).format(pokemon=attacker_pokemon.name)
- battle_log.append(f"🗣️ {attacker_name}: {dialogue}")
-
- # Aplicar daño
- if is_trainer_first:
- opponent_hp -= damage
- opponent_hp = max(0, opponent_hp) # No puede ser negativo
- else:
- trainer_hp -= damage
- trainer_hp = max(0, trainer_hp) # No puede ser negativo
-
- # Mensajes de log
- type_message = ""
- if type_multiplier >= 4.0:
- type_message = " ¡Es extremadamente efectivo! (x4)"
- elif type_multiplier > 1.5:
- type_message = " ¡Es muy efectivo!"
- elif type_multiplier <= 0.25:
- type_message = " ¡Casi no afecta... (x0.25)"
- elif type_multiplier < 0.5:
- type_message = " ¡No es muy efectivo..."
-
- critical_message = " 💥¡Golpe crítico!" if is_critical else ""
- special_message = " ✨(Ataque especial)" if is_special else ""
- resist_message = " 🛡️¡Resistió el daño!" if resisted else ""
-
- # Determinar HP restante para mostrar
- if is_trainer_first:
- remaining_hp = opponent_hp
- max_hp = max_opponent_hp
- defender_pokemon_name = opponent_pokemon.name
- else:
- remaining_hp = trainer_hp
- max_hp = max_trainer_hp
- defender_pokemon_name = trainer_pokemon.name
-
- hp_percentage = (remaining_hp / max_hp) * 100
- hp_status = ""
- if hp_percentage > 60:
- hp_status = "🟢"
- elif hp_percentage > 30:
- hp_status = "🟡"
- else:
- hp_status = "🔴"
-
- battle_log.append(
- f"🔹 Turno {turn_count}: {attacker_pokemon.name} usa {attack_used}{special_message} "
- f"contra {defender_pokemon_name} -{damage} HP{type_message}{critical_message}{resist_message} "
- f"{hp_status} HP: {remaining_hp}/{max_hp}"
- )
-
- # Verificar si el defensor se debilitó
- if (is_trainer_first and opponent_hp <= 0) or (not is_trainer_first and trainer_hp <= 0):
- # 10% + 0.1% por nivel de probabilidad de un último ataque antes de debilitarse
- last_attack_chance = 0.1 + (defender_pokemon.level * 0.001)
- if random.random() < last_attack_chance:
- last_attack = get_random_attack(defender_pokemon)
- last_damage, last_critical, last_special, _ = calculate_damage(
- defender_pokemon,
- attacker_pokemon,
- last_attack,
- defender_level,
- attacker_level
- )
-
- if is_trainer_first:
- trainer_hp -= last_damage
- trainer_hp = max(0, trainer_hp)
- else:
- opponent_hp -= last_damage
- opponent_hp = max(0, opponent_hp)
-
- last_critical_msg = " 💥¡Golpe crítico!" if last_critical else ""
- last_special_msg = " ✨(Ataque especial)" if last_special else ""
-
- battle_log.append(
- f"🔥 ¡{defender_pokemon.name} contraataca con {last_attack}{last_special_msg} antes de debilitarse! "
- f"-{last_damage} HP{last_critical_msg}"
- )
-
- battle_log.append(f"💀 ¡{defender_pokemon.name} se debilitó!")
- break
-
- # Cambiar turnos para el siguiente ataque
- is_trainer_first = not is_trainer_first
-
- # Determinar el ganador de esta batalla
- if trainer_hp > 0 and opponent_hp <= 0:
- winner = "trainer"
- winner_name = trainer.name
- winner_pokemon = trainer_pokemon
- loser_name = opponent.name
- loser_pokemon = opponent_pokemon
- elif opponent_hp > 0 and trainer_hp <= 0:
- winner = "opponent"
- winner_name = opponent.name
- winner_pokemon = opponent_pokemon
- loser_name = trainer.name
- loser_pokemon = trainer_pokemon
- else:
- winner = "draw"
- winner_name = "Empate"
- loser_name = "Empate"
- winner_pokemon = None
- loser_pokemon = None
-
- # Calcular subida de nivel para los Pokémon
- trainer_levels_gained = calculate_level_up(trainer_pokemon, turn_count, winner == "trainer")
- opponent_levels_gained = calculate_level_up(opponent_pokemon, turn_count, winner == "opponent")
-
- if trainer_levels_gained > 0:
- battle_log.append(f"🎉 ¡{trainer_pokemon.name} subió {trainer_levels_gained} nivel(es)! Ahora es nivel {trainer_pokemon_level + trainer_levels_gained}")
- if opponent_levels_gained > 0:
- battle_log.append(f"🎉 ¡{opponent_pokemon.name} subió {opponent_levels_gained} nivel(es)! Ahora es nivel {opponent_pokemon_level + opponent_levels_gained}")
-
- return {
- "winner": winner,
- "winner_name": winner_name,
- "loser_name": loser_name,
- "winner_pokemon": winner_pokemon,
- "loser_pokemon": loser_pokemon,
- "trainer_hp_remaining": max(0, trainer_hp),
- "opponent_hp_remaining": max(0, opponent_hp),
- "battle_log": battle_log,
- "trainer_pokemon": trainer_pokemon,
- "opponent_pokemon": opponent_pokemon,
- "last_trainer_attack": last_trainer_attack,
- "last_opponent_attack": last_opponent_attack,
- "turn_count": turn_count,
- "trainer_levels_gained": trainer_levels_gained,
- "opponent_levels_gained": opponent_levels_gained
- }
-
-# --------------------------------------------------
-# SIMULACIÓN DE BATALLA COMPLETA (MEJOR DE 3) CON MVP
-# --------------------------------------------------
-
-async def simulate_battle(
- db: AsyncSession,
- trainer_id: int,
- opponent_id: int,
- keep_winner_pokemon: bool = True,
- smart_selection: bool = True
-) -> schemas.BattleResult:
- """
- Simula una batalla Pokémon completa entre dos entrenadores (mejor de 3)
- con comentarios mejorados, diálogos y resumen MVP.
- - keep_winner_pokemon: Si True, los Pokémon ganadores permanecen en batalla
- - smart_selection: Si True, los entrenadores eligen Pokémon estratégicamente
- """
- # Validación de entrenadores
- trainer = await crud.get_trainer(db, trainer_id)
- opponent = await crud.get_trainer(db, opponent_id)
-
- if not trainer or not opponent:
- raise HTTPException(status_code=404, detail="Entrenador no encontrado")
-
- # Validación de equipos Pokémon
- trainer_pokemons = await crud.get_trainer_pokemons(db, trainer_id)
- opponent_pokemons = await crud.get_trainer_pokemons(db, opponent_id)
-
- if not trainer_pokemons or not opponent_pokemons:
- raise HTTPException(
- status_code=400,
- detail="Ambos entrenadores necesitan Pokémon para pelear"
- )
-
- # Registro de batalla general
- master_battle_log = []
- battle_results = []
- trainer_wins = 0
- opponent_wins = 0
-
- # Variables para mantener Pokémon ganadores entre batallas
- current_trainer_pokemon = None
- current_opponent_pokemon = None
-
- # Listas para Pokémon derrotados (no pueden volver a ser seleccionados)
- defeated_trainer_pokemons = set()
- defeated_opponent_pokemons = set()
-
- # Comentarista de la batalla
- commentator = "¡Esto fue épico! 🌟"
-
- # Mejor de 3 batallas
- for battle_num in range(1, 4):
- master_battle_log.append(f"🔥 BATALLA {battle_num} 🔥")
-
- # Función para selección inteligente de Pokémon
- def smart_pokemon_selection(pokemons, opponent_pokemon, defeated_pokemons, current_pokemon):
- """Selecciona el mejor Pokémon disponible contra el oponente"""
- available_pokemons = [
- p for p in pokemons
- if p.pokemon.id not in defeated_pokemons and
- (current_pokemon is None or p.pokemon.id != current_pokemon["pokemon"].id)
- ]
-
- if not available_pokemons:
- return None
-
- if opponent_pokemon:
- # Seleccionar Pokémon con ventaja de tipo
- best_pokemon = None
- best_score = -1
-
- for p in available_pokemons:
- pokemon = p.pokemon
- score = 0
-
- # Ventaja de tipo
- type_multiplier = get_type_multiplier(pokemon.element or "Normal", opponent_pokemon.element or "Normal")
- if type_multiplier >= 2.0:
- score += 3
- elif type_multiplier > 1.0:
- score += 1
- elif type_multiplier <= 0.5:
- score -= 2
-
- # Estadísticas
- score += (pokemon.attack or 0) / 10
- score += (pokemon.special_attack or 0) / 10
- score += (pokemon.speed or 0) / 20
-
- if score > best_score:
- best_score = score
- best_pokemon = pokemon
-
- return best_pokemon if best_pokemon else random.choice(available_pokemons).pokemon
- else:
- # Primera batalla, seleccionar el más fuerte
- return max(available_pokemons, key=lambda x: (x.pokemon.attack or 0) + (x.pokemon.special_attack or 0)).pokemon
-
- # Selección de Pokémon para esta batalla
- if keep_winner_pokemon:
- # Para el entrenador
- if current_trainer_pokemon and current_trainer_pokemon.get("hp_remaining", 0) > 0:
- trainer_pokemon = current_trainer_pokemon["pokemon"]
- master_battle_log.append(f"⚡ {trainer.name} mantiene a {trainer_pokemon.name} en el campo! (HP: {current_trainer_pokemon['hp_remaining']}/{trainer_pokemon.hp})")
- else:
- if smart_selection:
- opponent_current = current_opponent_pokemon["pokemon"] if current_opponent_pokemon else None
- trainer_pokemon = smart_pokemon_selection(
- trainer_pokemons,
- opponent_current,
- defeated_trainer_pokemons,
- current_trainer_pokemon
- )
- else:
- available_pokemons = [
- p for p in trainer_pokemons
- if p.pokemon.id not in defeated_trainer_pokemons and
- (current_trainer_pokemon is None or p.pokemon.id != current_trainer_pokemon["pokemon"].id)
- ]
- if not available_pokemons:
- raise HTTPException(
- status_code=400,
- detail=f"{trainer.name} no tiene Pokémon disponibles para pelear"
- )
- trainer_pokemon = random.choice(available_pokemons).pokemon
-
- master_battle_log.append(f"⚡ {trainer.name} elige a {trainer_pokemon.name} (Nv. {trainer_pokemon.level}) para la Batalla {battle_num}!")
- current_trainer_pokemon = None
-
- # Para el oponente
- if current_opponent_pokemon and current_opponent_pokemon.get("hp_remaining", 0) > 0:
- opponent_pokemon = current_opponent_pokemon["pokemon"]
- master_battle_log.append(f"⚡ {opponent.name} mantiene a {opponent_pokemon.name} en combate! (HP: {current_opponent_pokemon['hp_remaining']}/{opponent_pokemon.hp})")
- else:
- if smart_selection:
- trainer_current = current_trainer_pokemon["pokemon"] if current_trainer_pokemon else None
- opponent_pokemon = smart_pokemon_selection(
- opponent_pokemons,
- trainer_current,
- defeated_opponent_pokemons,
- current_opponent_pokemon
- )
- else:
- available_pokemons = [
- p for p in opponent_pokemons
- if p.pokemon.id not in defeated_opponent_pokemons and
- (current_opponent_pokemon is None or p.pokemon.id != current_opponent_pokemon["pokemon"].id)
- ]
- if not available_pokemons:
- raise HTTPException(
- status_code=400,
- detail=f"{opponent.name} no tiene Pokémon disponibles para pelear"
- )
- opponent_pokemon = random.choice(available_pokemons).pokemon
-
- master_battle_log.append(f"⚡ {opponent.name} saca a {opponent_pokemon.name} (Nv. {opponent_pokemon.level}) al ruedo!")
- current_opponent_pokemon = None
- else:
- # Selección aleatoria simple (sin mantener Pokémon ganadores)
- available_trainer = [p for p in trainer_pokemons if p.pokemon.id not in defeated_trainer_pokemons]
- available_opponent = [p for p in opponent_pokemons if p.pokemon.id not in defeated_opponent_pokemons]
-
- if not available_trainer or not available_opponent:
- raise HTTPException(
- status_code=400,
- detail="No hay suficientes Pokémon disponibles para continuar la batalla"
- )
-
- trainer_pokemon = random.choice(available_trainer).pokemon
- opponent_pokemon = random.choice(available_opponent).pokemon
- master_battle_log.append(f"⚡ {trainer.name} elige a {trainer_pokemon.name} (Nv. {trainer_pokemon.level})")
- master_battle_log.append(f"⚡ {opponent.name} elige a {opponent_pokemon.name} (Nv. {opponent_pokemon.level})")
-
- # Simular la batalla individual
- previous_trainer_hp = current_trainer_pokemon["hp_remaining"] if current_trainer_pokemon else None
- result = await simulate_single_battle(
- db, trainer, opponent, trainer_pokemon, opponent_pokemon, previous_trainer_hp
- )
-
- # Actualizar niveles de los Pokémon
- if result["trainer_levels_gained"] > 0:
- updated_pokemon = schemas.PokemonUpdate(
- level=trainer_pokemon.level + result["trainer_levels_gained"]
- )
- await crud.update_pokemon(db, trainer_pokemon.id, updated_pokemon)
- trainer_pokemon.level += result["trainer_levels_gained"]
-
- if result["opponent_levels_gained"] > 0:
- updated_pokemon = schemas.PokemonUpdate(
- level=opponent_pokemon.level + result["opponent_levels_gained"]
- )
- await crud.update_pokemon(db, opponent_pokemon.id, updated_pokemon)
- opponent_pokemon.level += result["opponent_levels_gained"]
-
- # Actualizar conteo de victorias
- if result["winner"] == "trainer":
- trainer_wins += 1
- defeated_opponent_pokemons.add(result["loser_pokemon"].id)
- if keep_winner_pokemon:
- current_trainer_pokemon = {
- "pokemon": result["winner_pokemon"],
- "hp_remaining": result["trainer_hp_remaining"]
- }
- current_opponent_pokemon = None
- elif result["winner"] == "opponent":
- opponent_wins += 1
- defeated_trainer_pokemons.add(result["loser_pokemon"].id)
- if keep_winner_pokemon:
- current_opponent_pokemon = {
- "pokemon": result["winner_pokemon"],
- "hp_remaining": result["opponent_hp_remaining"]
- }
- current_trainer_pokemon = None
- else:
- current_trainer_pokemon = None
- current_opponent_pokemon = None
-
- # Agregar logs al registro maestro
- master_battle_log.extend(result["battle_log"])
- master_battle_log.append(f"🏆 Resultado de la Batalla {battle_num}: ¡{result['winner_name']} se lleva la victoria!")
- master_battle_log.append(f"📊 Marcador: {trainer.name} {trainer_wins} - {opponent_wins} {opponent.name}")
-
- # Guardar resultados
- battle_results.append(result)
-
- # Verificar si ya hay un ganador definitivo
- if trainer_wins >= 2 or opponent_wins >= 2:
- break
-
- # Determinar el ganador general
- if trainer_wins > opponent_wins:
- overall_winner = trainer.name
- overall_winner_id = trainer.id
- overall_loser = opponent.name
- is_trainer_winner = True
- commentator += f" ¡Y con una actuación estelar, {overall_winner} se corona como el campeón de este encuentro! 🎉"
- elif opponent_wins > trainer_wins:
- overall_winner = opponent.name
- overall_winner_id = opponent.id
- overall_loser = trainer.name
- is_trainer_winner = False
- commentator += f" ¡Increíble! ¡{overall_winner} demuestra su poder y se lleva la victoria general! 🏆"
- else:
- overall_winner = "Empate"
- overall_winner_id = None
- overall_loser = "Empate"
- is_trainer_winner = None
- commentator += " ¡Un final reñido! ¡La batalla termina en un empate! 🤝"
-
- # Determinar Pokémon MVP (mayor daño total o más victorias)
- mvp_data = []
- for result in battle_results:
- # Daño infligido por el Pokémon del entrenador
- trainer_damage = (result["opponent_pokemon"].hp or 100) - result["opponent_hp_remaining"]
- mvp_data.append({
- "pokemon": result["trainer_pokemon"],
- "damage": trainer_damage,
- "wins": 1 if result["winner"] == "trainer" else 0,
- "trainer": trainer.name
- })
-
- # Daño infligido por el Pokémon del oponente
- opponent_damage = (result["trainer_pokemon"].hp or 100) - result["trainer_hp_remaining"]
- mvp_data.append({
- "pokemon": result["opponent_pokemon"],
- "damage": opponent_damage,
- "wins": 1 if result["winner"] == "opponent" else 0,
- "trainer": opponent.name
- })
-
- # Consolidar datos por Pokémon
- mvp_stats = {}
- for data in mvp_data:
- key = data["pokemon"].id
- if key not in mvp_stats:
- mvp_stats[key] = {
- "pokemon": data["pokemon"],
- "total_damage": 0,
- "total_wins": 0,
- "trainer": data["trainer"]
- }
- mvp_stats[key]["total_damage"] += data["damage"]
- mvp_stats[key]["total_wins"] += data["wins"]
-
- # Determinar MVP
- if mvp_stats:
- mvp = max(mvp_stats.values(), key=lambda x: (x["total_wins"], x["total_damage"]))
- mvp_message = (
- f"🏅 MVP del combate: {mvp['pokemon'].name} (Nv. {mvp['pokemon'].level}) de {mvp['trainer']}!"
- f"• Victorias: {mvp['total_wins']}"
- f"• Daño total infligido: {mvp['total_damage']} HP"
- f"• Movimiento más usado: {random.choice(mvp['pokemon'].moves) if hasattr(mvp['pokemon'], 'moves') and mvp['pokemon'].moves else 'Placaje'}"
- )
- master_battle_log.append(mvp_message)
-
- master_battle_log.append("🎯 RESULTADO FINAL 🎯")
- master_battle_log.append(
- f"{trainer.name}: {trainer_wins} victoria(s) | "
- f"{opponent.name}: {opponent_wins} victoria(s)"
- )
- master_battle_log.append(f"🏅 ¡{overall_winner} gana el combate!")
- master_battle_log.append(f"💬 Comentario del experto: {commentator}")
-
- # Registro en base de datos
- battle_data = schemas.BattleCreate(
- trainer_id=trainer_id,
- opponent_id=opponent_id,
- is_best_of_three=True
- )
-
- db_battle = await crud.create_battle(db, battle_data)
-
- # Actualización con resultado
- if overall_winner != "Empate":
- await crud.update_battle(
- db,
- db_battle.id,
- schemas.BattleUpdate(
- winner=overall_winner,
- date=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
- battle_log="".join(master_battle_log)
- )
- )
-
- # Registro de Pokémon participantes (todos los que participaron)
- for result in battle_results:
- await crud.add_pokemon_to_battle(
- db,
- schemas.BattlePokemonCreate(
- battle_id=db_battle.id,
- pokemon_id=result["trainer_pokemon"].id,
- hp_remaining=result["trainer_hp_remaining"],
- participated=True,
- battle_round=battle_results.index(result) + 1
- )
- )
- await crud.add_pokemon_to_battle(
- db,
- schemas.BattlePokemonCreate(
- battle_id=db_battle.id,
- pokemon_id=result["opponent_pokemon"].id,
- hp_remaining=result["opponent_hp_remaining"],
- participated=True,
- battle_round=battle_results.index(result) + 1
- )
- )
-
- # Obtener la última batalla para los datos finales
- last_battle = battle_results[-1]
-
- # Resultado detallado
- return schemas.BattleResult(
- battle_id=db_battle.id,
- winner_id=overall_winner_id,
- winner_name=overall_winner,
- loser_name=overall_loser,
- trainer_pokemon=last_battle["trainer_pokemon"],
- opponent_pokemon=last_battle["opponent_pokemon"],
- trainer_hp_remaining=last_battle["trainer_hp_remaining"],
- opponent_hp_remaining=last_battle["opponent_hp_remaining"],
- battle_log=master_battle_log,
- last_trainer_attack=last_battle["last_trainer_attack"],
- last_opponent_attack=last_battle["last_opponent_attack"],
- trainer_wins=trainer_wins,
- opponent_wins=opponent_wins,
- is_best_of_three=True,
- keep_winner_pokemon=keep_winner_pokemon,
- mvp_pokemon=mvp["pokemon"] if mvp_stats else None
- )
-
-# --------------------------------------------------
-# ENDPOINTS DE LA API
-# --------------------------------------------------
-
-@router.post("/", response_model=schemas.BattleResult)
-async def create_battle(
- battle: schemas.BattleCreate,
- db: AsyncSession = Depends(get_db),
- keep_winner_pokemon: bool = True,
- smart_selection: bool = True
-):
- """Inicia una nueva batalla entre dos entrenadores (mejor de 3)
- - keep_winner_pokemon: Si True, los Pokémon ganadores permanecen en batalla
- - smart_selection: Si True, los entrenadores eligen Pokémon estratégicamente
- """
- if battle.trainer_id == battle.opponent_id:
- raise HTTPException(
- status_code=400,
- detail="No puedes pelear contra ti mismo"
- )
- return await simulate_battle(db, battle.trainer_id, battle.opponent_id, keep_winner_pokemon, smart_selection)
-
-@router.get("/{battle_id}", response_model=schemas.BattleWithPokemon)
-async def read_battle(
- battle_id: int,
- db: AsyncSession = Depends(get_db)
-):
- """Obtiene los detalles completos de una batalla específica"""
- db_battle = await crud.get_battle_with_pokemons(db, battle_id)
- if db_battle is None:
- raise HTTPException(status_code=404, detail="Batalla no encontrada")
-
- # Asegurar nombres de entrenadores
- if not hasattr(db_battle, 'trainer_name'):
- trainer = await crud.get_trainer(db, db_battle.trainer_id)
- db_battle.trainer_name = trainer.name if trainer else "Desconocido"
-
- if not hasattr(db_battle, 'opponent_name'):
- opponent = await crud.get_trainer(db, db_battle.opponent_id)
- db_battle.opponent_name = opponent.name if opponent else "Desconocido"
-
- return db_battle
-
-@router.get("/", response_model=List[schemas.Battle])
-async def read_battles(
- skip: int = 0,
- limit: int = 10,
- db: AsyncSession = Depends(get_db)
-):
- """Obtiene un listado paginado de todas las batallas registradas"""
- battles = await crud.get_battles(db, skip=skip, limit=limit)
-
- # Asegurar nombres de entrenadores
- for battle in battles:
- if not hasattr(battle, 'trainer_name'):
- trainer = await crud.get_trainer(db, battle.trainer_id)
- battle.trainer_name = trainer.name if trainer else "Desconocido"
- if not hasattr(battle, 'opponent_name'):
- opponent = await crud.get_trainer(db, battle.opponent_id)
- battle.opponent_name = opponent.name if opponent else "Desconocido"
-
- return battles
\ No newline at end of file
diff --git a/app/routers/pokemon.py b/app/routers/pokemon.py
deleted file mode 100644
index 15460f9..0000000
--- a/app/routers/pokemon.py
+++ /dev/null
@@ -1,262 +0,0 @@
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import func, or_
-from typing import List, Optional
-from difflib import get_close_matches
-
-from app.schemas import Admin
-from app.routers.auth import get_current_superadmin, get_current_admin
-
-import unicodedata
-import re
-
-from .. import models, schemas, crud
-from ..database import get_db
-
-router = APIRouter(
- tags=["Pokémon"] # Agrupación para la documentación Swagger/OpenAPI
-)
-
-# --------------------------------------------------
-# FUNCIONES AUXILIARES PARA BÚSQUEDA INTELIGENTE
-# --------------------------------------------------
-
-def normalize_text(text: str) -> str:
- """
- Normaliza texto para búsquedas:
- - Convierte a minúsculas
- - Elimina acentos y caracteres especiales
- - Elimina espacios extras
- """
- text = text.lower().strip()
- text = unicodedata.normalize('NFKD', text).encode('ASCII', 'ignore').decode('ASCII')
- return re.sub(r'\s+', ' ', text)
-
-def find_similar_names(search_term: str, names: List[str], threshold: float = 0.6) -> List[str]:
- """
- Encuentra nombres similares usando coincidencia aproximada.
-
- Args:
- search_term: Término de búsqueda
- names: Lista de nombres disponibles
- threshold: Umbral de similitud (0-1)
-
- Returns:
- Lista de nombres que superan el umbral de similitud
- """
- normalized_search = normalize_text(search_term)
- normalized_names = [normalize_text(name) for name in names]
-
- matches = get_close_matches(
- normalized_search,
- normalized_names,
- n=5,
- cutoff=threshold
- )
-
- # Recuperar los nombres originales
- original_matches = []
- for match in matches:
- index = normalized_names.index(match)
- original_matches.append(names[index])
-
- return original_matches
-
-# --------------------------------------------------
-# ENDPOINTS
-# --------------------------------------------------
-
-@router.post("/", response_model=schemas.Pokemon)
-async def create_pokemon(
- pokemon: schemas.PokemonCreate,
- db: AsyncSession = Depends(get_db),
- current_admin: Admin = Depends(get_current_admin)
-):
- """
- Crea un nuevo Pokémon en la base de datos.
-
- Args:
- pokemon: Datos del Pokémon a crear
- db: Sesión de base de datos
-
- Returns:
- El Pokémon creado con su ID asignado
- """
- return await crud.create_pokemon(db, pokemon)
-
-@router.get("/", response_model=List[schemas.Pokemon])
-async def read_pokemons(
- name: Optional[str] = None,
- db: AsyncSession = Depends(get_db),
- current_admin: Admin = Depends(get_current_admin)
-):
- """
- Obtiene un listado paginado de Pokémon.
-
- Args:
- skip: Número de registros a omitir (paginación)
- limit: Máximo número de registros a devolver
- name: Filtro opcional por nombre (búsqueda exacta)
- db: Sesión de base de datos
-
- Returns:
- Lista de Pokémon paginados
- """
- pokemons = await crud.get_pokemons(db,name=name)
- return pokemons
-
-@router.get("/search/", response_model=List[schemas.Pokemon])
-async def search_pokemons_by_name(
- name: str,
- db: AsyncSession = Depends(get_db)
-):
- """
- Búsqueda avanzada por nombre con coincidencias aproximadas.
-
- Características:
- - Case insensitive
- - Coincidencias parciales
- - Ordena por mejor coincidencia primero
-
- Args:
- name: Término de búsqueda (ej: "pika")
- skip: Número de registros a omitir
- limit: Máximo número de resultados
- db: Sesión de base de datos
-
- Returns:
- Lista de Pokémon que coinciden con el criterio
-
- Example:
- GET /pokemon/search/?name=pika
- Encontrará "Pikachu", "Pikachu Gigamax", etc.
- """
- pokemons = await crud.search_pokemons_by_name(db, name=name)
- if not pokemons:
- raise HTTPException(
- status_code=404,
- detail=f"No se encontraron Pokémon con nombre similar a '{name}'"
- )
- return pokemons
-
-@router.get("/flexible-search/", response_model=List[schemas.Pokemon])
-async def flexible_pokemon_search(
- search_term: str,
- db: AsyncSession = Depends(get_db),
- current_admin: Admin = Depends(get_current_admin)
-):
- """
- Búsqueda inteligente con tolerancia a errores ortográficos.
-
- Implementa un sistema de 3 capas:
- 1. Búsqueda exacta (case insensitive)
- 2. Coincidencia aproximada (difflib)
- 3. Búsqueda por subcadena
-
- Args:
- search_term: Término a buscar (ej: "picachu")
- skip: Registros a omitir
- limit: Máximo resultados
- db: Sesión de base de datos
-
- Returns:
- Lista de Pokémon ordenados por relevancia
-
- Example:
- GET /pokemon/flexible-search/?search_term=picachu
- Encontrará "Pikachu" aunque esté mal escrito
- """
- # Capa 1: Búsqueda exacta
- exact_match = await crud.get_pokemon_by_name(db, search_term)
- if exact_match:
- return [exact_match]
-
- # Capa 2: Coincidencia aproximada
- all_pokemons = await crud.get_pokemons(db,)
- all_names = [p.name for p in all_pokemons]
-
- similar_names = find_similar_names(search_term, all_names)
- if similar_names:
- pokemons = await crud.get_pokemons_by_names(db, similar_names)
- if pokemons:
- return pokemons
-
- # Capa 3: Búsqueda por subcadena
- pokemons = await crud.search_pokemons_by_name(db, name=search_term)
- if not pokemons:
- raise HTTPException(
- status_code=404,
- detail=f"No se encontraron Pokémon para el término '{search_term}'"
- )
- return pokemons
-
-@router.get("/{pokemon_id}", response_model=schemas.Pokemon)
-async def read_pokemon(
- pokemon_id: int,
- db: AsyncSession = Depends(get_db)
-):
- """
- Obtiene un Pokémon específico por su ID.
-
- Args:
- pokemon_id: ID del Pokémon
- db: Sesión de base de datos
-
- Returns:
- Los datos completos del Pokémon
-
- Raises:
- HTTPException: 404 si no se encuentra
- """
- db_pokemon = await crud.get_pokemon(db, pokemon_id=pokemon_id)
- if db_pokemon is None:
- raise HTTPException(status_code=404, detail="Pokémon no encontrado")
- return db_pokemon
-
-@router.put("/{pokemon_id}", response_model=schemas.Pokemon)
-async def update_pokemon(
- pokemon_id: int,
- pokemon: schemas.PokemonCreate,
- db: AsyncSession = Depends(get_db)
-):
- """
- Actualiza los datos de un Pokémon existente.
-
- Args:
- pokemon_id: ID del Pokémon a actualizar
- pokemon: Nuevos datos
- db: Sesión de base de datos
-
- Returns:
- Pokémon actualizado
-
- Raises:
- HTTPException: 404 si no se encuentra
- """
- db_pokemon = await crud.update_pokemon(db, pokemon_id, pokemon)
- if db_pokemon is None:
- raise HTTPException(status_code=404, detail="Pokémon no encontrado")
- return db_pokemon
-
-@router.delete("/{pokemon_id}", response_model=schemas.Pokemon)
-async def delete_pokemon(
- pokemon_id: int,
- db: AsyncSession = Depends(get_db)
-):
- """
- Elimina un Pokémon de la base de datos.
-
- Args:
- pokemon_id: ID del Pokémon a eliminar
- db: Sesión de base de datos
-
- Returns:
- Pokémon eliminado
-
- Raises:
- HTTPException: 404 si no se encuentra
- """
- db_pokemon = await crud.delete_pokemon(db, pokemon_id)
- if db_pokemon is None:
- raise HTTPException(status_code=404, detail="Pokémon no encontrado")
- return db_pokemon
\ No newline at end of file
diff --git a/app/routers/trainer.py b/app/routers/trainer.py
deleted file mode 100644
index 0e1b3a1..0000000
--- a/app/routers/trainer.py
+++ /dev/null
@@ -1,181 +0,0 @@
-# Importamos las bibliotecas necesarias
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from typing import List
-
-# Importamos los esquemas, operaciones CRUD y la conexión a la base de datos desde nuestros módulos
-from .. import schemas, crud
-from ..database import get_db
-
-# Creamos un router de FastAPI para agrupar todas las rutas relacionadas con entrenadores
-router = APIRouter(
- tags=["Entrenadores"] # Agrupación para la documentación Swagger/OpenAPI
-)
-
-# Endpoint para crear un nuevo entrenador
-@router.post("/", response_model=schemas.Trainer)
-async def create_trainer(
- trainer: schemas.TrainerCreate, # Datos del entrenador a crear (validados por el esquema)
- db: AsyncSession = Depends(get_db) # Conexión a la base de datos (inyectada por FastAPI)
-):
- """
- Crea un nuevo entrenador en la base de datos.
-
- Args:
- trainer: Datos del entrenador a crear.
- db: Sesión de base de datos asíncrona.
-
- Returns:
- El entrenador creado con su ID asignado.
- """
- return await crud.create_trainer(db, trainer)
-
-# Endpoint para obtener una lista de entrenadores
-@router.get("/", response_model=List[schemas.Trainer])
-async def read_trainers(
- skip: int = 0, # Número de registros a saltar (para paginación)
- limit: int = 10, # Número máximo de registros a devolver (para paginación)
- db: AsyncSession = Depends(get_db) # Conexión a la base de datos
-):
- """
- Obtiene una lista de entrenadores con paginación.
-
- Args:
- skip: Número de entrenadores a saltar.
- limit: Número máximo de entrenadores a devolver.
- db: Sesión de base de datos asíncrona.
-
- Returns:
- Lista de entrenadores.
- """
- trainers = await crud.get_trainers(db, skip=skip, limit=limit)
- return trainers
-
-# Endpoint para obtener un entrenador específico por su ID
-@router.get("/{trainer_id}", response_model=schemas.Trainer)
-async def read_trainer(
- trainer_id: int, # ID del entrenador a buscar
- db: AsyncSession = Depends(get_db) # Conexión a la base de datos
-):
- """
- Obtiene un entrenador específico por su ID.
-
- Args:
- trainer_id: ID del entrenador a buscar.
- db: Sesión de base de datos asíncrona.
-
- Returns:
- Los datos del entrenador si existe.
-
- Raises:
- HTTPException: 404 si el entrenador no se encuentra.
- """
- db_trainer = await crud.get_trainer(db, trainer_id=trainer_id)
- if db_trainer is None:
- raise HTTPException(status_code=404, detail="Entrenador no encontrado")
- return db_trainer
-
-# Endpoint para actualizar un entrenador existente
-@router.put("/{trainer_id}", response_model=schemas.Trainer)
-async def update_trainer(
- trainer_id: int, # ID del entrenador a actualizar
- trainer: schemas.TrainerCreate, # Nuevos datos del entrenador
- db: AsyncSession = Depends(get_db) # Conexión a la base de datos
-):
- """
- Actualiza los datos de un entrenador existente.
-
- Args:
- trainer_id: ID del entrenador a actualizar.
- trainer: Nuevos datos del entrenador.
- db: Sesión de base de datos asíncrona.
-
- Returns:
- Los datos actualizados del entrenador.
-
- Raises:
- HTTPException: 404 si el entrenador no se encuentra.
- """
- db_trainer = await crud.update_trainer(db, trainer_id, trainer)
- if db_trainer is None:
- raise HTTPException(status_code=404, detail="Entrenador no encontrado")
- return db_trainer
-
-# Endpoint para eliminar un entrenador
-@router.delete("/{trainer_id}", response_model=schemas.Trainer)
-async def delete_trainer(
- trainer_id: int, # ID del entrenador a eliminar
- db: AsyncSession = Depends(get_db) # Conexión a la base de datos
-):
- """
- Elimina un entrenador de la base de datos.
-
- Args:
- trainer_id: ID del entrenador a eliminar.
- db: Sesión de base de datos asíncrona.
-
- Returns:
- Los datos del entrenador eliminado.
-
- Raises:
- HTTPException: 404 si el entrenador no se encuentra.
- """
- db_trainer = await crud.delete_trainer(db, trainer_id)
- if db_trainer is None:
- raise HTTPException(status_code=404, detail="Entrenador no encontrado")
- return db_trainer
-
-# Endpoint para agregar un Pokémon a un entrenador
-@router.post("/{trainer_id}/pokemons", response_model=schemas.TrainerPokemon)
-async def add_pokemon_to_trainer(
- trainer_id: int, # ID del entrenador al que se agregará el Pokémon
- pokemon_id: int, # ID del Pokémon a agregar
- is_shiny: bool = False, # Indica si el Pokémon es shiny (opcional, default False)
- db: AsyncSession = Depends(get_db) # Conexión a la base de datos
-):
- """
- Agrega un Pokémon a la colección de un entrenador.
-
- Args:
- trainer_id: ID del entrenador.
- pokemon_id: ID del Pokémon a agregar.
- is_shiny: Si el Pokémon es shiny (opcional).
- db: Sesión de base de datos asíncrona.
-
- Returns:
- La relación entre el entrenador y el Pokémon creada.
- """
- # Creamos un objeto TrainerPokemonCreate con los datos recibidos
- trainer_pokemon = schemas.TrainerPokemonCreate(
- trainer_id=trainer_id,
- pokemon_id=pokemon_id,
- is_shiny=is_shiny
- )
- return await crud.add_pokemon_to_trainer(db, trainer_pokemon)
-
-# Endpoint para obtener todos los Pokémon de un entrenador
-@router.get("/{trainer_id}/pokemons", response_model=List[schemas.TrainerPokemon])
-async def get_trainer_pokemons(
- trainer_id: int, # ID del entrenador cuyos Pokémon queremos obtener
- db: AsyncSession = Depends(get_db) # Conexión a la base de datos
-):
- """
- Obtiene todos los Pokémon asociados a un entrenador.
-
- Args:
- trainer_id: ID del entrenador.
- db: Sesión de base de datos asíncrona.
-
- Returns:
- Lista de Pokémon del entrenador.
-
- Raises:
- HTTPException: 404 si el entrenador no tiene Pokémon o no existe.
- """
- pokemons = await crud.get_trainer_pokemons(db, trainer_id)
- if not pokemons:
- raise HTTPException(
- status_code=404,
- detail="No se encontraron Pokémon para este entrenador"
- )
- return pokemons
\ No newline at end of file
diff --git a/app/schemas.py b/app/schemas.py
deleted file mode 100644
index 6982d8f..0000000
--- a/app/schemas.py
+++ /dev/null
@@ -1,215 +0,0 @@
-# Importaciones necesarias
-from typing import List, Optional
-from pydantic import BaseModel, EmailStr # BaseModel para esquemas, EmailStr para validación de email
-
-class AdminBase(BaseModel):
- username: str
- email: EmailStr
-
-class AdminCreate(AdminBase):
- password: str
-
-class Admin(AdminBase):
- id: int
- is_active: bool
- is_superadmin: bool
-
- class Config:
- from_attributes = True
-
-class AdminInDB(Admin):
- hashed_password: str
-
-## ------------------------- ESQUEMAS PARA POKÉMON ------------------------- ##
-
-class PokemonBase(BaseModel):
- """
- Esquema base para Pokémon con todos los atributos comunes.
- Todos los campos excepto 'name' son opcionales para mayor flexibilidad.
- """
- name: str # Nombre del Pokémon (requerido)
- element: Optional[str] = None # Tipo elemental (Agua, Fuego, etc.)
- hp: Optional[int] = None # Puntos de salud base
- attack: Optional[int] = None # Ataque físico
- defense: Optional[int] = None # Defensa física
- special_attack: Optional[int] = None # Ataque especial
- special_defense: Optional[int] = None # Defensa especial
- speed: Optional[int] = None # Velocidad en combate
- moves: Optional[List[str]] = None # Lista de movimientos disponibles
- current_hp: Optional[int] = None # HP actual (para combates)
- level: Optional[int] = 1 # Nivel del Pokémon (nuevo campo con valor por defecto 1)
-
-class PokemonCreate(PokemonBase):
- """
- Esquema para creación de Pokémon. Hereda todos los campos de PokemonBase.
- Podría incluir validaciones adicionales específicas para creación.
- """
- pass
-
-class Pokemon(PokemonBase):
- """
- Esquema para representación completa de Pokémon, incluyendo ID.
- Se usa para respuestas API cuando se necesita mostrar un Pokémon.
- """
- id: int # ID único del Pokémon en la base de datos
-
- class Config:
- orm_mode = True # Permite la conversión automática desde ORM de SQLAlchemy
-
-class PokemonUpdate(BaseModel):
- current_hp: int | None = None
- level: int | None = None
- # Agrega aquí otros campos que necesites actualizar
-## ------------------------- ESQUEMAS PARA ENTRENADORES ------------------------- ##
-
-class TrainerBase(BaseModel):
- """
- Esquema base para Entrenadores Pokémon.
- """
- name: str # Nombre del entrenador (requerido)
- email: EmailStr # Email con validación automática de formato
- level: Optional[int] = 1 # Nivel del entrenador (default 1)
-
-class TrainerCreate(TrainerBase):
- """
- Esquema para creación de entrenadores.
- Hereda los campos de TrainerBase.
- """
- pass
-
-class Trainer(TrainerBase):
- """
- Esquema completo de entrenador para respuestas API.
- Incluye el ID generado por la base de datos.
- """
- id: int # ID único del entrenador
-
- class Config:
- orm_mode = True # Habilita compatibilidad con ORM
-
-## ------------------------- ESQUEMAS PARA RELACIÓN ENTRENADOR-POKÉMON ------------------------- ##
-
-class TrainerPokemonBase(BaseModel):
- """
- Esquema base para la relación entre entrenadores y Pokémon.
- Representa qué Pokémon pertenecen a qué entrenador.
- """
- trainer_id: int # ID del entrenador dueño
- pokemon_id: int # ID del Pokémon en la colección
- is_shiny: bool = False # Indica si es una variante shiny (default False)
-
-class TrainerPokemonCreate(TrainerPokemonBase):
- """
- Esquema para crear nuevas relaciones entrenador-pokémon.
- """
- pass
-
-class TrainerPokemon(TrainerPokemonBase):
- """
- Esquema completo de la relación, útil para respuestas API.
- """
- class Config:
- orm_mode = True # Compatibilidad con ORM
-
-## ------------------------- ESQUEMAS PARA BATALLAS ------------------------- ##
-
-class BattleBase(BaseModel):
- """
- Esquema base para batallas Pokémon.
- """
- trainer_id: int # ID del entrenador que inicia la batalla
- opponent_id: int # ID del entrenador oponente
- winner: Optional[str] = None # Nombre del ganador (se establece al terminar)
- date: Optional[str] = None # Fecha de la batalla (auto-generada)
-
-class BattleCreate(BaseModel):
- """
- Esquema para iniciar una nueva batalla.
- Solo necesita los IDs de los participantes.
- """
- trainer_id: int
- opponent_id: int
-
-class BattleUpdate(BaseModel):
- """
- Esquema para actualizar una batalla existente.
- Solo campos que pueden modificarse después de creada.
- """
- winner: Optional[str] = None # Para establecer el ganador
- date: Optional[str] = None # Fecha personalizada (raro pero posible)
-
-class Battle(BattleBase):
- """
- Esquema completo de batalla para respuestas API.
- Incluye información extendida como nombres de participantes.
- """
- id: int # ID único de la batalla
- trainer_name: Optional[str] = None # Nombre del entrenador (para mostrar)
- opponent_name: Optional[str] = None # Nombre del oponente (para mostrar)
-
- class Config:
- orm_mode = True # Compatibilidad con ORM
-
-class BattlePokemonBase(BaseModel):
- """
- Esquema base para Pokémon participantes en batallas.
- Lleva registro del estado durante la batalla.
- """
- battle_id: int # ID de la batalla
- pokemon_id: int # ID del Pokémon
- hp_remaining: int # HP actual durante la batalla
- participated: bool = False # Si participó efectivamente
-
-class BattlePokemonCreate(BattlePokemonBase):
- """
- Esquema para agregar Pokémon a una batalla.
- """
- pass
-
-class BattlePokemon(BattlePokemonBase):
- """
- Esquema completo de Pokémon en batalla para respuestas API.
- Incluye los datos completos del Pokémon asociado.
- """
- id: int # ID de esta relación
- pokemon: Optional[Pokemon] = None # Datos completos del Pokémon
-
- class Config:
- orm_mode = True # Compatibilidad con ORM
-
-class BattleWithPokemon(Battle):
- """
- Esquema extendido de batalla que incluye lista de Pokémon participantes.
- Útil para mostrar todos los detalles de una batalla en una sola respuesta.
- """
- pokemons: List[BattlePokemon] = [] # Lista de Pokémon en la batalla
-
- class Config:
- orm_mode = True # Compatibilidad con ORM
-
-## ------------------------- RESULTADO DETALLADO DE BATALLA ------------------------- ##
-
-class BattleResult(BaseModel):
- """
- Esquema para el resultado detallado de una batalla.
- Contiene información completa para mostrar el desarrollo del combate.
- """
- battle_id: int
- winner_id: Optional[int]
- winner_name: str
- loser_name: str
- trainer_pokemon: Pokemon
- opponent_pokemon: Pokemon
- trainer_hp_remaining: int
- opponent_hp_remaining: int
- battle_log: List[str]
- last_trainer_attack: Optional[str] = None # Hacer opcional o proporcionar valor por defecto
- last_opponent_attack: Optional[str] = None
- trainer_wins: int
- opponent_wins: int
- is_best_of_three: bool
- keep_winner_pokemon: bool # Indica si se mantiene el Pokémon ganador para la siguiente batalla
-## ------------------------- MANEJO DE REFERENCIAS CIRCULARES ------------------------- ##
-
-# Resuelve referencias circulares entre esquemas que se referencian mutuamente
-BattleWithPokemon.update_forward_refs()
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 39bad23..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,27 +0,0 @@
-# Core FastAPI y ASGI
-fastapi==0.110.1
-uvicorn==0.29.0
-python-dotenv==1.0.1
-
-# Base de datos (PostgreSQL)
-sqlalchemy==2.0.29
-psycopg2-binary==2.9.9
-asyncpg==0.29.0 # Opcional, solo si usas async/await con PostgreSQL
-
-# Utilidades
-python-multipart==0.0.9
-fuzzywuzzy==0.18.0
-python-Levenshtein==0.12.2
-fastapi-cache2==0.2.2
-slowapi==0.1.8
-limits==3.7.0
-
-# Dependencias de Pydantic (requeridas por FastAPI)
-pydantic==2.7.1
-pydantic_core==2.18.2
-email-validator==2.1.1
-
-# Autenticación JWT y seguridad
-python-jose[cryptography]==3.3.0
-passlib==1.7.4
-bcrypt==4.1.2
\ No newline at end of file
diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj
new file mode 100644
index 0000000..40b2c67
--- /dev/null
+++ b/src/Api/Api.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ true
+
+ $(NoWarn);1591
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Api/Api.http b/src/Api/Api.http
new file mode 100644
index 0000000..fa50852
--- /dev/null
+++ b/src/Api/Api.http
@@ -0,0 +1,6 @@
+@Api_HostAddress = http://localhost:5146
+
+GET {{Api_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/src/Api/Controllers/BattlesController.cs b/src/Api/Controllers/BattlesController.cs
new file mode 100644
index 0000000..bbce671
--- /dev/null
+++ b/src/Api/Controllers/BattlesController.cs
@@ -0,0 +1,35 @@
+using Application.Dtos;
+using Application.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+using System.Threading.Tasks;
+
+namespace Api.Controllers
+{
+ [ApiController]
+ [Route("api/[controller]")]
+ public class BattlesController : ControllerBase
+ {
+ private readonly IBattleService _battleService;
+
+ public BattlesController(IBattleService battleService)
+ {
+ _battleService = battleService;
+ }
+
+ [HttpPost("simulate")]
+ public async Task> SimulateBattle([FromBody] StartBattleDto startBattleDto)
+ {
+ try
+ {
+ var result = await _battleService.SimulateBattleAsync(startBattleDto.Trainer1Id, startBattleDto.Trainer2Id);
+ return Ok(result);
+ }
+ catch (System.Exception ex)
+ {
+ // En una aplicación real, registraríamos el error.
+ // Aquí, devolvemos un BadRequest con el mensaje de la excepción.
+ return BadRequest(ex.Message);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/Controllers/PokemonsController.cs b/src/Api/Controllers/PokemonsController.cs
new file mode 100644
index 0000000..7f62895
--- /dev/null
+++ b/src/Api/Controllers/PokemonsController.cs
@@ -0,0 +1,105 @@
+using Application.Dtos;
+using Application.Interfaces;
+using Core.Entities;
+using Microsoft.AspNetCore.Mvc;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Api.Controllers
+{
+ [ApiController]
+ [Route("api/[controller]")]
+ public class PokemonsController : ControllerBase
+ {
+ private readonly IPokemonService _pokemonService;
+
+ public PokemonsController(IPokemonService pokemonService)
+ {
+ _pokemonService = pokemonService;
+ }
+
+ [HttpGet]
+ public async Task>> GetPokemons()
+ {
+ var pokemons = await _pokemonService.GetAllPokemonsAsync();
+ var pokemonDtos = pokemons.Select(p => new PokemonDto
+ {
+ Id = p.Id,
+ Name = p.Name,
+ Element = p.Element,
+ Hp = p.Hp,
+ Attack = p.Attack,
+ Defense = p.Defense,
+ SpecialAttack = p.SpecialAttack,
+ SpecialDefense = p.SpecialDefense,
+ Speed = p.Speed,
+ Moves = p.Moves,
+ Level = p.Level
+ });
+ return Ok(pokemonDtos);
+ }
+
+ [HttpGet("{id}")]
+ public async Task> GetPokemon(int id)
+ {
+ var pokemon = await _pokemonService.GetPokemonByIdAsync(id);
+ if (pokemon == null)
+ {
+ return NotFound();
+ }
+ var pokemonDto = new PokemonDto
+ {
+ Id = pokemon.Id,
+ Name = pokemon.Name,
+ Element = pokemon.Element,
+ Hp = pokemon.Hp,
+ Attack = pokemon.Attack,
+ Defense = pokemon.Defense,
+ SpecialAttack = pokemon.SpecialAttack,
+ SpecialDefense = pokemon.SpecialDefense,
+ Speed = pokemon.Speed,
+ Moves = pokemon.Moves,
+ Level = pokemon.Level
+ };
+ return Ok(pokemonDto);
+ }
+
+ [HttpPost]
+ public async Task> CreatePokemon(CreatePokemonDto createPokemonDto)
+ {
+ var pokemon = new Pokemon
+ {
+ Name = createPokemonDto.Name,
+ Element = createPokemonDto.Element,
+ Hp = createPokemonDto.Hp,
+ Attack = createPokemonDto.Attack,
+ Defense = createPokemonDto.Defense,
+ SpecialAttack = createPokemonDto.SpecialAttack,
+ SpecialDefense = createPokemonDto.SpecialDefense,
+ Speed = createPokemonDto.Speed,
+ Moves = createPokemonDto.Moves,
+ Level = createPokemonDto.Level
+ };
+
+ var createdPokemon = await _pokemonService.CreatePokemonAsync(pokemon);
+
+ var pokemonDto = new PokemonDto
+ {
+ Id = createdPokemon.Id,
+ Name = createdPokemon.Name,
+ Element = createdPokemon.Element,
+ Hp = createdPokemon.Hp,
+ Attack = createdPokemon.Attack,
+ Defense = createdPokemon.Defense,
+ SpecialAttack = createdPokemon.SpecialAttack,
+ SpecialDefense = createdPokemon.SpecialDefense,
+ Speed = createdPokemon.Speed,
+ Moves = createdPokemon.Moves,
+ Level = createdPokemon.Level
+ };
+
+ return CreatedAtAction(nameof(GetPokemon), new { id = createdPokemon.Id }, pokemonDto);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/Controllers/TrainersController.cs b/src/Api/Controllers/TrainersController.cs
new file mode 100644
index 0000000..179d92e
--- /dev/null
+++ b/src/Api/Controllers/TrainersController.cs
@@ -0,0 +1,83 @@
+using Application.Dtos;
+using Application.Interfaces;
+using Core.Entities;
+using Microsoft.AspNetCore.Mvc;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Api.Controllers
+{
+ [ApiController]
+ [Route("api/[controller]")]
+ public class TrainersController : ControllerBase
+ {
+ private readonly ITrainerService _trainerService;
+
+ public TrainersController(ITrainerService trainerService)
+ {
+ _trainerService = trainerService;
+ }
+
+ [HttpGet]
+ public async Task>> GetTrainers()
+ {
+ var trainers = await _trainerService.GetAllTrainersAsync();
+ var trainerDtos = trainers.Select(t => new TrainerDto
+ {
+ Id = t.Id,
+ Name = t.Name,
+ Email = t.Email,
+ Level = t.Level
+ });
+ return Ok(trainerDtos);
+ }
+
+ [HttpGet("{id}")]
+ public async Task> GetTrainer(int id)
+ {
+ var trainer = await _trainerService.GetTrainerByIdAsync(id);
+ if (trainer == null)
+ {
+ return NotFound();
+ }
+ var trainerDto = new TrainerDto
+ {
+ Id = trainer.Id,
+ Name = trainer.Name,
+ Email = trainer.Email,
+ Level = trainer.Level
+ };
+ return Ok(trainerDto);
+ }
+
+ [HttpPost]
+ public async Task> CreateTrainer(CreateTrainerDto createTrainerDto)
+ {
+ var trainer = new Trainer
+ {
+ Name = createTrainerDto.Name,
+ Email = createTrainerDto.Email
+ };
+
+ var createdTrainer = await _trainerService.CreateTrainerAsync(trainer);
+
+ var trainerDto = new TrainerDto
+ {
+ Id = createdTrainer.Id,
+ Name = createdTrainer.Name,
+ Email = createdTrainer.Email,
+ Level = createdTrainer.Level
+ };
+
+ return CreatedAtAction(nameof(GetTrainer), new { id = createdTrainer.Id }, trainerDto);
+ }
+
+ [HttpPost("{trainerId}/pokemons")]
+ public async Task AddPokemonToTrainer(int trainerId, [FromBody] AddPokemonDto addPokemonDto)
+ {
+ await _trainerService.AddPokemonToTrainerAsync(trainerId, addPokemonDto.PokemonId, addPokemonDto.IsShiny);
+ return NoContent();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/Program.cs b/src/Api/Program.cs
new file mode 100644
index 0000000..1760230
--- /dev/null
+++ b/src/Api/Program.cs
@@ -0,0 +1,37 @@
+using Application;
+using Infrastructure;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+
+// Registra los servicios de las capas de Aplicación e Infraestructura.
+builder.Services.AddApplication();
+builder.Services.AddInfrastructure(builder.Configuration);
+
+builder.Services.AddControllers();
+// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(options =>
+{
+ // Construye la ruta al archivo de documentación XML.
+ var xmlFilename = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml";
+ options.IncludeXmlComments(System.IO.Path.Combine(AppContext.BaseDirectory, xmlFilename));
+});
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+app.Run();
diff --git a/src/Api/Properties/launchSettings.json b/src/Api/Properties/launchSettings.json
new file mode 100644
index 0000000..0131937
--- /dev/null
+++ b/src/Api/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:19743",
+ "sslPort": 44320
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5146",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7031;http://localhost:5146",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/src/Api/appsettings.Development.json b/src/Api/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/src/Api/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/src/Api/appsettings.json b/src/Api/appsettings.json
new file mode 100644
index 0000000..9bffa31
--- /dev/null
+++ b/src/Api/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "ConnectionStrings": {
+ "DefaultConnection": "Data Source=pokemon.db"
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj
new file mode 100644
index 0000000..0cab673
--- /dev/null
+++ b/src/Application/Application.csproj
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/src/Application/DependencyInjection.cs b/src/Application/DependencyInjection.cs
new file mode 100644
index 0000000..a1ed849
--- /dev/null
+++ b/src/Application/DependencyInjection.cs
@@ -0,0 +1,23 @@
+using Application.Interfaces;
+using Application.Services;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Application
+{
+ ///
+ /// Clase para registrar las dependencias de la capa de aplicación.
+ ///
+ public static class DependencyInjection
+ {
+ public static IServiceCollection AddApplication(this IServiceCollection services)
+ {
+ // Registra los servicios de la aplicación con sus interfaces.
+ // Esto permite que la capa de presentación (API) los inyecte.
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+
+ return services;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Application/Dtos/AddPokemonDto.cs b/src/Application/Dtos/AddPokemonDto.cs
new file mode 100644
index 0000000..39f252e
--- /dev/null
+++ b/src/Application/Dtos/AddPokemonDto.cs
@@ -0,0 +1,11 @@
+namespace Application.Dtos
+{
+ ///
+ /// DTO para agregar un Pokémon a un entrenador.
+ ///
+ public class AddPokemonDto
+ {
+ public int PokemonId { get; set; }
+ public bool IsShiny { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Application/Dtos/BattleDto.cs b/src/Application/Dtos/BattleDto.cs
new file mode 100644
index 0000000..dfd0b9b
--- /dev/null
+++ b/src/Application/Dtos/BattleDto.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace Application.Dtos
+{
+ ///
+ /// DTO para transferir datos de una Batalla.
+ ///
+ public class BattleDto
+ {
+ public int Id { get; set; }
+ public int TrainerId { get; set; }
+ public required string OpponentName { get; set; }
+ public string? Winner { get; set; }
+ public DateTime Date { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Application/Dtos/BattleResultDto.cs b/src/Application/Dtos/BattleResultDto.cs
new file mode 100644
index 0000000..9945dbf
--- /dev/null
+++ b/src/Application/Dtos/BattleResultDto.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace Application.Dtos
+{
+ ///
+ /// DTO para devolver el resultado de una batalla.
+ ///
+ public class BattleResultDto
+ {
+ public int BattleId { get; set; }
+ public int Trainer1Id { get; set; }
+ public string Trainer1Name { get; set; } = string.Empty;
+ public int Trainer2Id { get; set; }
+ public string Trainer2Name { get; set; } = string.Empty;
+ public int? WinnerId { get; set; }
+ public string? WinnerName { get; set; }
+ public DateTime Date { get; set; }
+ public List Logs { get; set; } = new List();
+ }
+}
\ No newline at end of file
diff --git a/src/Application/Dtos/CreatePokemonDto.cs b/src/Application/Dtos/CreatePokemonDto.cs
new file mode 100644
index 0000000..59c3c53
--- /dev/null
+++ b/src/Application/Dtos/CreatePokemonDto.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+
+namespace Application.Dtos
+{
+ ///
+ /// DTO para crear un nuevo Pokémon.
+ ///
+ public class CreatePokemonDto
+ {
+ public required string Name { get; set; }
+ public string? Element { get; set; }
+ public int Hp { get; set; }
+ public int Attack { get; set; }
+ public int Defense { get; set; }
+ public int SpecialAttack { get; set; }
+ public int SpecialDefense { get; set; }
+ public int Speed { get; set; }
+ public List Moves { get; set; } = new List();
+ public int Level { get; set; } = 1;
+ }
+}
\ No newline at end of file
diff --git a/src/Application/Dtos/CreateTrainerDto.cs b/src/Application/Dtos/CreateTrainerDto.cs
new file mode 100644
index 0000000..de4bc0d
--- /dev/null
+++ b/src/Application/Dtos/CreateTrainerDto.cs
@@ -0,0 +1,11 @@
+namespace Application.Dtos
+{
+ ///
+ /// DTO para crear un nuevo Entrenador.
+ ///
+ public class CreateTrainerDto
+ {
+ public required string Name { get; set; }
+ public required string Email { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Application/Dtos/PokemonDto.cs b/src/Application/Dtos/PokemonDto.cs
new file mode 100644
index 0000000..9679bff
--- /dev/null
+++ b/src/Application/Dtos/PokemonDto.cs
@@ -0,0 +1,20 @@
+namespace Application.Dtos
+{
+ ///
+ /// DTO para transferir datos de un Pokémon.
+ ///
+ public class PokemonDto
+ {
+ public int Id { get; set; }
+ public required string Name { get; set; }
+ public string? Element { get; set; }
+ public int Hp { get; set; }
+ public int Attack { get; set; }
+ public int Defense { get; set; }
+ public int SpecialAttack { get; set; }
+ public int SpecialDefense { get; set; }
+ public int Speed { get; set; }
+ public List Moves { get; set; } = new List();
+ public int Level { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Application/Dtos/StartBattleDto.cs b/src/Application/Dtos/StartBattleDto.cs
new file mode 100644
index 0000000..5610643
--- /dev/null
+++ b/src/Application/Dtos/StartBattleDto.cs
@@ -0,0 +1,18 @@
+namespace Application.Dtos
+{
+ ///
+ /// DTO para iniciar una nueva batalla.
+ ///
+ public class StartBattleDto
+ {
+ ///
+ /// ID del primer entrenador (el retador).
+ ///
+ public int Trainer1Id { get; set; }
+
+ ///
+ /// ID del segundo entrenador (el oponente).
+ ///
+ public int Trainer2Id { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Application/Dtos/TrainerDto.cs b/src/Application/Dtos/TrainerDto.cs
new file mode 100644
index 0000000..b960dff
--- /dev/null
+++ b/src/Application/Dtos/TrainerDto.cs
@@ -0,0 +1,14 @@
+namespace Application.Dtos
+{
+ ///
+ /// DTO para transferir datos de un Entrenador.
+ ///
+ public class TrainerDto
+ {
+ public int Id { get; set; }
+ public required string Name { get; set; }
+ public required string Email { get; set; }
+ public int Level { get; set; }
+ public List Pokemons { get; set; } = new List();
+ }
+}
\ No newline at end of file
diff --git a/src/Application/Interfaces/IBattleService.cs b/src/Application/Interfaces/IBattleService.cs
new file mode 100644
index 0000000..82d6b71
--- /dev/null
+++ b/src/Application/Interfaces/IBattleService.cs
@@ -0,0 +1,27 @@
+using Application.Dtos;
+using Core.Entities;
+using System.Threading.Tasks;
+
+namespace Application.Interfaces
+{
+ ///
+ /// Define el contrato para el servicio de gestión de Batallas.
+ ///
+ public interface IBattleService
+ {
+ ///
+ /// Simula una batalla entre dos entrenadores y guarda el resultado.
+ ///
+ /// El ID del primer entrenador.
+ /// El ID del segundo entrenador.
+ /// El resultado de la batalla.
+ Task SimulateBattleAsync(int trainer1Id, int trainer2Id);
+
+ ///
+ /// Obtiene una batalla por su ID.
+ ///
+ /// El ID de la batalla.
+ /// La batalla encontrada, o null si no existe.
+ Task GetBattleByIdAsync(int id);
+ }
+}
\ No newline at end of file
diff --git a/src/Application/Interfaces/IPokemonService.cs b/src/Application/Interfaces/IPokemonService.cs
new file mode 100644
index 0000000..e3cc4e8
--- /dev/null
+++ b/src/Application/Interfaces/IPokemonService.cs
@@ -0,0 +1,44 @@
+using Core.Entities;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Application.Interfaces
+{
+ ///
+ /// Define el contrato para el servicio de gestión de Pokémon.
+ ///
+ public interface IPokemonService
+ {
+ ///
+ /// Obtiene todos los Pokémon.
+ ///
+ /// Una lista de todos los Pokémon.
+ Task> GetAllPokemonsAsync();
+
+ ///
+ /// Obtiene un Pokémon por su ID.
+ ///
+ /// El ID del Pokémon.
+ /// El Pokémon encontrado, o null si no existe.
+ Task GetPokemonByIdAsync(int id);
+
+ ///
+ /// Crea un nuevo Pokémon.
+ ///
+ /// El Pokémon a crear.
+ /// El Pokémon creado.
+ Task CreatePokemonAsync(Pokemon pokemon);
+
+ ///
+ /// Actualiza un Pokémon existente.
+ ///
+ /// El Pokémon a actualizar.
+ Task UpdatePokemonAsync(Pokemon pokemon);
+
+ ///
+ /// Elimina un Pokémon por su ID.
+ ///
+ /// El ID del Pokémon a eliminar.
+ Task DeletePokemonAsync(int id);
+ }
+}
\ No newline at end of file
diff --git a/src/Application/Interfaces/IRepository.cs b/src/Application/Interfaces/IRepository.cs
new file mode 100644
index 0000000..4f64942
--- /dev/null
+++ b/src/Application/Interfaces/IRepository.cs
@@ -0,0 +1,45 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Application.Interfaces
+{
+ ///
+ /// Define un contrato genérico para las operaciones de repositorio.
+ /// Esto nos permite abstraer el acceso a datos para cualquier entidad.
+ ///
+ /// La entidad con la que trabajará el repositorio.
+ public interface IRepository where T : class
+ {
+ ///
+ /// Obtiene una entidad por su ID.
+ ///
+ /// El ID de la entidad.
+ /// La entidad encontrada, o null si no existe.
+ Task GetByIdAsync(int id);
+
+ ///
+ /// Obtiene todas las entidades de un tipo.
+ ///
+ /// Una lista de todas las entidades.
+ Task> ListAllAsync();
+
+ ///
+ /// Agrega una nueva entidad a la base de datos.
+ ///
+ /// La entidad a agregar.
+ /// La entidad agregada.
+ Task AddAsync(T entity);
+
+ ///
+ /// Actualiza una entidad existente.
+ ///
+ /// La entidad a actualizar.
+ Task UpdateAsync(T entity);
+
+ ///
+ /// Elimina una entidad de la base de datos.
+ ///
+ /// La entidad a eliminar.
+ Task DeleteAsync(T entity);
+ }
+}
\ No newline at end of file
diff --git a/src/Application/Interfaces/ITrainerRepository.cs b/src/Application/Interfaces/ITrainerRepository.cs
new file mode 100644
index 0000000..6bd9dbe
--- /dev/null
+++ b/src/Application/Interfaces/ITrainerRepository.cs
@@ -0,0 +1,18 @@
+using Core.Entities;
+using System.Threading.Tasks;
+
+namespace Application.Interfaces
+{
+ ///
+ /// Repositorio específico para la entidad Trainer, extendiendo el genérico.
+ ///
+ public interface ITrainerRepository : IRepository
+ {
+ ///
+ /// Obtiene un entrenador por su ID, incluyendo su lista de Pokémon.
+ ///
+ /// El ID del entrenador.
+ /// El entrenador con su equipo de Pokémon, o null si no se encuentra.
+ Task GetByIdWithPokemonsAsync(int id);
+ }
+}
\ No newline at end of file
diff --git a/src/Application/Interfaces/ITrainerService.cs b/src/Application/Interfaces/ITrainerService.cs
new file mode 100644
index 0000000..d4b96e2
--- /dev/null
+++ b/src/Application/Interfaces/ITrainerService.cs
@@ -0,0 +1,40 @@
+using Core.Entities;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Application.Interfaces
+{
+ ///
+ /// Define el contrato para el servicio de gestión de Entrenadores.
+ ///
+ public interface ITrainerService
+ {
+ ///
+ /// Obtiene todos los entrenadores.
+ ///
+ /// Una lista de todos los entrenadores.
+ Task> GetAllTrainersAsync();
+
+ ///
+ /// Obtiene un entrenador por su ID.
+ ///
+ /// El ID del entrenador.
+ /// El entrenador encontrado, o null si no existe.
+ Task GetTrainerByIdAsync(int id);
+
+ ///
+ /// Crea un nuevo entrenador.
+ ///
+ /// El entrenador a crear.
+ /// El entrenador creado.
+ Task CreateTrainerAsync(Trainer trainer);
+
+ ///
+ /// Agrega un Pokémon a la colección de un entrenador.
+ ///
+ /// El ID del entrenador.
+ /// El ID del Pokémon a agregar.
+ /// Indica si el Pokémon es shiny.
+ Task AddPokemonToTrainerAsync(int trainerId, int pokemonId, bool isShiny);
+ }
+}
\ No newline at end of file
diff --git a/src/Application/Services/BattleService.cs b/src/Application/Services/BattleService.cs
new file mode 100644
index 0000000..2df7312
--- /dev/null
+++ b/src/Application/Services/BattleService.cs
@@ -0,0 +1,118 @@
+using Application.Dtos;
+using Application.Interfaces;
+using Core.Entities;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Application.Services
+{
+ ///
+ /// Implementa la lógica de negocio para la gestión de Batallas.
+ ///
+ public class BattleService : IBattleService
+ {
+ private readonly IRepository _battleRepository;
+ private readonly ITrainerRepository _trainerRepository;
+
+ public BattleService(IRepository battleRepository, ITrainerRepository trainerRepository)
+ {
+ _battleRepository = battleRepository;
+ _trainerRepository = trainerRepository;
+ }
+
+ public async Task SimulateBattleAsync(int trainer1Id, int trainer2Id)
+ {
+ var trainer1 = await _trainerRepository.GetByIdWithPokemonsAsync(trainer1Id);
+ var trainer2 = await _trainerRepository.GetByIdWithPokemonsAsync(trainer2Id);
+
+ if (trainer1 == null || trainer2 == null)
+ throw new Exception("Uno o ambos entrenadores no fueron encontrados.");
+ if (!trainer1.Pokemons.Any() || !trainer2.Pokemons.Any())
+ throw new Exception("Uno o ambos entrenadores no tienen Pokémon para luchar.");
+
+ var battle = new Battle
+ {
+ TrainerId = trainer1.Id,
+ OpponentName = trainer2.Name,
+ Date = DateTime.UtcNow
+ };
+
+ var team1 = trainer1.Pokemons.Select(p => p.Pokemon).ToList();
+ var team2 = trainer2.Pokemons.Select(p => p.Pokemon).ToList();
+ team1.ForEach(p => p.CurrentHp = p.Hp);
+ team2.ForEach(p => p.CurrentHp = p.Hp);
+
+ int turn = 1;
+ var logs = new List();
+
+ while (team1.Any(p => p.CurrentHp > 0) && team2.Any(p => p.CurrentHp > 0))
+ {
+ var activePokemon1 = team1.First(p => p.CurrentHp > 0);
+ var activePokemon2 = team2.First(p => p.CurrentHp > 0);
+
+ Pokemon attacker, defender;
+ if (activePokemon1.Speed >= activePokemon2.Speed)
+ {
+ attacker = activePokemon1;
+ defender = activePokemon2;
+ }
+ else
+ {
+ attacker = activePokemon2;
+ defender = activePokemon1;
+ }
+
+ // Primer ataque
+ int damage = Math.Max(1, (attacker.Attack * 10) / defender.Defense);
+ defender.CurrentHp = Math.Max(0, defender.CurrentHp - damage);
+ logs.Add(new BattleLog { TurnNumber = turn, Action = $"{attacker.Name} ataca a {defender.Name} e inflige {damage} de daño. {defender.Name} tiene {defender.CurrentHp} HP restante." });
+
+ if (defender.CurrentHp <= 0)
+ {
+ logs.Add(new BattleLog { TurnNumber = turn, Action = $"{defender.Name} ha sido debilitado." });
+ continue; // Pasa al siguiente turno si el defensor es debilitado
+ }
+
+ // Segundo ataque (el otro Pokémon)
+ attacker = defender; // El defensor ahora es el atacante
+ defender = (attacker == activePokemon1) ? activePokemon2 : activePokemon1;
+
+ damage = Math.Max(1, (attacker.Attack * 10) / defender.Defense);
+ defender.CurrentHp = Math.Max(0, defender.CurrentHp - damage);
+ logs.Add(new BattleLog { TurnNumber = turn, Action = $"{attacker.Name} ataca a {defender.Name} e inflige {damage} de daño. {defender.Name} tiene {defender.CurrentHp} HP restante." });
+
+ if (defender.CurrentHp <= 0)
+ {
+ logs.Add(new BattleLog { TurnNumber = turn, Action = $"{defender.Name} ha sido debilitado." });
+ }
+
+ turn++;
+ }
+
+ Trainer? winner = team1.Any(p => p.CurrentHp > 0) ? trainer1 : trainer2;
+ battle.Winner = winner.Name;
+ battle.Logs = logs;
+
+ var createdBattle = await _battleRepository.AddAsync(battle);
+
+ return new BattleResultDto
+ {
+ BattleId = createdBattle.Id,
+ Trainer1Id = trainer1.Id,
+ Trainer1Name = trainer1.Name,
+ Trainer2Id = trainer2.Id,
+ Trainer2Name = trainer2.Name,
+ WinnerId = winner.Id,
+ WinnerName = winner.Name,
+ Date = createdBattle.Date,
+ Logs = createdBattle.Logs.Select(l => $"Turno {l.TurnNumber}: {l.Action}").ToList()
+ };
+ }
+
+ public async Task GetBattleByIdAsync(int id)
+ {
+ return await _battleRepository.GetByIdAsync(id);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Application/Services/PokemonService.cs b/src/Application/Services/PokemonService.cs
new file mode 100644
index 0000000..133c82a
--- /dev/null
+++ b/src/Application/Services/PokemonService.cs
@@ -0,0 +1,53 @@
+using Application.Interfaces;
+using Core.Entities;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Application.Services
+{
+ ///
+ /// Implementa la lógica de negocio para la gestión de Pokémon.
+ ///
+ public class PokemonService : IPokemonService
+ {
+ private readonly IRepository _pokemonRepository;
+
+ ///
+ /// Inicializa una nueva instancia del servicio de Pokémon.
+ ///
+ /// El repositorio para las operaciones de datos de Pokémon.
+ public PokemonService(IRepository pokemonRepository)
+ {
+ _pokemonRepository = pokemonRepository;
+ }
+
+ public async Task> GetAllPokemonsAsync()
+ {
+ return await _pokemonRepository.ListAllAsync();
+ }
+
+ public async Task GetPokemonByIdAsync(int id)
+ {
+ return await _pokemonRepository.GetByIdAsync(id);
+ }
+
+ public async Task CreatePokemonAsync(Pokemon pokemon)
+ {
+ return await _pokemonRepository.AddAsync(pokemon);
+ }
+
+ public async Task UpdatePokemonAsync(Pokemon pokemon)
+ {
+ await _pokemonRepository.UpdateAsync(pokemon);
+ }
+
+ public async Task DeletePokemonAsync(int id)
+ {
+ var pokemon = await _pokemonRepository.GetByIdAsync(id);
+ if (pokemon != null)
+ {
+ await _pokemonRepository.DeleteAsync(pokemon);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Application/Services/TrainerService.cs b/src/Application/Services/TrainerService.cs
new file mode 100644
index 0000000..fb06da2
--- /dev/null
+++ b/src/Application/Services/TrainerService.cs
@@ -0,0 +1,59 @@
+using Application.Interfaces;
+using Core.Entities;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Application.Services
+{
+ ///
+ /// Implementa la lógica de negocio para la gestión de Entrenadores.
+ ///
+ public class TrainerService : ITrainerService
+ {
+ private readonly IRepository _trainerRepository;
+ private readonly IRepository _pokemonRepository;
+ private readonly IRepository _trainerPokemonRepository;
+
+ public TrainerService(
+ IRepository trainerRepository,
+ IRepository pokemonRepository,
+ IRepository trainerPokemonRepository)
+ {
+ _trainerRepository = trainerRepository;
+ _pokemonRepository = pokemonRepository;
+ _trainerPokemonRepository = trainerPokemonRepository;
+ }
+
+ public async Task> GetAllTrainersAsync()
+ {
+ return await _trainerRepository.ListAllAsync();
+ }
+
+ public async Task GetTrainerByIdAsync(int id)
+ {
+ return await _trainerRepository.GetByIdAsync(id);
+ }
+
+ public async Task CreateTrainerAsync(Trainer trainer)
+ {
+ return await _trainerRepository.AddAsync(trainer);
+ }
+
+ public async Task AddPokemonToTrainerAsync(int trainerId, int pokemonId, bool isShiny)
+ {
+ var trainer = await _trainerRepository.GetByIdAsync(trainerId);
+ var pokemon = await _pokemonRepository.GetByIdAsync(pokemonId);
+
+ if (trainer != null && pokemon != null)
+ {
+ var trainerPokemon = new TrainerPokemon
+ {
+ TrainerId = trainerId,
+ PokemonId = pokemonId,
+ IsShiny = isShiny
+ };
+ await _trainerPokemonRepository.AddAsync(trainerPokemon);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Core/Class1.cs b/src/Core/Class1.cs
new file mode 100644
index 0000000..bbbaf24
--- /dev/null
+++ b/src/Core/Class1.cs
@@ -0,0 +1,6 @@
+namespace Core;
+
+public class Class1
+{
+
+}
diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj
new file mode 100644
index 0000000..fa71b7a
--- /dev/null
+++ b/src/Core/Core.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/src/Core/Entities/Admin.cs b/src/Core/Entities/Admin.cs
new file mode 100644
index 0000000..bece61f
--- /dev/null
+++ b/src/Core/Entities/Admin.cs
@@ -0,0 +1,38 @@
+namespace Core.Entities
+{
+ ///
+ /// Representa un administrador del sistema.
+ ///
+ public class Admin
+ {
+ ///
+ /// ID único del administrador.
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Nombre de usuario para el inicio de sesión. Debe ser único.
+ ///
+ public required string Username { get; set; }
+
+ ///
+ /// Hash de la contraseña del administrador.
+ ///
+ public required string HashedPassword { get; set; }
+
+ ///
+ /// Correo electrónico del administrador. Debe ser único.
+ ///
+ public required string Email { get; set; }
+
+ ///
+ /// Indica si la cuenta del administrador está activa.
+ ///
+ public bool IsActive { get; set; } = true;
+
+ ///
+ /// Indica si el administrador tiene privilegios de superadministrador.
+ ///
+ public bool IsSuperAdmin { get; set; } = false;
+ }
+}
\ No newline at end of file
diff --git a/src/Core/Entities/Battle.cs b/src/Core/Entities/Battle.cs
new file mode 100644
index 0000000..78492e6
--- /dev/null
+++ b/src/Core/Entities/Battle.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+
+namespace Core.Entities
+{
+ ///
+ /// Representa una batalla Pokémon entre un entrenador y un oponente.
+ ///
+ public class Battle
+ {
+ ///
+ /// ID único de la batalla.
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// ID del entrenador que participa en la batalla.
+ ///
+ public int TrainerId { get; set; }
+
+ ///
+ /// Nombre del oponente en la batalla.
+ ///
+ public required string OpponentName { get; set; }
+
+ ///
+ /// Nombre del ganador de la batalla. Puede ser nulo en caso de empate.
+ ///
+ public string? Winner { get; set; }
+
+ ///
+ /// Fecha y hora en que se realizó la batalla.
+ ///
+ public DateTime Date { get; set; } = DateTime.UtcNow;
+
+ // Propiedades de navegación
+
+ ///
+ /// Referencia al entrenador que participó en la batalla.
+ ///
+ public Trainer Trainer { get; set; } = null!;
+
+ ///
+ /// Pokémon que participaron en esta batalla.
+ ///
+ public ICollection Pokemons { get; set; } = new List();
+
+ ///
+ /// Registros de eventos que ocurrieron durante la batalla.
+ ///
+ public ICollection Logs { get; set; } = new List();
+ }
+}
\ No newline at end of file
diff --git a/src/Core/Entities/BattleLog.cs b/src/Core/Entities/BattleLog.cs
new file mode 100644
index 0000000..7f37a57
--- /dev/null
+++ b/src/Core/Entities/BattleLog.cs
@@ -0,0 +1,35 @@
+namespace Core.Entities
+{
+ ///
+ /// Registra un evento que ocurrió durante una batalla.
+ ///
+ public class BattleLog
+ {
+ ///
+ /// ID único del log.
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// ID de la batalla a la que pertenece este log.
+ ///
+ public int BattleId { get; set; }
+
+ ///
+ /// El número de turno en el que ocurrió el evento.
+ ///
+ public int TurnNumber { get; set; }
+
+ ///
+ /// Descripción de la acción que tuvo lugar (ej: "Pikachu usó Placaje").
+ ///
+ public required string Action { get; set; }
+
+ // Propiedad de navegación
+
+ ///
+ /// Referencia a la Batalla.
+ ///
+ public Battle Battle { get; set; } = null!;
+ }
+}
\ No newline at end of file
diff --git a/src/Core/Entities/BattlePokemon.cs b/src/Core/Entities/BattlePokemon.cs
new file mode 100644
index 0000000..e731305
--- /dev/null
+++ b/src/Core/Entities/BattlePokemon.cs
@@ -0,0 +1,45 @@
+namespace Core.Entities
+{
+ ///
+ /// Modelo de relación que asocia una Batalla con un Pokémon participante.
+ ///
+ public class BattlePokemon
+ {
+ ///
+ /// ID único de esta entrada de participación.
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// ID de la batalla.
+ ///
+ public int BattleId { get; set; }
+
+ ///
+ /// ID del Pokémon.
+ ///
+ public int PokemonId { get; set; }
+
+ ///
+ /// HP restante del Pokémon al finalizar la batalla.
+ ///
+ public int HpRemaining { get; set; }
+
+ ///
+ /// Indica si el Pokémon participó activamente en el combate.
+ ///
+ public bool Participated { get; set; } = false;
+
+ // Propiedades de navegación
+
+ ///
+ /// Referencia a la Batalla.
+ ///
+ public Battle Battle { get; set; } = null!;
+
+ ///
+ /// Referencia al Pokémon.
+ ///
+ public Pokemon Pokemon { get; set; } = null!;
+ }
+}
\ No newline at end of file
diff --git a/src/Core/Entities/Pokemon.cs b/src/Core/Entities/Pokemon.cs
new file mode 100644
index 0000000..16c915c
--- /dev/null
+++ b/src/Core/Entities/Pokemon.cs
@@ -0,0 +1,72 @@
+namespace Core.Entities
+{
+ ///
+ /// Representa un Pokémon en el sistema.
+ /// Contiene todos los atributos y estadísticas base de un Pokémon.
+ ///
+ public class Pokemon
+ {
+ ///
+ /// ID único del Pokémon en la base de datos.
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Nombre del Pokémon (ej: "Pikachu", "Charizard").
+ /// Es un campo obligatorio.
+ ///
+ public required string Name { get; set; }
+
+ ///
+ /// Tipo(s) elemental(es) del Pokémon (ej: "Fuego", "Agua/Volador").
+ ///
+ public string? Element { get; set; }
+
+ ///
+ /// Puntos de Salud (HP) base del Pokémon.
+ ///
+ public int Hp { get; set; }
+
+ ///
+ /// Estadística de Ataque físico base.
+ ///
+ public int Attack { get; set; }
+
+ ///
+ /// Estadística de Defensa física base.
+ ///
+ public int Defense { get; set; }
+
+ ///
+ /// Estadística de Ataque Especial base.
+ ///
+ public int SpecialAttack { get; set; }
+
+ ///
+ /// Estadística de Defensa Especial base.
+ ///
+ public int SpecialDefense { get; set; }
+
+ ///
+ /// Estadística de Velocidad base.
+ ///
+ public int Speed { get; set; }
+
+ ///
+ /// Lista de movimientos que el Pokémon puede aprender o usar.
+ ///
+ public List Moves { get; set; } = new List();
+
+ ///
+ /// Nivel actual del Pokémon.
+ ///
+ public int Level { get; set; } = 1;
+
+ ///
+ /// HP actual del Pokémon, usado en combates.
+ /// No se mapea a la base de datos ya que es un estado temporal.
+ ///
+ [System.ComponentModel.DataAnnotations.Schema.NotMapped]
+ public int CurrentHp { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Core/Entities/Trainer.cs b/src/Core/Entities/Trainer.cs
new file mode 100644
index 0000000..6c47cbc
--- /dev/null
+++ b/src/Core/Entities/Trainer.cs
@@ -0,0 +1,42 @@
+namespace Core.Entities
+{
+ ///
+ /// Representa un Entrenador Pokémon en el sistema.
+ /// Contiene la información básica y las relaciones del entrenador.
+ ///
+ public class Trainer
+ {
+ ///
+ /// ID único del entrenador.
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Nombre del entrenador. Es un campo obligatorio.
+ ///
+ public required string Name { get; set; }
+
+ ///
+ /// Correo electrónico del entrenador. Debe ser único.
+ ///
+ public required string Email { get; set; }
+
+ ///
+ /// Nivel del entrenador, que puede aumentar con la experiencia.
+ ///
+ public int Level { get; set; } = 1;
+
+ // Propiedades de navegación para las relaciones
+
+ ///
+ /// Colección de los Pokémon que pertenecen a este entrenador.
+ /// Esta es la tabla de unión entre Trainer y Pokemon.
+ ///
+ public ICollection Pokemons { get; set; } = new List();
+
+ ///
+ /// Historial de batallas en las que ha participado el entrenador.
+ ///
+ public ICollection Battles { get; set; } = new List();
+ }
+}
\ No newline at end of file
diff --git a/src/Core/Entities/TrainerPokemon.cs b/src/Core/Entities/TrainerPokemon.cs
new file mode 100644
index 0000000..9d74f5f
--- /dev/null
+++ b/src/Core/Entities/TrainerPokemon.cs
@@ -0,0 +1,36 @@
+namespace Core.Entities
+{
+ ///
+ /// Modelo de relación que asocia un Entrenador con un Pokémon.
+ /// Representa una instancia de un Pokémon que pertenece a un entrenador.
+ ///
+ public class TrainerPokemon
+ {
+ ///
+ /// ID del entrenador.
+ ///
+ public int TrainerId { get; set; }
+
+ ///
+ /// ID del Pokémon.
+ ///
+ public int PokemonId { get; set; }
+
+ ///
+ /// Indica si esta instancia específica del Pokémon es una variante "shiny".
+ ///
+ public bool IsShiny { get; set; } = false;
+
+ // Propiedades de navegación para acceder a las entidades relacionadas
+
+ ///
+ /// Referencia al Entrenador.
+ ///
+ public Trainer Trainer { get; set; } = null!;
+
+ ///
+ /// Referencia al Pokémon.
+ ///
+ public Pokemon Pokemon { get; set; } = null!;
+ }
+}
\ No newline at end of file
diff --git a/src/Infrastructure/Class1.cs b/src/Infrastructure/Class1.cs
new file mode 100644
index 0000000..cffa0c9
--- /dev/null
+++ b/src/Infrastructure/Class1.cs
@@ -0,0 +1,6 @@
+namespace Infrastructure;
+
+public class Class1
+{
+
+}
diff --git a/src/Infrastructure/Data/AppDbContext.cs b/src/Infrastructure/Data/AppDbContext.cs
new file mode 100644
index 0000000..71c0736
--- /dev/null
+++ b/src/Infrastructure/Data/AppDbContext.cs
@@ -0,0 +1,54 @@
+using Core.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace Infrastructure.Data
+{
+ ///
+ /// Contexto de la base de datos para la aplicación.
+ /// Representa la sesión con la base de datos y permite consultar y guardar entidades.
+ ///
+ public class AppDbContext : DbContext
+ {
+ public AppDbContext(DbContextOptions options) : base(options)
+ {
+ }
+
+ // Define los DbSet para cada entidad que EF Core debe gestionar.
+ public DbSet Pokemons { get; set; }
+ public DbSet Trainers { get; set; }
+ public DbSet TrainerPokemons { get; set; }
+ public DbSet Battles { get; set; }
+ public DbSet BattlePokemons { get; set; }
+ public DbSet BattleLogs { get; set; }
+ public DbSet Admins { get; set; }
+
+ ///
+ /// Configura el modelo de la base de datos, incluyendo claves primarias,
+ /// claves foráneas y relaciones.
+ ///
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ base.OnModelCreating(modelBuilder);
+
+ // Configuración para la entidad TrainerPokemon (relación muchos a muchos)
+ modelBuilder.Entity()
+ .HasKey(tp => new { tp.TrainerId, tp.PokemonId }); // Clave primaria compuesta
+
+ modelBuilder.Entity()
+ .HasOne(tp => tp.Trainer)
+ .WithMany(t => t.Pokemons)
+ .HasForeignKey(tp => tp.TrainerId);
+
+ modelBuilder.Entity()
+ .HasOne(tp => tp.Pokemon)
+ .WithMany() // Un Pokémon puede estar en muchas colecciones de entrenadores
+ .HasForeignKey(tp => tp.PokemonId);
+
+ // Configuración para la entidad Battle
+ modelBuilder.Entity()
+ .HasOne(b => b.Trainer)
+ .WithMany(t => t.Battles)
+ .HasForeignKey(b => b.TrainerId);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Infrastructure/Data/EfRepository.cs b/src/Infrastructure/Data/EfRepository.cs
new file mode 100644
index 0000000..93208cf
--- /dev/null
+++ b/src/Infrastructure/Data/EfRepository.cs
@@ -0,0 +1,50 @@
+using Application.Interfaces;
+using Microsoft.EntityFrameworkCore;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Infrastructure.Data
+{
+ ///
+ /// Implementación genérica del repositorio utilizando Entity Framework Core.
+ ///
+ /// La entidad con la que trabajará el repositorio.
+ public class EfRepository : IRepository where T : class
+ {
+ protected readonly AppDbContext _dbContext;
+
+ public EfRepository(AppDbContext dbContext)
+ {
+ _dbContext = dbContext;
+ }
+
+ public async Task GetByIdAsync(int id)
+ {
+ return await _dbContext.Set().FindAsync(id);
+ }
+
+ public async Task> ListAllAsync()
+ {
+ return await _dbContext.Set().ToListAsync();
+ }
+
+ public async Task AddAsync(T entity)
+ {
+ await _dbContext.Set().AddAsync(entity);
+ await _dbContext.SaveChangesAsync();
+ return entity;
+ }
+
+ public async Task UpdateAsync(T entity)
+ {
+ _dbContext.Entry(entity).State = EntityState.Modified;
+ await _dbContext.SaveChangesAsync();
+ }
+
+ public async Task DeleteAsync(T entity)
+ {
+ _dbContext.Set().Remove(entity);
+ await _dbContext.SaveChangesAsync();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Infrastructure/Data/TrainerRepository.cs b/src/Infrastructure/Data/TrainerRepository.cs
new file mode 100644
index 0000000..f649fd0
--- /dev/null
+++ b/src/Infrastructure/Data/TrainerRepository.cs
@@ -0,0 +1,28 @@
+using Application.Interfaces;
+using Core.Entities;
+using Microsoft.EntityFrameworkCore;
+using System.Threading.Tasks;
+
+namespace Infrastructure.Data
+{
+ ///
+ /// Implementación del repositorio específico para la entidad Trainer.
+ ///
+ public class TrainerRepository : EfRepository, ITrainerRepository
+ {
+ public TrainerRepository(AppDbContext dbContext) : base(dbContext)
+ {
+ }
+
+ ///
+ /// Obtiene un entrenador por su ID, incluyendo su lista de Pokémon.
+ ///
+ public async Task GetByIdWithPokemonsAsync(int id)
+ {
+ return await _dbContext.Trainers
+ .Include(t => t.Pokemons)
+ .ThenInclude(tp => tp.Pokemon)
+ .FirstOrDefaultAsync(t => t.Id == id);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs
new file mode 100644
index 0000000..7a99e4c
--- /dev/null
+++ b/src/Infrastructure/DependencyInjection.cs
@@ -0,0 +1,31 @@
+using Application.Interfaces;
+using Infrastructure.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Infrastructure
+{
+ ///
+ /// Clase para registrar las dependencias de la capa de infraestructura.
+ ///
+ public static class DependencyInjection
+ {
+ public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
+ {
+ // Configura el DbContext para usar SQLite.
+ // La cadena de conexión se obtiene del archivo de configuración (appsettings.json).
+ services.AddDbContext(options =>
+ options.UseSqlite(configuration.GetConnectionString("DefaultConnection")));
+
+ // Registra el repositorio genérico.
+ // Esto permite que cualquier servicio pueda inyectar IRepository.
+ services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
+
+ // Registra repositorios específicos.
+ services.AddScoped();
+
+ return services;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj
new file mode 100644
index 0000000..d77410c
--- /dev/null
+++ b/src/Infrastructure/Infrastructure.csproj
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/src/Infrastructure/Migrations/20250925013450_InitialCreate.Designer.cs b/src/Infrastructure/Migrations/20250925013450_InitialCreate.Designer.cs
new file mode 100644
index 0000000..3cc5eaa
--- /dev/null
+++ b/src/Infrastructure/Migrations/20250925013450_InitialCreate.Designer.cs
@@ -0,0 +1,251 @@
+//
+using System;
+using Infrastructure.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Infrastructure.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20250925013450_InitialCreate")]
+ partial class InitialCreate
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.9");
+
+ modelBuilder.Entity("Core.Entities.Admin", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("HashedPassword")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsSuperAdmin")
+ .HasColumnType("INTEGER");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Admins");
+ });
+
+ modelBuilder.Entity("Core.Entities.Battle", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Date")
+ .HasColumnType("TEXT");
+
+ b.Property("OpponentName")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("TrainerId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Winner")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TrainerId");
+
+ b.ToTable("Battles");
+ });
+
+ modelBuilder.Entity("Core.Entities.BattlePokemon", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("BattleId")
+ .HasColumnType("INTEGER");
+
+ b.Property("HpRemaining")
+ .HasColumnType("INTEGER");
+
+ b.Property("Participated")
+ .HasColumnType("INTEGER");
+
+ b.Property("PokemonId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BattleId");
+
+ b.HasIndex("PokemonId");
+
+ b.ToTable("BattlePokemons");
+ });
+
+ modelBuilder.Entity("Core.Entities.Pokemon", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Attack")
+ .HasColumnType("INTEGER");
+
+ b.Property("Defense")
+ .HasColumnType("INTEGER");
+
+ b.Property("Element")
+ .HasColumnType("TEXT");
+
+ b.Property("Hp")
+ .HasColumnType("INTEGER");
+
+ b.Property("Level")
+ .HasColumnType("INTEGER");
+
+ b.PrimitiveCollection("Moves")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("SpecialAttack")
+ .HasColumnType("INTEGER");
+
+ b.Property("SpecialDefense")
+ .HasColumnType("INTEGER");
+
+ b.Property("Speed")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("Pokemons");
+ });
+
+ modelBuilder.Entity("Core.Entities.Trainer", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Level")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Trainers");
+ });
+
+ modelBuilder.Entity("Core.Entities.TrainerPokemon", b =>
+ {
+ b.Property("TrainerId")
+ .HasColumnType("INTEGER");
+
+ b.Property("PokemonId")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsShiny")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("TrainerId", "PokemonId");
+
+ b.HasIndex("PokemonId");
+
+ b.ToTable("TrainerPokemons");
+ });
+
+ modelBuilder.Entity("Core.Entities.Battle", b =>
+ {
+ b.HasOne("Core.Entities.Trainer", "Trainer")
+ .WithMany("Battles")
+ .HasForeignKey("TrainerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Trainer");
+ });
+
+ modelBuilder.Entity("Core.Entities.BattlePokemon", b =>
+ {
+ b.HasOne("Core.Entities.Battle", "Battle")
+ .WithMany("Pokemons")
+ .HasForeignKey("BattleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Core.Entities.Pokemon", "Pokemon")
+ .WithMany()
+ .HasForeignKey("PokemonId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Battle");
+
+ b.Navigation("Pokemon");
+ });
+
+ modelBuilder.Entity("Core.Entities.TrainerPokemon", b =>
+ {
+ b.HasOne("Core.Entities.Pokemon", "Pokemon")
+ .WithMany()
+ .HasForeignKey("PokemonId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Core.Entities.Trainer", "Trainer")
+ .WithMany("Pokemons")
+ .HasForeignKey("TrainerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Pokemon");
+
+ b.Navigation("Trainer");
+ });
+
+ modelBuilder.Entity("Core.Entities.Battle", b =>
+ {
+ b.Navigation("Pokemons");
+ });
+
+ modelBuilder.Entity("Core.Entities.Trainer", b =>
+ {
+ b.Navigation("Battles");
+
+ b.Navigation("Pokemons");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Infrastructure/Migrations/20250925013450_InitialCreate.cs b/src/Infrastructure/Migrations/20250925013450_InitialCreate.cs
new file mode 100644
index 0000000..fa769f7
--- /dev/null
+++ b/src/Infrastructure/Migrations/20250925013450_InitialCreate.cs
@@ -0,0 +1,186 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Infrastructure.Migrations
+{
+ ///
+ public partial class InitialCreate : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "Admins",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ Username = table.Column(type: "TEXT", nullable: false),
+ HashedPassword = table.Column(type: "TEXT", nullable: false),
+ Email = table.Column(type: "TEXT", nullable: false),
+ IsActive = table.Column(type: "INTEGER", nullable: false),
+ IsSuperAdmin = table.Column(type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Admins", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Pokemons",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ Name = table.Column(type: "TEXT", nullable: false),
+ Element = table.Column(type: "TEXT", nullable: true),
+ Hp = table.Column(type: "INTEGER", nullable: false),
+ Attack = table.Column(type: "INTEGER", nullable: false),
+ Defense = table.Column(type: "INTEGER", nullable: false),
+ SpecialAttack = table.Column(type: "INTEGER", nullable: false),
+ SpecialDefense = table.Column(type: "INTEGER", nullable: false),
+ Speed = table.Column(type: "INTEGER", nullable: false),
+ Moves = table.Column(type: "TEXT", nullable: false),
+ Level = table.Column(type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Pokemons", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Trainers",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ Name = table.Column(type: "TEXT", nullable: false),
+ Email = table.Column(type: "TEXT", nullable: false),
+ Level = table.Column(type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Trainers", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Battles",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ TrainerId = table.Column(type: "INTEGER", nullable: false),
+ OpponentName = table.Column(type: "TEXT", nullable: false),
+ Winner = table.Column(type: "TEXT", nullable: true),
+ Date = table.Column(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Battles", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Battles_Trainers_TrainerId",
+ column: x => x.TrainerId,
+ principalTable: "Trainers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "TrainerPokemons",
+ columns: table => new
+ {
+ TrainerId = table.Column(type: "INTEGER", nullable: false),
+ PokemonId = table.Column(type: "INTEGER", nullable: false),
+ IsShiny = table.Column(type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_TrainerPokemons", x => new { x.TrainerId, x.PokemonId });
+ table.ForeignKey(
+ name: "FK_TrainerPokemons_Pokemons_PokemonId",
+ column: x => x.PokemonId,
+ principalTable: "Pokemons",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_TrainerPokemons_Trainers_TrainerId",
+ column: x => x.TrainerId,
+ principalTable: "Trainers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "BattlePokemons",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ BattleId = table.Column(type: "INTEGER", nullable: false),
+ PokemonId = table.Column(type: "INTEGER", nullable: false),
+ HpRemaining = table.Column(type: "INTEGER", nullable: false),
+ Participated = table.Column(type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_BattlePokemons", x => x.Id);
+ table.ForeignKey(
+ name: "FK_BattlePokemons_Battles_BattleId",
+ column: x => x.BattleId,
+ principalTable: "Battles",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_BattlePokemons_Pokemons_PokemonId",
+ column: x => x.PokemonId,
+ principalTable: "Pokemons",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BattlePokemons_BattleId",
+ table: "BattlePokemons",
+ column: "BattleId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BattlePokemons_PokemonId",
+ table: "BattlePokemons",
+ column: "PokemonId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Battles_TrainerId",
+ table: "Battles",
+ column: "TrainerId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_TrainerPokemons_PokemonId",
+ table: "TrainerPokemons",
+ column: "PokemonId");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Admins");
+
+ migrationBuilder.DropTable(
+ name: "BattlePokemons");
+
+ migrationBuilder.DropTable(
+ name: "TrainerPokemons");
+
+ migrationBuilder.DropTable(
+ name: "Battles");
+
+ migrationBuilder.DropTable(
+ name: "Pokemons");
+
+ migrationBuilder.DropTable(
+ name: "Trainers");
+ }
+ }
+}
diff --git a/src/Infrastructure/Migrations/20250925020735_AddBattleLogs.Designer.cs b/src/Infrastructure/Migrations/20250925020735_AddBattleLogs.Designer.cs
new file mode 100644
index 0000000..9eb29a2
--- /dev/null
+++ b/src/Infrastructure/Migrations/20250925020735_AddBattleLogs.Designer.cs
@@ -0,0 +1,287 @@
+//
+using System;
+using Infrastructure.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Infrastructure.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20250925020735_AddBattleLogs")]
+ partial class AddBattleLogs
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.9");
+
+ modelBuilder.Entity("Core.Entities.Admin", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("HashedPassword")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsSuperAdmin")
+ .HasColumnType("INTEGER");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Admins");
+ });
+
+ modelBuilder.Entity("Core.Entities.Battle", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Date")
+ .HasColumnType("TEXT");
+
+ b.Property("OpponentName")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("TrainerId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Winner")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TrainerId");
+
+ b.ToTable("Battles");
+ });
+
+ modelBuilder.Entity("Core.Entities.BattleLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Action")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("BattleId")
+ .HasColumnType("INTEGER");
+
+ b.Property("TurnNumber")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BattleId");
+
+ b.ToTable("BattleLogs");
+ });
+
+ modelBuilder.Entity("Core.Entities.BattlePokemon", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("BattleId")
+ .HasColumnType("INTEGER");
+
+ b.Property("HpRemaining")
+ .HasColumnType("INTEGER");
+
+ b.Property("Participated")
+ .HasColumnType("INTEGER");
+
+ b.Property("PokemonId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BattleId");
+
+ b.HasIndex("PokemonId");
+
+ b.ToTable("BattlePokemons");
+ });
+
+ modelBuilder.Entity("Core.Entities.Pokemon", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Attack")
+ .HasColumnType("INTEGER");
+
+ b.Property("Defense")
+ .HasColumnType("INTEGER");
+
+ b.Property("Element")
+ .HasColumnType("TEXT");
+
+ b.Property("Hp")
+ .HasColumnType("INTEGER");
+
+ b.Property("Level")
+ .HasColumnType("INTEGER");
+
+ b.PrimitiveCollection("Moves")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("SpecialAttack")
+ .HasColumnType("INTEGER");
+
+ b.Property("SpecialDefense")
+ .HasColumnType("INTEGER");
+
+ b.Property("Speed")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("Pokemons");
+ });
+
+ modelBuilder.Entity("Core.Entities.Trainer", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Level")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Trainers");
+ });
+
+ modelBuilder.Entity("Core.Entities.TrainerPokemon", b =>
+ {
+ b.Property("TrainerId")
+ .HasColumnType("INTEGER");
+
+ b.Property("PokemonId")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsShiny")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("TrainerId", "PokemonId");
+
+ b.HasIndex("PokemonId");
+
+ b.ToTable("TrainerPokemons");
+ });
+
+ modelBuilder.Entity("Core.Entities.Battle", b =>
+ {
+ b.HasOne("Core.Entities.Trainer", "Trainer")
+ .WithMany("Battles")
+ .HasForeignKey("TrainerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Trainer");
+ });
+
+ modelBuilder.Entity("Core.Entities.BattleLog", b =>
+ {
+ b.HasOne("Core.Entities.Battle", "Battle")
+ .WithMany("Logs")
+ .HasForeignKey("BattleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Battle");
+ });
+
+ modelBuilder.Entity("Core.Entities.BattlePokemon", b =>
+ {
+ b.HasOne("Core.Entities.Battle", "Battle")
+ .WithMany("Pokemons")
+ .HasForeignKey("BattleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Core.Entities.Pokemon", "Pokemon")
+ .WithMany()
+ .HasForeignKey("PokemonId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Battle");
+
+ b.Navigation("Pokemon");
+ });
+
+ modelBuilder.Entity("Core.Entities.TrainerPokemon", b =>
+ {
+ b.HasOne("Core.Entities.Pokemon", "Pokemon")
+ .WithMany()
+ .HasForeignKey("PokemonId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Core.Entities.Trainer", "Trainer")
+ .WithMany("Pokemons")
+ .HasForeignKey("TrainerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Pokemon");
+
+ b.Navigation("Trainer");
+ });
+
+ modelBuilder.Entity("Core.Entities.Battle", b =>
+ {
+ b.Navigation("Logs");
+
+ b.Navigation("Pokemons");
+ });
+
+ modelBuilder.Entity("Core.Entities.Trainer", b =>
+ {
+ b.Navigation("Battles");
+
+ b.Navigation("Pokemons");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Infrastructure/Migrations/20250925020735_AddBattleLogs.cs b/src/Infrastructure/Migrations/20250925020735_AddBattleLogs.cs
new file mode 100644
index 0000000..4e997ec
--- /dev/null
+++ b/src/Infrastructure/Migrations/20250925020735_AddBattleLogs.cs
@@ -0,0 +1,47 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Infrastructure.Migrations
+{
+ ///
+ public partial class AddBattleLogs : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "BattleLogs",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ BattleId = table.Column(type: "INTEGER", nullable: false),
+ TurnNumber = table.Column(type: "INTEGER", nullable: false),
+ Action = table.Column(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_BattleLogs", x => x.Id);
+ table.ForeignKey(
+ name: "FK_BattleLogs_Battles_BattleId",
+ column: x => x.BattleId,
+ principalTable: "Battles",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BattleLogs_BattleId",
+ table: "BattleLogs",
+ column: "BattleId");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "BattleLogs");
+ }
+ }
+}
diff --git a/src/Infrastructure/Migrations/AppDbContextModelSnapshot.cs b/src/Infrastructure/Migrations/AppDbContextModelSnapshot.cs
new file mode 100644
index 0000000..3098f65
--- /dev/null
+++ b/src/Infrastructure/Migrations/AppDbContextModelSnapshot.cs
@@ -0,0 +1,284 @@
+//
+using System;
+using Infrastructure.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Infrastructure.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ partial class AppDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.9");
+
+ modelBuilder.Entity("Core.Entities.Admin", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("HashedPassword")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsSuperAdmin")
+ .HasColumnType("INTEGER");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Admins");
+ });
+
+ modelBuilder.Entity("Core.Entities.Battle", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Date")
+ .HasColumnType("TEXT");
+
+ b.Property("OpponentName")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("TrainerId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Winner")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TrainerId");
+
+ b.ToTable("Battles");
+ });
+
+ modelBuilder.Entity("Core.Entities.BattleLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Action")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("BattleId")
+ .HasColumnType("INTEGER");
+
+ b.Property("TurnNumber")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BattleId");
+
+ b.ToTable("BattleLogs");
+ });
+
+ modelBuilder.Entity("Core.Entities.BattlePokemon", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("BattleId")
+ .HasColumnType("INTEGER");
+
+ b.Property("HpRemaining")
+ .HasColumnType("INTEGER");
+
+ b.Property("Participated")
+ .HasColumnType("INTEGER");
+
+ b.Property("PokemonId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BattleId");
+
+ b.HasIndex("PokemonId");
+
+ b.ToTable("BattlePokemons");
+ });
+
+ modelBuilder.Entity("Core.Entities.Pokemon", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Attack")
+ .HasColumnType("INTEGER");
+
+ b.Property("Defense")
+ .HasColumnType("INTEGER");
+
+ b.Property("Element")
+ .HasColumnType("TEXT");
+
+ b.Property("Hp")
+ .HasColumnType("INTEGER");
+
+ b.Property("Level")
+ .HasColumnType("INTEGER");
+
+ b.PrimitiveCollection("Moves")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("SpecialAttack")
+ .HasColumnType("INTEGER");
+
+ b.Property("SpecialDefense")
+ .HasColumnType("INTEGER");
+
+ b.Property("Speed")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("Pokemons");
+ });
+
+ modelBuilder.Entity("Core.Entities.Trainer", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Level")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Trainers");
+ });
+
+ modelBuilder.Entity("Core.Entities.TrainerPokemon", b =>
+ {
+ b.Property("TrainerId")
+ .HasColumnType("INTEGER");
+
+ b.Property("PokemonId")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsShiny")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("TrainerId", "PokemonId");
+
+ b.HasIndex("PokemonId");
+
+ b.ToTable("TrainerPokemons");
+ });
+
+ modelBuilder.Entity("Core.Entities.Battle", b =>
+ {
+ b.HasOne("Core.Entities.Trainer", "Trainer")
+ .WithMany("Battles")
+ .HasForeignKey("TrainerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Trainer");
+ });
+
+ modelBuilder.Entity("Core.Entities.BattleLog", b =>
+ {
+ b.HasOne("Core.Entities.Battle", "Battle")
+ .WithMany("Logs")
+ .HasForeignKey("BattleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Battle");
+ });
+
+ modelBuilder.Entity("Core.Entities.BattlePokemon", b =>
+ {
+ b.HasOne("Core.Entities.Battle", "Battle")
+ .WithMany("Pokemons")
+ .HasForeignKey("BattleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Core.Entities.Pokemon", "Pokemon")
+ .WithMany()
+ .HasForeignKey("PokemonId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Battle");
+
+ b.Navigation("Pokemon");
+ });
+
+ modelBuilder.Entity("Core.Entities.TrainerPokemon", b =>
+ {
+ b.HasOne("Core.Entities.Pokemon", "Pokemon")
+ .WithMany()
+ .HasForeignKey("PokemonId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Core.Entities.Trainer", "Trainer")
+ .WithMany("Pokemons")
+ .HasForeignKey("TrainerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Pokemon");
+
+ b.Navigation("Trainer");
+ });
+
+ modelBuilder.Entity("Core.Entities.Battle", b =>
+ {
+ b.Navigation("Logs");
+
+ b.Navigation("Pokemons");
+ });
+
+ modelBuilder.Entity("Core.Entities.Trainer", b =>
+ {
+ b.Navigation("Battles");
+
+ b.Navigation("Pokemons");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}