feat(matchmaking): support matchmaking (#48)
This commit is contained in:
@@ -33,6 +33,11 @@ from .item_attempts_count import (
|
||||
ItemAttemptsResp,
|
||||
PlaylistAggregateScore,
|
||||
)
|
||||
from .matchmaking import (
|
||||
MatchmakingPool,
|
||||
MatchmakingPoolBeatmap,
|
||||
MatchmakingUserStats,
|
||||
)
|
||||
from .multiplayer_event import MultiplayerEvent, MultiplayerEventResp
|
||||
from .notification import Notification, UserNotification
|
||||
from .password_reset import PasswordReset
|
||||
@@ -98,6 +103,9 @@ __all__ = [
|
||||
"ItemAttemptsResp",
|
||||
"LoginSession",
|
||||
"LoginSessionResp",
|
||||
"MatchmakingPool",
|
||||
"MatchmakingPoolBeatmap",
|
||||
"MatchmakingUserStats",
|
||||
"MeResp",
|
||||
"MonthlyPlaycounts",
|
||||
"MultiplayerEvent",
|
||||
|
||||
106
app/database/matchmaking.py
Normal file
106
app/database/matchmaking.py
Normal file
@@ -0,0 +1,106 @@
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from app.models.model import UTCBaseModel
|
||||
from app.models.mods import APIMod
|
||||
|
||||
from sqlalchemy import Column, DateTime, ForeignKey, Index, SmallInteger
|
||||
from sqlmodel import (
|
||||
JSON,
|
||||
BigInteger,
|
||||
Field,
|
||||
Relationship,
|
||||
SQLModel,
|
||||
func,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .beatmap import Beatmap
|
||||
from .user import User
|
||||
|
||||
|
||||
class MatchmakingUserStatsBase(SQLModel, UTCBaseModel):
|
||||
user_id: int = Field(
|
||||
default=None,
|
||||
sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), primary_key=True),
|
||||
)
|
||||
ruleset_id: int = Field(
|
||||
default=None,
|
||||
sa_column=Column(SmallInteger, primary_key=True),
|
||||
)
|
||||
first_placements: int = Field(default=0, ge=0)
|
||||
total_points: int = Field(default=0, ge=0)
|
||||
elo_data: dict[str, Any] | None = Field(default=None, sa_column=Column(JSON))
|
||||
created_at: datetime | None = Field(
|
||||
default=None,
|
||||
sa_column=Column(DateTime(timezone=True), server_default=func.now()),
|
||||
)
|
||||
updated_at: datetime | None = Field(
|
||||
default=None,
|
||||
sa_column=Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()),
|
||||
)
|
||||
|
||||
|
||||
class MatchmakingUserStats(MatchmakingUserStatsBase, table=True):
|
||||
__tablename__: str = "matchmaking_user_stats"
|
||||
|
||||
user: "User" = Relationship(back_populates="matchmaking_stats", sa_relationship_kwargs={"lazy": "joined"})
|
||||
|
||||
|
||||
class MatchmakingPoolBase(SQLModel, UTCBaseModel):
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
ruleset_id: int = Field(
|
||||
default=0,
|
||||
sa_column=Column(SmallInteger, nullable=False),
|
||||
)
|
||||
variant_id: int = Field(
|
||||
default=0,
|
||||
ge=0,
|
||||
sa_column=Column(SmallInteger, nullable=False, server_default="0"),
|
||||
)
|
||||
name: str = Field(max_length=255)
|
||||
active: bool = Field(default=True)
|
||||
created_at: datetime | None = Field(
|
||||
default=None,
|
||||
sa_column=Column(DateTime(timezone=True), server_default=func.now()),
|
||||
)
|
||||
updated_at: datetime | None = Field(
|
||||
default=None,
|
||||
sa_column=Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()),
|
||||
)
|
||||
|
||||
|
||||
class MatchmakingPool(MatchmakingPoolBase, table=True):
|
||||
__tablename__: str = "matchmaking_pools"
|
||||
__table_args__ = (Index("matchmaking_pools_ruleset_variant_active_idx", "ruleset_id", "variant_id", "active"),)
|
||||
|
||||
beatmaps: list["MatchmakingPoolBeatmap"] = Relationship(
|
||||
back_populates="pool",
|
||||
# sa_relationship_kwargs={
|
||||
# "lazy": "selectin",
|
||||
# },
|
||||
)
|
||||
|
||||
|
||||
class MatchmakingPoolBeatmapBase(SQLModel, UTCBaseModel):
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
pool_id: int = Field(
|
||||
default=None,
|
||||
sa_column=Column(ForeignKey("matchmaking_pools.id"), nullable=False, index=True),
|
||||
)
|
||||
beatmap_id: int = Field(
|
||||
default=None,
|
||||
sa_column=Column(ForeignKey("beatmaps.id"), nullable=False),
|
||||
)
|
||||
mods: list[APIMod] | None = Field(default=None, sa_column=Column(JSON))
|
||||
rating: int = Field(default=1500)
|
||||
selection_count: int = Field(default=0)
|
||||
|
||||
|
||||
class MatchmakingPoolBeatmap(MatchmakingPoolBeatmapBase, table=True):
|
||||
__tablename__: str = "matchmaking_pool_beatmaps"
|
||||
|
||||
pool: MatchmakingPool = Relationship(back_populates="beatmaps")
|
||||
beatmap: Optional["Beatmap"] = Relationship(
|
||||
# sa_relationship_kwargs={"lazy": "joined"},
|
||||
)
|
||||
@@ -42,6 +42,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .favourite_beatmapset import FavouriteBeatmapset
|
||||
from .matchmaking import MatchmakingUserStats
|
||||
from .relationship import RelationshipResp
|
||||
|
||||
|
||||
@@ -154,6 +155,7 @@ class User(AsyncAttrs, UserBase, table=True):
|
||||
achievement: list[UserAchievement] = Relationship(back_populates="user")
|
||||
team_membership: TeamMember | None = Relationship(back_populates="user")
|
||||
daily_challenge_stats: DailyChallengeStats | None = Relationship(back_populates="user")
|
||||
matchmaking_stats: list["MatchmakingUserStats"] = Relationship(back_populates="user")
|
||||
monthly_playcounts: list[MonthlyPlaycounts] = Relationship(back_populates="user")
|
||||
replays_watched_counts: list[ReplayWatchedCount] = Relationship(back_populates="user")
|
||||
favourite_beatmapsets: list["FavouriteBeatmapset"] = Relationship(back_populates="user")
|
||||
|
||||
@@ -15,6 +15,7 @@ class MatchType(str, Enum):
|
||||
PLAYLISTS = "playlists"
|
||||
HEAD_TO_HEAD = "head_to_head"
|
||||
TEAM_VERSUS = "team_versus"
|
||||
MATCHMAKING = "matchmaking"
|
||||
|
||||
|
||||
class QueueMode(str, Enum):
|
||||
|
||||
@@ -4,6 +4,7 @@ import base64
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from app.const import BANCHOBOT_ID
|
||||
from app.database.chat import ChannelType, ChatChannel # ChatChannel 模型 & 枚举
|
||||
from app.database.playlists import Playlist as DBPlaylist
|
||||
from app.database.room import Room
|
||||
@@ -15,7 +16,7 @@ from app.dependencies.storage import StorageService
|
||||
from app.log import log
|
||||
from app.models.playlist import PlaylistItem
|
||||
from app.models.room import MatchType, QueueMode, RoomCategory, RoomStatus
|
||||
from app.utils import utcnow
|
||||
from app.utils import camel_to_snake, utcnow
|
||||
|
||||
from .notification.server import server
|
||||
|
||||
@@ -91,12 +92,12 @@ async def _validate_user_exists(db: Database, user_id: int) -> User:
|
||||
def _parse_room_enums(match_type: str, queue_mode: str) -> tuple[MatchType, QueueMode]:
|
||||
"""Parse and validate room type enums."""
|
||||
try:
|
||||
match_type_enum = MatchType(match_type.lower())
|
||||
match_type_enum = MatchType(camel_to_snake(match_type))
|
||||
except ValueError:
|
||||
match_type_enum = MatchType.HEAD_TO_HEAD
|
||||
|
||||
try:
|
||||
queue_mode_enum = QueueMode(queue_mode.lower())
|
||||
queue_mode_enum = QueueMode(camel_to_snake(queue_mode))
|
||||
except ValueError:
|
||||
queue_mode_enum = QueueMode.HOST_ONLY
|
||||
|
||||
@@ -157,10 +158,10 @@ def _validate_playlist_items(items: list[dict[str, Any]]) -> None:
|
||||
|
||||
|
||||
async def _create_room(db: Database, room_data: dict[str, Any]) -> tuple[Room, int]:
|
||||
host_user_id = room_data.get("user_id")
|
||||
room_name = room_data.get("name", "Unnamed Room")
|
||||
host_user_id = room_data.get("user_id", BANCHOBOT_ID)
|
||||
match_type = room_data.get("match_type", "HeadToHead" if host_user_id != BANCHOBOT_ID else "Matchmaking")
|
||||
room_name = room_data.get("name", f"{match_type} room: {utcnow().isoformat()}")
|
||||
password = room_data.get("password")
|
||||
match_type = room_data.get("match_type", "HeadToHead")
|
||||
queue_mode = room_data.get("queue_mode", "HostOnly")
|
||||
|
||||
if not host_user_id or not isinstance(host_user_id, int):
|
||||
|
||||
@@ -12,7 +12,7 @@ from app.database.score import Score
|
||||
from app.database.user import User, UserResp
|
||||
from app.dependencies.database import Database, Redis
|
||||
from app.dependencies.user import ClientUser, get_current_user
|
||||
from app.models.room import RoomCategory, RoomStatus
|
||||
from app.models.room import MatchType, RoomCategory, RoomStatus
|
||||
from app.service.room import create_playlist_room_from_api
|
||||
from app.utils import utcnow
|
||||
|
||||
@@ -50,7 +50,7 @@ async def get_all_rooms(
|
||||
status: Annotated[RoomStatus | None, Query(description="房间状态(可选)")] = None,
|
||||
):
|
||||
resp_list: list[RoomResp] = []
|
||||
where_clauses: list[ColumnElement[bool]] = [col(Room.category) == category]
|
||||
where_clauses: list[ColumnElement[bool]] = [col(Room.category) == category, col(Room.type) != MatchType.MATCHMAKING]
|
||||
now = utcnow()
|
||||
|
||||
if status is not None:
|
||||
|
||||
Reference in New Issue
Block a user