Phase 5: Complete Core Game Engine implementation

- Added MTGOnlineGame, GamePlayer, GameHistory models
- Implemented full game CRUD endpoints (create, list, get, join, leave, start, end)
- Integrated WebSocket game server with database persistence
- Added Commander deck validation service (100-card min, color identity, ban list)
- Created Alembic migration 006 for new game tables
- All game endpoints now database-backed (no more stubs)
This commit is contained in:
2026-08-27 05:29:38 +00:00
parent 0e6506d97f
commit e9f5377e0a
9 changed files with 1057 additions and 102 deletions
@@ -0,0 +1,117 @@
"""Add game models: mtgonline_games, game_players, game_history
Revision ID: 006
Revises: 005
Create Date: 2026-01-05 00:00:00.000000
This migration creates tables for the Core Game Engine:
- mtgonline_games: Active game sessions
- game_players: Player associations with games
- game_history: Historical game records
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '006'
down_revision: Union[str, None] = '005'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Create game engine tables."""
# ------------------------------------------------------------------
# 1. MTG Online Games Table (mtgonline_games)
# Mirrors: app/models/models.py – class MTGOnlineGame
# ------------------------------------------------------------------
op.create_table(
'mtgonline_games',
sa.Column('id', sa.Integer(), primary_key=True, index=True),
sa.Column('room_id', sa.Integer(),
sa.ForeignKey('mtgonline_rooms.id'),
nullable=False, index=True),
sa.Column('host_id', sa.Integer(),
sa.ForeignKey('mtgonline_users.id'),
nullable=False, index=True),
sa.Column('game_type', sa.String(50), nullable=False,
server_default='Commander'),
sa.Column('state', sa.String(20), nullable=False,
server_default='WAITING'),
sa.Column('is_password_protected', sa.Boolean(),
nullable=False, server_default='false'),
sa.Column('max_players', sa.Integer(), nullable=False,
server_default='4'),
sa.Column('current_players', sa.Integer(), nullable=False,
server_default='0'),
sa.Column('created_at', sa.DateTime(),
server_default=sa.func.now()),
sa.Column('started_at', sa.DateTime(), nullable=True),
sa.Column('finished_at', sa.DateTime(), nullable=True),
)
# ------------------------------------------------------------------
# 2. Game Players Table (game_players)
# Mirrors: app/models/models.py – class GamePlayer
# ------------------------------------------------------------------
op.create_table(
'game_players',
sa.Column('id', sa.Integer(), primary_key=True, index=True),
sa.Column('game_id', sa.Integer(),
sa.ForeignKey('mtgonline_games.id', ondelete='CASCADE'),
nullable=False, index=True),
sa.Column('user_id', sa.Integer(),
sa.ForeignKey('mtgonline_users.id'),
nullable=False, index=True),
sa.Column('seat_number', sa.Integer(), nullable=True),
sa.Column('life_total', sa.Integer(), nullable=False,
server_default='20'),
sa.Column('is_ready', sa.Boolean(), nullable=False,
server_default='false'),
sa.Column('is_host', sa.Boolean(), nullable=False,
server_default='false'),
sa.Column('joined_at', sa.DateTime(),
server_default=sa.func.now()),
)
# Unique constraint + indexes
op.create_unique_constraint(
'uq_game_player_unique', 'game_players',
['game_id', 'user_id'])
op.create_index('idx_game_players_game', 'game_players', ['game_id'])
op.create_index('idx_game_players_user', 'game_players', ['user_id'])
# ------------------------------------------------------------------
# 3. Game History Table (game_history)
# Mirrors: app/models/models.py – class GameHistory
# ------------------------------------------------------------------
op.create_table(
'game_history',
sa.Column('id', sa.Integer(), primary_key=True, index=True),
sa.Column('game_id', sa.Integer(),
sa.ForeignKey('mtgonline_games.id'),
nullable=True),
sa.Column('room_id', sa.Integer(),
sa.ForeignKey('mtgonline_rooms.id'),
nullable=True, index=True),
sa.Column('game_type', sa.String(50), nullable=False,
server_default='Commander'),
sa.Column('winner_id', sa.Integer(),
sa.ForeignKey('mtgonline_users.id'),
nullable=True, index=True),
sa.Column('duration_seconds', sa.Integer(), nullable=True),
sa.Column('result_type', sa.String(50), nullable=True),
sa.Column('replay_data', sa.JSON(), nullable=True),
sa.Column('created_at', sa.DateTime(),
server_default=sa.func.now()),
)
def downgrade() -> None:
"""Drop game engine tables in reverse dependency order."""
op.drop_table('game_history')
op.drop_table('game_players')
op.drop_table('mtgonline_games')