PokeDo is a gamified task manager that combines productivity tracking with Pokemon collection mechanics. The application follows a layered architecture pattern with clear separation between:
Technology Stack:
pokedo/
├── __init__.py # Package init with version
├── __main__.py # Entry point for `python -m pokedo`
├── cli/ # Presentation Layer
│ ├── app.py # Main Typer application
│ └── commands/
│ ├── tasks.py # Task commands
│ ├── pokemon.py # Pokemon commands
│ ├── wellbeing.py # Wellbeing commands
│ ├── stats.py # Stats/profile commands
│ ├── profile.py # Profile management commands
│ ├── battle.py # PvP battle CLI commands
│ └── leaderboard.py # Leaderboard CLI commands
│ └── ui/
│ ├── displays.py # Display helpers (Rich tables, panels)
│ └── menus.py # Interactive menus
├── tui/ # TUI Layer (Textual)
│ ├── app.py # Textual dashboard app
│ ├── screens/ # Screen classes (tasks, etc.)
│ ├── widgets/ # Reusable UI components
│ └── styles/ # Textual CSS styling
├── core/ # Business Logic Layer
│ ├── auth.py # Authentication logic (Bcrypt/JWT)
│ ├── battle.py # PvP battle state machine and turn resolution
│ ├── moves.py # Move model, 18-type chart, damage formula, natures
│ ├── task.py # Task model and enums
│ ├── trainer.py # Trainer model, progression, and PvP stats
│ ├── pokemon.py # Pokemon and Pokedex models (EV/IV, battle conversion)
│ ├── rewards.py # Encounter and reward system
│ └── wellbeing.py # Wellbeing tracking models
├── data/ # Data Access Layer
│ ├── database.py # SQLite operations (local)
│ ├── pokeapi.py # PokeAPI client (async, cached)
│ ├── server_models.py # Postgres models (ServerUser, BattleRecord, LeaderboardEntry)
│ └── sync.py # Sync client and change queue
├── server.py # FastAPI server (auth, battles, leaderboard, sync)
└── utils/ # Utilities
├── config.py # Configuration management
├── helpers.py # Helper functions
└── sprites.py # Terminal sprite rendering
cli/, tui/)The CLI layer handles all user interaction through the Typer framework.
app.py - Main application entry point
commands/ - Command implementations
ui/ - Display components
displays.py: Tables, panels, progress bars, ASCII artmenus.py: Interactive selection menusTUI Layer (tui/)
app.py: Textual-based dashboard for trainer, tasks, and team summariesdata/) for read-only viewsself._task for domain objects in widgets/screens/modals. Use explicit names like self._selected_task or self._editing_task.server.py)Handles centralized multiplayer operations, authentication, and synchronization.
server.py - FastAPI Application
lifespan context manager pattern (not deprecated on_event)/register, /token)BattleEngine/sync)data/server_models.py - Server Database Models
ServerUser: Username, hashed password, ELO rating, battle stats, rankBattleRecord: Battle state persistence (format, status, teams, turn history)LeaderboardEntry: Pydantic response model for rankingsget_leaderboard(): Query helper with sortable columns and disabled-user filteringcore/)The core layer contains domain models and game logic.
auth.py - Authentication
battle.py - PvP Battle System
class BattleState(BaseModel):
# Full battle state: teams, active Pokemon, turn history,
# pending actions, status, winner tracking
class BattleEngine:
@staticmethod
def resolve_turn(state: BattleState) -> list[TurnEvent]:
# Resolves forfeits, switches, then attacks (priority-sorted)
# Applies damage, status effects, end-of-turn damage
# Handles mutual KO (draw support), auto-switching
def calculate_elo_change(winner_elo, loser_elo, k=32) -> tuple[int, int]
def compute_rank(elo: int) -> str # ELO to rank name
moves.py - Move System
class Move(BaseModel):
# name, type, power, accuracy, pp, category (physical/special/status)
# priority, drain_percent, recoil_percent, status_effect, secondary_effect
TYPE_CHART: dict[str, dict[str, float]] # 18x18 effectiveness matrix
NATURE_MODIFIERS: dict[str, dict[str, float]] # 25 natures
def calculate_damage(...) # Gen V+ damage formula
def generate_default_moveset(pokemon, level) -> list[Move]
task.py - Task Management
class TaskCategory(Enum):
WORK, EXERCISE, LEARNING, PERSONAL, HEALTH, CREATIVE
class TaskDifficulty(Enum):
EASY, MEDIUM, HARD, EPIC
class Task(BaseModel):
# Properties: is_overdue, xp_reward, stat_affinity, ev_yield
pokemon.py - Pokemon System
class PokemonRarity(Enum):
COMMON, UNCOMMON, RARE, EPIC, LEGENDARY, MYTHICAL
class Pokemon(BaseModel):
# Evolution tracking, XP system, happiness
# EV/IV stats (new)
trainer.py - Player Progression
class TrainerClass(str, Enum):
ACE_TRAINER, HIKER, SCIENTIST, ... # Specialization classes
class Trainer(BaseModel):
# Level calculation, XP management
# Streak tracking, badge system
# Inventory management
# PvP stats: battle_wins, battle_losses, battle_draws
# elo_rating (starting 1000), pvp_rank (derived from ELO)
# record_battle(won, elo_delta) -- updates stats and rank
rewards.py - Reward System
class RewardSystem:
# Encounter probability calculation
# Catch rate computation
# Shiny rate with streak bonuses
# Streak milestone rewards
wellbeing.py - Wellbeing Tracking
class MoodEntry, ExerciseEntry, SleepEntry,
HydrationEntry, MeditationEntry, JournalEntry
class DailyWellbeing:
# Aggregate daily wellbeing data
# Type affinity bonuses
data/)database.py - SQLite Operations
pokemon.evs, pokemon.ivs) store serialized stat distributions so training progress survives restarts without an extra join table.pokeapi.py - External API Client
httpxsync.py - Synchronization Client
Change entity)requestsThis layer enables local-first data handling with cloud synchronization.
Concept:
change table in the local DB.change table and pushes them to the server via HTTP POST.Change Entity:
class Change(SQLModel):
id: str (UUID)
entity_id: str
entity_type: str (e.g., "task", "pokemon")
action: str (CREATE, UPDATE, DELETE)
payload: JSON
timestamp: datetime
synced: bool
Sync Process:
queue_change(): Called by CRUD functions in database.py whenever data modifies.push_changes(): Reads unsynced records, sends to POST /sync.synced=True.User Input -> CLI Command -> Validate Input -> Database Update -> Queue Change
|
v
Display Result <- UI Components <- Reward System <- Calculate Rewards
|
v
Pokemon Encounter
|
v
Catch Attempt
|
v
Update Database -> Queue Change
1. Task completed
2. Calculate encounter probability
- Base rate (70%)
- Difficulty bonus (+5-15%)
- Streak bonus (+1%/day, max 10%)
3. If encounter:
a. Determine rarity (weighted by difficulty)
b. Select Pokemon from rarity pool
c. Check shiny status (1% + streak bonus)
d. Calculate catch rate
e. Apply ball modifiers
f. Determine success/failure
4. Update database (trainer stats, Pokemon, Pokedex)
5. Display result
┌─────────────┐ ┌─────────────┐
│ trainer │ │ tasks │
├─────────────┤ ├─────────────┤
│ id (PK) │ │ id (PK) │
│ │ │ trainer_id │
│ name │ │ title │
│ total_xp │ │ category │
│ badges │ │ difficulty │
│ inventory │ │ due_date │
│ streaks │ │ completed │
└─────────────┘ └─────────────┘
┌─────────────┐ ┌─────────────┐
│ pokemon │──────→│ pokedex │
├─────────────┤ ├─────────────┤
│ id (PK) │ │ pokedex_id │
│ trainer_id │ │ trainer_id │
│ pokedex_id │ │ name │
│ nickname │ │ type1/type2 │
│ level │ │ rarity │
│ is_shiny │ │ is_caught │
│ is_active │ │ times_caught│
└─────────────┘ └─────────────┘
┌──────────────────┐ ┌──────────────────┐
│ mood_entries │ │ exercise_entries │
├──────────────────┤ ├──────────────────┤
│ id, trainer_id │ │ id, trainer_id │
│ date, mood │ │ date, type │
│ note, energy │ │ duration, note │
└──────────────────┘ └──────────────────┘
┌──────────────────┐ ┌──────────────────┐
│ sleep_entries │ │hydration_entries │
├──────────────────┤ ├──────────────────┤
│ id, trainer_id │ │ id, trainer_id │
│ date, hours │ │ date, glasses │
│ quality, note │ │ note │
└──────────────────┘ └──────────────────┘
┌──────────────────┐ ┌──────────────────┐
│meditation_entries│ │ journal_entries │
├──────────────────┤ ├──────────────────┤
│ id, trainer_id │ │ id, trainer_id │
│ date, minutes │ │ date, content │
│ note │ │ gratitude_items │
└──────────────────┘ └──────────────────┘
┌──────────────────┐ ┌──────────────────┐
│ server_users │ │ battle_records │
├──────────────────┤ ├──────────────────┤
│ id (PK, UUID) │ │ id (PK, UUID) │
│ username (unique)│ │ challenger_id FK │
│ hashed_password │ │ opponent_id FK │
│ elo_rating │ │ format │
│ battle_wins │ │ status │
│ battle_losses │ │ winner_id │
│ battle_draws │ │ state_json │
│ rank │ │ turn_history │
│ disabled │ │ created/updated │
│ created_at │ │ │
└──────────────────┘ └──────────────────┘
tasks | Column | Type | Description | |——–|——|————-| | id | INTEGER PK | Auto-increment ID | | trainer_id | INTEGER FK | Owning trainer profile | | title | TEXT | Task title | | description | TEXT | Optional description | | category | TEXT | work/exercise/learning/personal/health/creative | | difficulty | TEXT | easy/medium/hard/epic | | priority | TEXT | low/medium/high/urgent | | created_at | TIMESTAMP | Creation time | | due_date | DATE | Optional due date | | completed_at | TIMESTAMP | Completion time | | is_completed | BOOLEAN | Completion status | | is_archived | BOOLEAN | Archive status | | recurrence | TEXT | daily/weekly/monthly/none | | parent_task_id | INTEGER FK | Parent for recurring tasks | | tags | TEXT | JSON array of tags |
pokemon | Column | Type | Description | |——–|——|————-| | id | INTEGER PK | Auto-increment ID | | trainer_id | INTEGER FK | Owning trainer profile | | pokedex_id | INTEGER | National Pokedex number | | name | TEXT | Species name | | nickname | TEXT | User-assigned nickname | | type1, type2 | TEXT | Pokemon types | | level | INTEGER | Current level (1-100) | | xp | INTEGER | Experience points | | happiness | INTEGER | Happiness value | | caught_at | TIMESTAMP | Catch time | | is_shiny | BOOLEAN | Shiny variant | | catch_location | TEXT | Where caught (task category) | | is_active | BOOLEAN | In active team | | is_favorite | BOOLEAN | Marked as favorite | | can_evolve | BOOLEAN | Evolution available | | evolution_id | INTEGER | Evolution target ID | | evolution_level | INTEGER | Level required to evolve | | sprite_url | TEXT | Remote sprite URL | | sprite_path | TEXT | Local cached sprite path |
pokedex | Column | Type | Description | |——–|——|————-| | trainer_id | INTEGER PK | Owning trainer profile | | pokedex_id | INTEGER PK | National Pokedex number | | name | TEXT | Species name | | type1, type2 | TEXT | Pokemon types | | is_seen | BOOLEAN | Encountered | | is_caught | BOOLEAN | Successfully caught | | times_caught | INTEGER | Total catch count | | first_caught_at | TIMESTAMP | First catch time | | shiny_caught | BOOLEAN | Shiny variant caught | | sprite_url | TEXT | Sprite URL | | rarity | TEXT | Rarity tier | | evolves_from | INTEGER | Pre-evolution ID | | evolves_to | TEXT | JSON array of evolution IDs |
trainer | Column | Type | Description | |——–|——|————-| | id | INTEGER PK | Trainer profile ID | | name | TEXT | Trainer name | | created_at | TIMESTAMP | Profile creation time | | total_xp | INTEGER | Accumulated XP | | tasks_completed | INTEGER | Total tasks done | | pokemon_caught | INTEGER | Total Pokemon caught | | pokemon_released | INTEGER | Total released | | evolutions_triggered | INTEGER | Total evolutions | | pokedex_seen | INTEGER | Unique species seen | | pokedex_caught | INTEGER | Unique species caught | | daily_streak_count | INTEGER | Current task streak | | daily_streak_best | INTEGER | Best task streak | | daily_streak_last_date | DATE | Last task completion | | wellbeing_streak_count | INTEGER | Current wellbeing streak | | wellbeing_streak_best | INTEGER | Best wellbeing streak | | wellbeing_streak_last_date | DATE | Last wellbeing log | | badges | TEXT | JSON array of badges | | inventory | TEXT | JSON object of items | | favorite_pokemon_id | INTEGER | Favorite Pokemon | | last_active_date | DATE | Last activity |
Base URL: https://pokeapi.co/api/v2
Endpoints Used:
GET /pokemon/{id} - Pokemon data (stats, types, sprites)GET /pokemon-species/{id} - Species data (evolution chain URL)GET /evolution-chain/{id} - Evolution requirementsCaching Strategy:
~/.pokedo/cache/~/.pokedo/cache/sprites/pokedo initRate Limiting:
Task XP Rewards: | Difficulty | XP | |————|—–| | Easy | 10 | | Medium | 25 | | Hard | 50 | | Epic | 100 |
Trainer Level Formula:
level = 1
while total_xp >= level * 100:
total_xp -= level * 100
level += 1
Pokemon Level:
Rarity Weights by Difficulty: | Rarity | Easy | Medium | Hard | Epic | |——–|——|——–|——|——| | Common | 70% | 50% | 30% | 15% | | Uncommon | 25% | 35% | 35% | 25% | | Rare | 4% | 10% | 20% | 25% | | Epic | 1% | 4% | 12% | 25% | | Legendary | 0% | 1% | 3% | 10% |
Pokemon Rarity Classification:
base_rate = {
'common': 0.90,
'uncommon': 0.75,
'rare': 0.50,
'epic': 0.30,
'legendary': 0.15,
'mythical': 0.05
}
# Modifiers
trainer_bonus = min(trainer_level * 0.02, 0.20) # Max +20%
ball_bonus = {'pokeball': 0, 'great': 0.10, 'ultra': 0.20, 'master': 1.0}
sleep_bonus = get_sleep_modifier() # +10% or -20%
final_rate = min(base_rate + trainer_bonus + ball_bonus + sleep_bonus, 1.0)
base_shiny_rate = 0.01 # 1%
streak_bonus = streak_days * 0.005 # +0.5% per day
max_shiny_rate = 0.10 # 10% cap
shiny_rate = min(base_shiny_rate + streak_bonus, max_shiny_rate)
Task categories influence Pokemon type encounter probabilities:
| Category | Boosted Types |
|---|---|
| Work | Steel, Electric, Normal |
| Exercise | Fighting, Fire, Rock |
| Learning | Psychic, Ghost, Dark |
| Personal | Normal, Fairy, Flying |
| Health | Grass, Water, Poison |
| Creative | Fairy, Dragon, Ice |
Wellbeing actions also affect type encounters:
pokemon.evs/pokemon.ivs are saved as JSON blobs in the database, so the stat training that occurs every time a task completes is recorded once and carried over every time the Pokemon is loaded. This supports the standard 510/252 caps and keeps spreads deterministic across sessions (pokedo/core/pokemon.py, pokedo/data/database.py)._ensure_pokedex_entry_types fetches missing Pokedex entries from the API before affinity filtering, enabling tasks and wellbeing logs to bias rarity pools even for unseen species. The reward engine caches the resulting entry so type-based filtering (type_affinities + wellbeing bonuses) reliably narrows the candidate pool (pokedo/core/rewards.py).DATA_DIR = Path.home() / ".pokedo"
DB_PATH = DATA_DIR / "pokedo.db"
CACHE_DIR = DATA_DIR / "cache"
SPRITES_DIR = CACHE_DIR / "sprites"
# XP Values
TASK_XP = {'easy': 10, 'medium': 25, 'hard': 50, 'epic': 100}
# Encounter Rates
BASE_ENCOUNTER_RATE = 0.70
DIFFICULTY_BONUS = {'easy': 0.05, 'medium': 0.10, 'hard': 0.12, 'epic': 0.15}
STREAK_BONUS_PER_DAY = 0.01
MAX_STREAK_BONUS = 0.10
# Catch Rates
BASE_CATCH_RATE = 0.60
SHINY_RATE = 0.01
STREAK_SHINY_BONUS = 0.005
# Generation Ranges
GENERATION_RANGES = {
1: (1, 151), # Kanto
2: (152, 251), # Johto
3: (252, 386), # Hoenn
4: (387, 493), # Sinnoh
5: (494, 649), # Unova
6: (650, 721), # Kalos
7: (722, 809), # Alola
8: (810, 905), # Galar
9: (906, 1025) # Paldea
}
STREAK_REWARDS = {
3: {'item': 'great_ball', 'quantity': 5},
7: {'item': 'evolution_stone', 'quantity': 1},
14: {'item': 'ultra_ball', 'quantity': 5},
21: {'item': 'rare_candy', 'quantity': 3},
30: {'item': 'master_ball', 'quantity': 1},
50: {'item': 'legendary_ticket', 'quantity': 1},
100: {'item': 'mythical_ticket', 'quantity': 1}
}
trainer_id scoping