feat: Initial project structure for WebTriviasMulti

- Backend: FastAPI + Python-SocketIO + SQLAlchemy
  - Models for categories, questions, game sessions, events
  - AI services for answer validation and question generation (Claude)
  - Room management with Redis
  - Game logic with stealing mechanics
  - Admin API for question management

- Frontend: React + Vite + TypeScript + Tailwind
  - 5 visual themes (DRRR, Retro, Minimal, RGB, Anime 90s)
  - Real-time game with Socket.IO
  - Achievement system
  - Replay functionality
  - Sound effects per theme

- Docker Compose for deployment
- Design documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 07:50:48 +00:00
commit 43021b9c3c
57 changed files with 5446 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
from app.models.category import Category
from app.models.question import Question
from app.models.game_session import GameSession
from app.models.game_event import GameEvent
from app.models.admin import Admin
__all__ = ["Category", "Question", "GameSession", "GameEvent", "Admin"]

View File

@@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.sql import func
from app.models.base import Base
class Admin(Base):
__tablename__ = "admins"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(100), unique=True, nullable=False, index=True)
password_hash = Column(String(255), nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
def __repr__(self):
return f"<Admin(id={self.id}, username='{self.username}')>"

View File

@@ -0,0 +1,27 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.config import get_settings
settings = get_settings()
# Convert postgresql:// to postgresql+asyncpg://
database_url = settings.database_url.replace(
"postgresql://", "postgresql+asyncpg://"
)
engine = create_async_engine(database_url, echo=True)
AsyncSessionLocal = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
Base = declarative_base()
async def get_db():
async with AsyncSessionLocal() as session:
try:
yield session
finally:
await session.close()

View File

@@ -0,0 +1,18 @@
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import relationship
from app.models.base import Base
class Category(Base):
__tablename__ = "categories"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False, unique=True)
icon = Column(String(50))
color = Column(String(7)) # Hex color
# Relationships
questions = relationship("Question", back_populates="category")
def __repr__(self):
return f"<Category(id={self.id}, name='{self.name}')>"

View File

@@ -0,0 +1,27 @@
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.models.base import Base
class GameEvent(Base):
__tablename__ = "game_events"
id = Column(Integer, primary_key=True, index=True)
session_id = Column(Integer, ForeignKey("game_sessions.id"), nullable=False)
event_type = Column(String(50), nullable=False) # question_selected, answer_submitted, steal_attempted, etc.
player_name = Column(String(100))
team = Column(String(1)) # 'A' or 'B'
question_id = Column(Integer, ForeignKey("questions.id"))
answer_given = Column(Text)
was_correct = Column(Boolean)
was_steal = Column(Boolean, default=False)
points_earned = Column(Integer)
timestamp = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
session = relationship("GameSession", back_populates="events")
question = relationship("Question", back_populates="game_events")
def __repr__(self):
return f"<GameEvent(id={self.id}, type='{self.event_type}', player='{self.player_name}')>"

View File

@@ -0,0 +1,24 @@
from sqlalchemy import Column, Integer, String, DateTime, ARRAY
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.models.base import Base
class GameSession(Base):
__tablename__ = "game_sessions"
id = Column(Integer, primary_key=True, index=True)
room_code = Column(String(6), unique=True, nullable=False, index=True)
status = Column(String(20), default="waiting") # waiting, playing, finished
team_a_score = Column(Integer, default=0)
team_b_score = Column(Integer, default=0)
current_team = Column(String(1)) # 'A' or 'B'
questions_used = Column(ARRAY(Integer), default=[])
created_at = Column(DateTime(timezone=True), server_default=func.now())
finished_at = Column(DateTime(timezone=True))
# Relationships
events = relationship("GameEvent", back_populates="session")
def __repr__(self):
return f"<GameSession(id={self.id}, room_code='{self.room_code}', status='{self.status}')>"

View File

@@ -0,0 +1,28 @@
from sqlalchemy import Column, Integer, String, Text, Date, DateTime, ForeignKey, ARRAY
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.models.base import Base
class Question(Base):
__tablename__ = "questions"
id = Column(Integer, primary_key=True, index=True)
category_id = Column(Integer, ForeignKey("categories.id"), nullable=False)
question_text = Column(Text, nullable=False)
correct_answer = Column(String(500), nullable=False)
alt_answers = Column(ARRAY(String), default=[])
difficulty = Column(Integer, nullable=False) # 1-5
points = Column(Integer, nullable=False)
time_seconds = Column(Integer, nullable=False)
date_active = Column(Date, index=True)
status = Column(String(20), default="pending") # pending, approved, used
fun_fact = Column(Text)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
category = relationship("Category", back_populates="questions")
game_events = relationship("GameEvent", back_populates="question")
def __repr__(self):
return f"<Question(id={self.id}, difficulty={self.difficulty}, status='{self.status}')>"