Add backend foundation with uv, Black, and pre-commit hooks

- Initialize FastAPI backend with uv package manager
- Configure Black, Ruff, pytest, mypy in pyproject.toml
- Add health check endpoint and initial test
- Create AGENTS.md with coding guidelines for AI agents
- Add pre-commit hook to enforce linting and tests
This commit is contained in:
Cal Corum 2026-01-24 00:12:33 -06:00
parent f473f94bce
commit 234e9a95c1
9 changed files with 1534 additions and 0 deletions

250
AGENTS.md Normal file
View File

@ -0,0 +1,250 @@
# AGENTS.md - Mantimon TCG
Guidelines for agentic coding agents working on this codebase.
## Quick Commands
### Development Servers
```bash
cd frontend && npm run dev # Frontend dev server
cd backend && uv run uvicorn app.main:app --reload # Backend dev server
```
### Testing
```bash
# Frontend (Vitest)
cd frontend && npm run test # Run all tests
cd frontend && npm run test -- path/to/file # Run single test file
cd frontend && npm run test -- -t "test name" # Run by test name
# Backend (pytest with uv)
cd backend && uv run pytest # Run all tests
cd backend && uv run pytest tests/test_file.py # Single file
cd backend && uv run pytest tests/test_file.py::test_fn # Single test
cd backend && uv run pytest -k "test_name" # By name pattern
cd backend && uv run pytest -x # Stop on first failure
cd backend && uv run pytest --cov=app # With coverage
```
### Linting & Formatting
```bash
# Frontend
cd frontend && npm run lint # ESLint
cd frontend && npm run typecheck # TypeScript check
# Backend (uses uv for all commands)
cd backend && uv run black app tests # Format with Black
cd backend && uv run black --check . # Check formatting (CI)
cd backend && uv run ruff check . # Lint with Ruff
cd backend && uv run ruff check --fix . # Auto-fix lint issues
cd backend && uv run mypy app # Type check
```
### Dependency Management (Backend)
```bash
cd backend && uv add <package> # Add runtime dependency
cd backend && uv add --dev <package> # Add dev dependency
cd backend && uv sync # Install all dependencies
cd backend && uv lock # Update lock file
```
---
## Code Style
### General Rules
- Line length: 100 characters max
- Indentation: 2 spaces (frontend), 4 spaces (backend)
- Trailing commas in multi-line structures
- Explicit over implicit
### TypeScript/Vue
**Import order** (separated by blank lines):
1. Standard library / Vue core
2. Third-party packages
3. Local imports (use `@/` alias)
```typescript
import { ref, computed } from 'vue'
import { useGameStore } from '@/stores/game'
import type { Card, GameState } from '@/types'
// Always use `import type` for type-only imports
import type { Player } from '@/types/player'
// Prefer const over let
const cards = ref<Card[]>([])
```
**Naming conventions:**
| Type | Convention | Example |
|------|------------|---------|
| Vue components | PascalCase | `CardHand.vue`, `GameBoard.vue` |
| Phaser scenes | PascalCase | `MatchScene.ts`, `PackOpeningScene.ts` |
| TypeScript files | camelCase | `useWebSocket.ts`, `cardUtils.ts` |
| Constants | UPPER_SNAKE_CASE | `MAX_HAND_SIZE`, `PRIZE_COUNT` |
### Python
**Import order** (separated by blank lines):
1. Standard library
2. Third-party packages
3. Local imports
```python
from typing import Optional
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import Card, Player
from app.services.game_service import GameService
```
**Type hints required** for all function signatures:
```python
async def get_card(card_id: int, db: AsyncSession = Depends(get_db)) -> Card:
...
```
**Naming conventions:**
| Type | Convention | Example |
|------|------------|---------|
| Modules | snake_case | `game_engine.py`, `card_service.py` |
| Database tables | snake_case | `user_collections`, `match_history` |
| Constants | UPPER_SNAKE_CASE | `MAX_HAND_SIZE`, `PRIZE_COUNT` |
---
## Architecture Patterns
### Frontend: Vue + Phaser Integration
Phaser mounts as a Vue component. Communication via event bridge:
```typescript
// Vue -> Phaser
phaserGame.value?.events.emit('card:play', { cardId, targetId })
// Phaser -> Vue
phaserGame.value?.events.on('animation:complete', handleAnimationComplete)
```
### Frontend: State Management (Pinia)
```typescript
export const useGameStore = defineStore('game', () => {
const gameState = ref<GameState | null>(null)
const myHand = computed(() => gameState.value?.myHand ?? [])
return { gameState, myHand }
})
```
### Backend: Service Layer
**Never bypass services for business logic:**
```python
# CORRECT
card = await card_service.get_card(card_id)
result = await game_service.play_card(game_id, player_id, card_id)
# WRONG - direct DB access in endpoint
card = await db.execute(select(Card).where(Card.id == card_id))
```
### Backend: Async by Default
All I/O operations use async/await:
```python
async def resolve_attack(attacker: Card, defender: Card) -> AttackResult:
...
```
---
## Testing Guidelines
### Test Docstrings Required
Every test must include a docstring explaining "what" and "why":
```python
@pytest.mark.asyncio
async def test_draw_card():
"""
Test that drawing a card moves it from deck to hand.
Verifies the fundamental draw mechanic works correctly
and updates both zones appropriately.
"""
# test implementation
```
### Frontend Tests (Vitest)
```typescript
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
describe('CardHand', () => {
it('renders cards in hand', () => {
const wrapper = mount(CardHand, {
props: { cards: [{ id: '1', name: 'Pikachu' }] }
})
expect(wrapper.text()).toContain('Pikachu')
})
})
```
---
## Critical Security Rules
### Hidden Information
**Never expose to clients:**
- Deck order (either player)
- Opponent's hand contents
- Unrevealed prize cards
- RNG seeds or future random results
```python
# Correct: Only send counts for opponent's hidden zones
opponent_hand_count=len(opponent.hand) # ONLY count, not contents
opponent_deck_count=len(opponent.deck) # ONLY count
```
### Server Authority
- All game logic runs server-side
- Client sends intentions, server validates and executes
- Never trust client-provided game state
---
## Critical Rules Summary
1. **Git**: Never commit directly to `main`. Create feature branches.
2. **Commits**: Do not commit without user approval.
3. **Hidden Info**: Never send deck contents, opponent hand, or unrevealed prizes to client.
4. **Validation**: Always validate actions server-side. Never trust client.
5. **Tests**: Include docstrings explaining "what" and "why" for each test.
6. **Phaser in Vue**: Keep Phaser scenes focused on rendering. Game logic lives in backend.
7. **Services**: Never bypass the service layer for business logic.
---
## Project Structure
```
mantimon-tcg/
├── frontend/ # Vue 3 + Phaser 3
│ ├── src/
│ │ ├── components/ # Vue components
│ │ ├── pages/ # Route pages
│ │ ├── stores/ # Pinia stores
│ │ ├── game/ # Phaser scenes and game objects
│ │ └── composables/ # Vue composables
│ └── package.json
├── backend/ # FastAPI + PostgreSQL
│ ├── app/
│ │ ├── api/ # REST endpoints
│ │ ├── core/ # Game engine
│ │ ├── models/ # Pydantic + SQLAlchemy models
│ │ ├── services/ # Business logic
│ │ └── websocket/ # Socket.io handlers
│ └── pyproject.toml
└── shared/ # Shared types/schemas
```

32
backend/.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
# Virtual environments
.venv/
venv/
ENV/
# Testing
.pytest_cache/
.coverage
htmlcov/
.mypy_cache/
# IDE
.idea/
.vscode/
*.swp
*.swo
# Environment
.env
.env.local
# Distribution
dist/
build/
*.egg-info/

1
backend/.python-version Normal file
View File

@ -0,0 +1 @@
3.12

1
backend/app/__init__.py Normal file
View File

@ -0,0 +1 @@
# Mantimon TCG Backend

15
backend/app/main.py Normal file
View File

@ -0,0 +1,15 @@
"""Mantimon TCG - FastAPI Application Entry Point."""
from fastapi import FastAPI
app = FastAPI(
title="Mantimon TCG",
description="A home-rule-modified Pokemon Trading Card Game",
version="0.1.0",
)
@app.get("/health")
async def health_check() -> dict[str, str]:
"""Health check endpoint."""
return {"status": "healthy"}

109
backend/pyproject.toml Normal file
View File

@ -0,0 +1,109 @@
[project]
name = "mantimon-tcg-backend"
version = "0.1.0"
description = "Mantimon TCG - Backend API and Game Engine"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"alembic>=1.18.1",
"asyncpg>=0.31.0",
"bcrypt>=5.0.0",
"fastapi>=0.128.0",
"passlib>=1.7.4",
"pydantic>=2.12.5",
"pydantic-settings>=2.12.0",
"python-jose>=3.5.0",
"python-socketio>=5.16.0",
"redis>=7.1.0",
"sqlalchemy>=2.0.46",
"uvicorn>=0.40.0",
]
[dependency-groups]
dev = [
"black>=26.1.0",
"httpx>=0.28.1",
"mypy>=1.19.1",
"pytest>=9.0.2",
"pytest-asyncio>=1.3.0",
"pytest-cov>=7.0.0",
"ruff>=0.14.14",
]
# Black configuration
[tool.black]
line-length = 100
target-version = ["py312"]
include = '\.pyi?$'
exclude = '''
/(
\.git
| \.venv
| __pycache__
| migrations
)/
'''
# Ruff configuration (fast Python linter)
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # Pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"SIM", # flake8-simplify
]
ignore = [
"E501", # line too long (handled by black)
"B008", # do not perform function calls in argument defaults (FastAPI Depends)
]
[tool.ruff.lint.isort]
known-first-party = ["app"]
# Pytest configuration
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
addopts = "-v --tb=short"
filterwarnings = [
"ignore::DeprecationWarning",
]
# MyPy configuration
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true
plugins = ["pydantic.mypy"]
[[tool.mypy.overrides]]
module = [
"redis.*",
"socketio.*",
"passlib.*",
]
ignore_missing_imports = true
# Coverage configuration
[tool.coverage.run]
source = ["app"]
branch = true
omit = ["*/tests/*", "*/__pycache__/*"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
]

View File

@ -0,0 +1 @@
# Tests package

View File

@ -0,0 +1,23 @@
"""Tests for health check endpoint."""
import pytest
from fastapi.testclient import TestClient
from app.main import app
@pytest.fixture
def client() -> TestClient:
"""Create a test client for the FastAPI app."""
return TestClient(app)
def test_health_check(client: TestClient) -> None:
"""
Test that the health check endpoint returns a healthy status.
This verifies the application is running and can respond to requests.
"""
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "healthy"}

1102
backend/uv.lock generated Normal file

File diff suppressed because it is too large Load Diff