This commit is contained in:
stijndcl 2022-07-27 21:10:43 +02:00
parent ea4181eac0
commit db499f3742
20 changed files with 350 additions and 101 deletions

2
database/constants.py Normal file
View file

@ -0,0 +1,2 @@
WORDLE_GUESS_COUNT = 6
WORDLE_WORD_LENGTH = 5

View file

@ -1,32 +1,47 @@
from typing import Optional
from database.enums import TempStorageKey
from database.mongo_types import MongoCollection
from database.schemas.mongo import WordleGame
from database.mongo_types import MongoDatabase
from database.schemas.mongo import TemporaryStorage, WordleGame
from database.utils.datetime import today_only_date
__all__ = ["get_active_wordle_game", "make_wordle_guess", "start_new_wordle_game"]
__all__ = [
"get_active_wordle_game",
"make_wordle_guess",
"start_new_wordle_game",
"set_daily_word",
"reset_wordle_games",
]
async def get_active_wordle_game(collection: MongoCollection, user_id: int) -> Optional[WordleGame]:
async def get_active_wordle_game(database: MongoDatabase, user_id: int) -> Optional[WordleGame]:
"""Find a player's active game"""
return await collection.find_one({"user_id": user_id})
collection = database[WordleGame.collection()]
result = await collection.find_one({"user_id": user_id})
if result is None:
return None
return WordleGame(**result)
async def start_new_wordle_game(collection: MongoCollection, user_id: int) -> WordleGame:
async def start_new_wordle_game(database: MongoDatabase, user_id: int) -> WordleGame:
"""Start a new game"""
collection = database[WordleGame.collection()]
game = WordleGame(user_id=user_id)
await collection.insert_one(game.dict(by_alias=True))
return game
async def make_wordle_guess(collection: MongoCollection, user_id: int, guess: str):
async def make_wordle_guess(database: MongoDatabase, user_id: int, guess: str):
"""Make a guess in your current game"""
collection = database[WordleGame.collection()]
await collection.update_one({"user_id": user_id}, {"$push": {"guesses": guess}})
async def get_daily_word(collection: MongoCollection) -> Optional[str]:
async def get_daily_word(database: MongoDatabase) -> Optional[str]:
"""Get the word of today"""
collection = database[TemporaryStorage.collection()]
result = await collection.find_one({"key": TempStorageKey.WORDLE_WORD, "day": today_only_date()})
if result is None:
return None
@ -34,16 +49,33 @@ async def get_daily_word(collection: MongoCollection) -> Optional[str]:
return result["word"]
async def set_daily_word(collection: MongoCollection, word: str):
async def set_daily_word(database: MongoDatabase, word: str, *, forced: bool = False) -> str:
"""Set the word of today
This does NOT overwrite the existing word if there is one, so that it can safely run
on startup every time
on startup every time.
In order to always overwrite the current word, set the "forced"-kwarg to True.
Returns the word that was chosen. If one already existed, return that instead.
"""
current_word = await get_daily_word(collection)
collection = database[TemporaryStorage.collection()]
current_word = None if forced else await get_daily_word(collection)
if current_word is not None:
return
return current_word
await collection.update_one(
{"key": TempStorageKey.WORDLE_WORD}, {"day": today_only_date(), "word": word}, upsert=True
{"key": TempStorageKey.WORDLE_WORD}, {"$set": {"day": today_only_date(), "word": word}}, upsert=True
)
# Remove all active games
await reset_wordle_games(database)
return word
async def reset_wordle_games(database: MongoDatabase):
"""Reset all active games"""
collection = database[WordleGame.collection()]
await collection.drop()

View file

@ -4,14 +4,14 @@ from typing import Optional
from bson import ObjectId
from overrides import overrides
from pydantic import BaseModel, Field, conlist
from pydantic import BaseModel, Field, validator
__all__ = ["MongoBase", "TemporaryStorage", "WordleGame"]
from database.utils.datetime import today_only_date
class PyObjectId(str):
class PyObjectId(ObjectId):
"""Custom type for bson ObjectIds"""
@classmethod
@ -71,12 +71,20 @@ class TemporaryStorage(MongoCollection):
class WordleStats(BaseModel):
"""Model that holds stats about a player's Wordle performance"""
guess_distribution: conlist(int, min_items=6, max_items=6) = Field(default_factory=lambda: [0, 0, 0, 0, 0, 0])
guess_distribution: list[int] = Field(default_factory=lambda: [0, 0, 0, 0, 0, 0])
last_guess: Optional[datetime.date] = None
win_rate: float = 0
current_streak: int = 0
max_streak: int = 0
@validator("guess_distribution")
def validate_guesses_length(cls, value: list[int]):
"""Check that the distribution of guesses is of the correct length"""
if len(value) != 6:
raise ValueError(f"guess_distribution must be length 6, found {len(value)}")
return value
class GameStats(MongoCollection):
"""Collection that holds stats about how well a user has performed in games"""
@ -94,10 +102,18 @@ class WordleGame(MongoCollection):
"""Collection that holds people's active Wordle games"""
day: datetime.date = Field(default_factory=lambda: today_only_date())
guesses: conlist(str, min_items=0, max_items=6) = Field(default_factory=list)
guesses: list[str] = Field(default_factory=list)
user_id: int
@staticmethod
@overrides
def collection() -> str:
return "wordle"
@validator("guesses")
def validate_guesses_length(cls, value: list[int]):
"""Check that the amount of guesses is of the correct length"""
if len(value) > 6:
raise ValueError(f"guess_distribution must be no longer than 6 elements, found {len(value)}")
return value

View file

@ -1,14 +1,18 @@
from abc import ABC, abstractmethod
from typing import Generic, TypeVar
from overrides import overrides
from sqlalchemy.ext.asyncio import AsyncSession
from database.crud import ufora_courses
from database.crud import ufora_courses, wordle
from database.mongo_types import MongoDatabase
__all__ = ["CacheManager"]
__all__ = ["CacheManager", "UforaCourseCache"]
T = TypeVar("T")
class DatabaseCache(ABC):
class DatabaseCache(ABC, Generic[T]):
"""Base class for a simple cache-like structure
The goal of this class is to store data for Discord auto-completion results
@ -31,10 +35,10 @@ class DatabaseCache(ABC):
self.data.clear()
@abstractmethod
async def refresh(self, database_session: AsyncSession):
async def refresh(self, database_session: T):
"""Refresh the data stored in this cache"""
async def invalidate(self, database_session: AsyncSession):
async def invalidate(self, database_session: T):
"""Invalidate the data stored in this cache"""
await self.refresh(database_session)
@ -45,7 +49,7 @@ class DatabaseCache(ABC):
return [self.data[index] for index, value in enumerate(self.data_transformed) if query in value]
class UforaCourseCache(DatabaseCache):
class UforaCourseCache(DatabaseCache[AsyncSession]):
"""Cache to store the names of Ufora courses"""
# Also store the aliases to add additional support
@ -90,14 +94,26 @@ class UforaCourseCache(DatabaseCache):
return sorted(list(results))
class WordleCache(DatabaseCache[MongoDatabase]):
"""Cache to store the current daily Wordle word"""
async def refresh(self, database_session: MongoDatabase):
word = await wordle.get_daily_word(database_session)
if word is not None:
self.data = [word]
class CacheManager:
"""Class that keeps track of all caches"""
ufora_courses: UforaCourseCache
wordle_word: WordleCache
def __init__(self):
self.ufora_courses = UforaCourseCache()
self.wordle_word = WordleCache()
async def initialize_caches(self, database_session: AsyncSession):
async def initialize_caches(self, postgres_session: AsyncSession, mongo_db: MongoDatabase):
"""Initialize the contents of all caches"""
await self.ufora_courses.refresh(database_session)
await self.ufora_courses.refresh(postgres_session)
await self.wordle_word.refresh(mongo_db)