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 + } + } +}