diff --git a/shvatka/core/games/interactors.py b/shvatka/core/games/interactors.py
index bd7a73a6..93c5d045 100644
--- a/shvatka/core/games/interactors.py
+++ b/shvatka/core/games/interactors.py
@@ -84,7 +84,7 @@ async def __call__(self, user: dto.User) -> CurrentHints:
level_time = await self.dao.get_current_level_time(team, game)
level = await self.dao.get_level_by_game_and_number(game, level_time.level_number)
hints = level.get_hints_for_timedelta(datetime.now(tz=tz_utc) - level_time.start_at)
- keys = await self.dao.get_team_typed_keys(game, team, level_time.level_number)
+ keys = await self.dao.get_team_typed_keys(game, team, level_time)
return CurrentHints(
hints=hints,
typed_keys=keys,
diff --git a/shvatka/core/interfaces/dal/game_play.py b/shvatka/core/interfaces/dal/game_play.py
index 69ed93a4..f49a790f 100644
--- a/shvatka/core/interfaces/dal/game_play.py
+++ b/shvatka/core/interfaces/dal/game_play.py
@@ -19,7 +19,7 @@ async def get_poll_msg(self, team: dto.Team, game: dto.Game) -> int | None:
class GamePlayerDao(Committer, WaiverChecker, GameOrgsGetter, LevelByTeamGetter, Protocol):
- async def is_key_duplicate(self, level: dto.Level, team: dto.Team, key: str) -> bool:
+ async def is_key_duplicate(self, level: dto.LevelTime, team: dto.Team, key: str) -> bool:
raise NotImplementedError
async def get_played_teams(self, game: dto.Game) -> Iterable[dto.Team]:
@@ -36,7 +36,7 @@ async def get_current_level(self, team: dto.Team, game: dto.Game) -> dto.Level:
async def get_correct_typed_keys(
self,
- level: dto.Level,
+ level_time: dto.LevelTime,
game: dto.Game,
team: dto.Team,
) -> set[str]:
@@ -46,7 +46,7 @@ async def save_key(
self,
key: str,
team: dto.Team,
- level: dto.Level,
+ level_time: dto.LevelTime,
game: dto.Game,
player: dto.Player,
type_: enums.KeyType,
@@ -55,12 +55,20 @@ async def save_key(
raise NotImplementedError
async def get_team_typed_keys(
- self, game: dto.Game, team: dto.Team, level_number: int
+ self, game: dto.Game, team: dto.Team, level_time: dto.LevelTime
) -> list[dto.KeyTime]:
raise NotImplementedError
- async def level_up(self, team: dto.Team, level: dto.Level, game: dto.Game) -> None:
+ async def level_up(
+ self, team: dto.Team, level: dto.Level, game: dto.Game, next_level_number: int
+ ) -> None:
raise NotImplementedError
async def finish(self, game: dto.Game) -> None:
raise NotImplementedError
+
+ async def get_next_level(self, level: dto.Level, game: dto.Game) -> dto.Level:
+ raise NotImplementedError
+
+ async def get_level_by_name(self, level_name: str, game: dto.Game) -> dto.Level | None:
+ raise NotImplementedError
diff --git a/shvatka/core/interfaces/dal/key_log.py b/shvatka/core/interfaces/dal/key_log.py
index 4af70c7c..095dcce4 100644
--- a/shvatka/core/interfaces/dal/key_log.py
+++ b/shvatka/core/interfaces/dal/key_log.py
@@ -10,7 +10,7 @@ async def get_typed_keys_grouped(self, game: dto.Game) -> dict[dto.Team, list[dt
class GameTeamKeyGetter(Protocol):
async def get_team_typed_keys(
- self, game: dto.Game, team: dto.Team, level_number: int
+ self, game: dto.Game, team: dto.Team, level_time: dto.LevelTime
) -> list[dto.KeyTime]:
raise NotImplementedError
diff --git a/shvatka/core/interfaces/dal/level_times.py b/shvatka/core/interfaces/dal/level_times.py
index 08e0a7b7..0b157f0f 100644
--- a/shvatka/core/interfaces/dal/level_times.py
+++ b/shvatka/core/interfaces/dal/level_times.py
@@ -11,12 +11,12 @@ async def set_game_started(self, game: dto.Game) -> None:
async def get_played_teams(self, game: dto.Game) -> Iterable[dto.Team]:
raise NotImplementedError
- async def set_teams_to_first_level(self, game: dto.Game, teams: Iterable[dto.Team]) -> None:
- raise NotImplementedError
-
-
-class LevelTimeChecker(Protocol):
- async def is_team_on_level(self, team: dto.Team, level: dto.Level) -> bool:
+ async def set_to_level(
+ self,
+ team: dto.Team,
+ game: dto.Game,
+ level_number: int,
+ ) -> dto.LevelTime:
raise NotImplementedError
diff --git a/shvatka/core/interfaces/scheduler/sheduler.py b/shvatka/core/interfaces/scheduler/sheduler.py
index b122e3d7..f40026e1 100644
--- a/shvatka/core/interfaces/scheduler/sheduler.py
+++ b/shvatka/core/interfaces/scheduler/sheduler.py
@@ -16,6 +16,7 @@ async def plain_hint(
level: dto.Level,
team: dto.Team,
hint_number: int,
+ lt_id: int,
run_at: datetime,
):
raise NotImplementedError
diff --git a/shvatka/core/models/dto/__init__.py b/shvatka/core/models/dto/__init__.py
index 5aa96090..617e0679 100644
--- a/shvatka/core/models/dto/__init__.py
+++ b/shvatka/core/models/dto/__init__.py
@@ -3,7 +3,7 @@
from .common import DateRange
from .forum_team import ForumTeam
from .forum_user import ForumUser
-from .game import Game, PreviewGame, FullGame, GameResults
+from .game import Game, PreviewGame, FullGame, GameResults, GameFinished
from .level import Level
from .level_testing import (
LevelTestSuite,
diff --git a/shvatka/core/models/dto/action/__init__.py b/shvatka/core/models/dto/action/__init__.py
index 5da040b9..0f835cf8 100644
--- a/shvatka/core/models/dto/action/__init__.py
+++ b/shvatka/core/models/dto/action/__init__.py
@@ -12,6 +12,7 @@
BonusKeyDecision,
KeyBonusCondition,
WrongKeyDecision,
+ LevelUpDecision,
)
from .state_holder import InMemoryStateHolder
diff --git a/shvatka/core/models/dto/action/interface.py b/shvatka/core/models/dto/action/interface.py
index 20cf00e9..21ccd265 100644
--- a/shvatka/core/models/dto/action/interface.py
+++ b/shvatka/core/models/dto/action/interface.py
@@ -33,13 +33,21 @@ def get(self, state_class: type[T]) -> T:
raise NotImplementedError
-class Decision(Protocol):
- type: DecisionType
-
-
class DecisionType(enum.StrEnum):
NOT_IMPLEMENTED = enum.auto()
LEVEL_UP = enum.auto()
SIGNIFICANT_ACTION = enum.auto()
NO_ACTION = enum.auto()
BONUS_TIME = enum.auto()
+
+
+class Decision(Protocol):
+ type: DecisionType
+
+ def is_level_up(self) -> bool:
+ return self.type == DecisionType.LEVEL_UP
+
+
+class LevelUpDecision(Decision):
+ type: typing.Literal[DecisionType.LEVEL_UP] = DecisionType.LEVEL_UP
+ next_level: str | None = None
diff --git a/shvatka/core/models/dto/action/keys.py b/shvatka/core/models/dto/action/keys.py
index a0d1f68f..aeb3f545 100644
--- a/shvatka/core/models/dto/action/keys.py
+++ b/shvatka/core/models/dto/action/keys.py
@@ -3,9 +3,18 @@
from typing import Literal
from shvatka.core.models import enums
+from shvatka.core.utils.input_validation import is_key_valid
from . import StateHolder
from .decisions import NotImplementedActionDecision
-from .interface import Action, State, Decision, Condition, DecisionType, ConditionType
+from .interface import (
+ Action,
+ State,
+ Decision,
+ Condition,
+ DecisionType,
+ ConditionType,
+ LevelUpDecision,
+)
SHKey: typing.TypeAlias = str
@@ -15,6 +24,12 @@ class BonusKey:
text: SHKey
bonus_minutes: float
+ def __post_init__(self):
+ if not is_key_valid(self.text):
+ raise ValueError
+ if not (-600 < self.bonus_minutes < 60):
+ raise ValueError("bonus out of available range")
+
def __eq__(self, other: object) -> bool:
if not isinstance(other, BonusKey):
return NotImplemented
@@ -50,25 +65,29 @@ def key_text(self) -> str:
return self.key
-@dataclass
+@dataclass(kw_only=True)
class KeyDecision(Decision):
type: DecisionType
key_type: enums.KeyType
duplicate: bool
key: SHKey
- def is_level_up(self) -> bool:
- return self.type == DecisionType.LEVEL_UP
-
@property
def key_text(self) -> str:
return self.key
+@dataclass(kw_only=True)
+class LevelUpKeyDecision(KeyDecision, LevelUpDecision):
+ type: typing.Literal[DecisionType.LEVEL_UP] = DecisionType.LEVEL_UP
+ next_level: str | None = None
+
+
@dataclass
class KeyWinCondition(Condition):
keys: set[SHKey]
type: Literal["WIN_KEY"] = ConditionType.WIN_KEY.name
+ next_level: str | None = None
def check(self, action: Action, state_holder: StateHolder) -> Decision:
if not isinstance(action, TypedKeyAction):
@@ -79,18 +98,26 @@ def check(self, action: Action, state_holder: StateHolder) -> Decision:
return WrongKeyDecision(duplicate=state.is_duplicate(action), key=action.key)
if not state.is_duplicate(action):
if self._is_all_typed(action, state):
- type_ = DecisionType.LEVEL_UP
+ return LevelUpKeyDecision(
+ key_type=self._get_key_type(action), # TODO always simple
+ duplicate=state.is_duplicate(action),
+ key=action.key,
+ next_level=self.next_level,
+ )
else:
type_ = DecisionType.SIGNIFICANT_ACTION
else:
type_ = DecisionType.NO_ACTION
return KeyDecision(
type=type_,
- key_type=enums.KeyType.simple if self._is_correct(action) else enums.KeyType.wrong,
+ key_type=self._get_key_type(action), # TODO always simple
duplicate=state.is_duplicate(action),
key=action.key,
)
+ def _get_key_type(self, action: TypedKeyAction):
+ return enums.KeyType.simple if self._is_correct(action) else enums.KeyType.wrong
+
def _is_correct(self, action: TypedKeyAction) -> bool:
return action.key in self.keys
diff --git a/shvatka/core/models/dto/forum_user.py b/shvatka/core/models/dto/forum_user.py
index 7a8c713e..35ad6a2f 100644
--- a/shvatka/core/models/dto/forum_user.py
+++ b/shvatka/core/models/dto/forum_user.py
@@ -5,9 +5,9 @@
@dataclass
class ForumUser:
db_id: int
- forum_id: int
+ forum_id: int | None
name: str
- registered: date
+ registered: date | None
player_id: int
@property
diff --git a/shvatka/core/models/dto/game.py b/shvatka/core/models/dto/game.py
index f7233f05..60f61b09 100644
--- a/shvatka/core/models/dto/game.py
+++ b/shvatka/core/models/dto/game.py
@@ -133,3 +133,8 @@ class GameResults:
published_chanel_id: int | None
results_picture_file_id: str | None
keys_url: str | None
+
+
+@dataclass
+class GameFinished:
+ game: Game
diff --git a/shvatka/core/models/dto/team_player.py b/shvatka/core/models/dto/team_player.py
index 2e6ee35e..f1eba920 100644
--- a/shvatka/core/models/dto/team_player.py
+++ b/shvatka/core/models/dto/team_player.py
@@ -17,7 +17,7 @@ class TeamPlayer:
date_joined: datetime
date_left: datetime | None
role: str
- emoji: str
+ emoji: str | None
_can_manage_waivers: bool
_can_manage_players: bool
diff --git a/shvatka/core/services/game_play.py b/shvatka/core/services/game_play.py
index 7566e4a5..09a6f87f 100644
--- a/shvatka/core/services/game_play.py
+++ b/shvatka/core/services/game_play.py
@@ -4,7 +4,7 @@
from datetime import timedelta, datetime
from shvatka.core.interfaces.dal.game_play import GamePreparer, GamePlayerDao
-from shvatka.core.interfaces.dal.level_times import GameStarter, LevelTimeChecker
+from shvatka.core.interfaces.dal.level_times import GameStarter, LevelByTeamGetter
from shvatka.core.interfaces.scheduler import Scheduler
from shvatka.core.models import dto, enums
from shvatka.core.models.dto import scn
@@ -70,13 +70,18 @@ async def start_game(
logger.info("game %s started", game.id)
teams = await dao.get_played_teams(game)
- await dao.set_teams_to_first_level(game, teams)
+ level_times = {}
+ for team in teams:
+ level_times[team.id] = await dao.set_to_level(team=team, game=game, level_number=0)
await dao.commit()
await asyncio.gather(*[view.send_puzzle(team, game.levels[0]) for team in teams])
await asyncio.gather(
- *[schedule_first_hint(scheduler, team, game.levels[0], now) for team in teams]
+ *[
+ schedule_first_hint(scheduler, team, game.levels[0], level_times[team.id].id, now)
+ for team in teams
+ ]
)
await game_log.log(GameLogEvent(GameLogType.GAME_STARTED, {"game": game.name}))
@@ -168,9 +173,16 @@ async def process_level_up(
await finish_team(team, game, view, game_log, dao, locker)
return
next_level = await dao.get_current_level(team, game)
+ lt = await dao.get_current_level_time(team, game)
await view.send_puzzle(team=team, level=next_level)
- await schedule_first_hint(scheduler, team, next_level)
+ await schedule_first_hint(
+ scheduler=scheduler,
+ team=team,
+ next_level=next_level,
+ lt_id=lt.id,
+ now=datetime.now(tz=tz_utc),
+ )
level_up_event = LevelUp(
team=team, new_level=next_level, orgs_list=await get_spying_orgs(game, dao)
)
@@ -209,9 +221,11 @@ async def finish_team(
async def send_hint(
level: dto.Level,
+ lt_id: int,
hint_number: int,
team: dto.Team,
- dao: LevelTimeChecker,
+ game: dto.Game,
+ dao: LevelByTeamGetter,
view: GameView,
scheduler: Scheduler,
):
@@ -220,17 +234,22 @@ async def send_hint(
Если команда уже на следующем уровне - отправлять не надо.
:param level: Подсказка относится к уровню.
+ :param lt_id: Идентификатор соответствия уровня и команды.
:param hint_number: Номер подсказки, которую надо отправить.
:param team: Какой команде надо отправить подсказку.
+ :param game: Текущая игра.
:param dao: Слой доступа к данным.
:param view: Слой отображения.
:param scheduler: Планировщик.
"""
- if not await dao.is_team_on_level(team, level):
+ lt = await dao.get_current_level_time(team, game)
+ if lt.id != lt_id:
logger.debug(
- "team %s is not on level %s, skip sending hint #%s",
+ "team %s is not on level %s (should %s, actually %s), skip sending hint #%s",
team.id,
- level.db_id,
+ level.number_in_game,
+ lt_id,
+ lt.id,
hint_number,
)
return
@@ -248,24 +267,26 @@ async def send_hint(
level.get_hint(hint_number),
level.get_hint(next_hint_number),
)
- await scheduler.plain_hint(level, team, next_hint_number, next_hint_time)
+ await scheduler.plain_hint(level, team, next_hint_number, lt_id, next_hint_time)
async def schedule_first_hint(
scheduler: Scheduler,
team: dto.Team,
next_level: dto.Level,
- now: datetime | None = None,
+ lt_id: int,
+ now: datetime,
):
await scheduler.plain_hint(
level=next_level,
team=team,
hint_number=1,
+ lt_id=lt_id,
run_at=calculate_first_hint_time(next_level, now),
)
-def calculate_first_hint_time(next_level: dto.Level, now: datetime | None = None) -> datetime:
+def calculate_first_hint_time(next_level: dto.Level, now: datetime) -> datetime:
return calculate_next_hint_time(next_level.get_hint(0), next_level.get_hint(1), now)
diff --git a/shvatka/core/services/key.py b/shvatka/core/services/key.py
index 33c9d6cc..44e50b39 100644
--- a/shvatka/core/services/key.py
+++ b/shvatka/core/services/key.py
@@ -33,19 +33,17 @@ async def submit_key(
team: dto.Team,
) -> dto.InsertedKey | None:
async with self.locker(team):
- level = await self.dao.get_current_level(team, self.game)
- assert level.number_in_game is not None
+ level_time = await self.dao.get_current_level_time(team, self.game)
+ lvl = await self.dao.get_current_level(team, self.game)
correct_keys = await self.dao.get_correct_typed_keys(
- level=level, game=self.game, team=team
- )
- all_typed = await self.dao.get_team_typed_keys(
- self.game, team, level_number=level.number_in_game
+ level_time=level_time, game=self.game, team=team
)
+ all_typed = await self.dao.get_team_typed_keys(self.game, team, level_time=level_time)
state = action.InMemoryStateHolder(
typed_correct=correct_keys,
all_typed={k.text for k in all_typed},
)
- decision = level.scenario.check(
+ decision = lvl.scenario.check(
action=action.TypedKeyAction(key=key),
state=state,
)
@@ -55,14 +53,19 @@ async def submit_key(
saved_key = await self.dao.save_key(
key=decision.key_text,
team=team,
- level=level,
+ level_time=level_time,
game=self.game,
player=player,
type_=decision.key_type,
is_duplicate=decision.duplicate,
)
- if is_level_up := decision.type == action.DecisionType.LEVEL_UP:
- await self.dao.level_up(team=team, level=level, game=self.game)
+ if is_level_up := isinstance(decision, action.LevelUpDecision):
+ await self.dao.level_up(
+ team=team,
+ level=lvl,
+ game=self.game,
+ next_level_number=await self.define_next_level(lvl, decision.next_level),
+ )
await self.dao.commit()
return dto.InsertedKey.from_key_time(
saved_key, is_level_up, parsed_key=decision_to_parsed_key(decision)
@@ -74,6 +77,23 @@ async def submit_key(
logger.warning("impossible decision here is %s", type(decision))
return None
+ async def define_next_level(self, level: dto.Level, level_name: str | None = None) -> int:
+ if level_name is None:
+ assert level.number_in_game is not None
+ if len(self.game.levels) == level.number_in_game + 1:
+ return level.number_in_game + 1
+ next_level_ = await self.dao.get_next_level(level, self.game)
+ assert next_level_.number_in_game is not None
+ return next_level_.number_in_game
+ else:
+ next_level = await self.dao.get_level_by_name(level_name, self.game)
+ if next_level is None:
+ raise exceptions.ScenarioNotCorrect(
+ text="Level name not found", name_id=level_name
+ )
+ assert next_level.number_in_game is not None
+ return next_level.number_in_game
+
def decision_to_parsed_key(
decision: action.KeyDecision | action.BonusKeyDecision | action.WrongKeyDecision,
diff --git a/shvatka/core/services/scenario/scn_zip.py b/shvatka/core/services/scenario/scn_zip.py
index dc979c3e..29ce3504 100644
--- a/shvatka/core/services/scenario/scn_zip.py
+++ b/shvatka/core/services/scenario/scn_zip.py
@@ -34,11 +34,9 @@ def unpack_scn(zip_file: ZipPath) -> scn.ParsedZip:
def pack_scn(game: scn.RawGameScenario) -> BinaryIO:
output = BytesIO()
+ data = yaml.safe_dump(game.scn, allow_unicode=True, sort_keys=False)
with ZipFile(output, "a", ZIP_DEFLATED, False) as zipfile:
- zipfile.writestr(
- "scn.yaml",
- yaml.dump(game.scn, allow_unicode=True, sort_keys=False).encode("utf8"),
- )
+ zipfile.writestr("scn.yaml", data.encode("utf8"))
zipfile.writestr(RESULTS_FILENAME, json.dumps(game.stat, ensure_ascii=False, indent=2))
for guid, content in game.files.items():
zipfile.writestr(guid, content.read())
diff --git a/shvatka/infrastructure/crawler/game_scn/loader/load_scns.py b/shvatka/infrastructure/crawler/game_scn/loader/load_scns.py
index e0206bc1..e91d9eaa 100644
--- a/shvatka/infrastructure/crawler/game_scn/loader/load_scns.py
+++ b/shvatka/infrastructure/crawler/game_scn/loader/load_scns.py
@@ -94,13 +94,18 @@ async def set_results(game: dto.FullGame, results: GameStat, dao: HolderDao):
await dao.game.set_start_at(game, game_start_at)
await dao.game.set_number(game, results.id)
team_getter = get_team_getter(dao.team, results.team_identity)
+ level_times = []
for team_name, levels in results.results.items():
team = await team_getter(team_name)
for level in levels:
if level.at is not None:
- await dao.level_time.set_to_level(
- team, game, level.number, add_timezone(level.at, timezone=tz_utc)
+ lvl = await dao.level_time.set_to_level(
+ team,
+ game,
+ level.number,
+ add_timezone(level.at, timezone=tz_utc),
)
+ level_times.append(lvl)
for team_name, keys in results.keys.items():
team = await team_getter(team_name)
for i, key in enumerate(keys): # type: int, dto.export_stat.Key
@@ -118,7 +123,7 @@ async def set_results(game: dto.FullGame, results: GameStat, dao: HolderDao):
team=team,
game=game,
player=player,
- level=game.levels[key.level - 1],
+ level_time=level_times[key.level - 1],
type_=enums.KeyType.simple if is_correct else enums.KeyType.wrong,
is_duplicate=False,
at=add_timezone(key.at, timezone=tz_utc),
diff --git a/shvatka/infrastructure/db/dao/complex/game.py b/shvatka/infrastructure/db/dao/complex/game.py
index 3b7d81b6..9f775286 100644
--- a/shvatka/infrastructure/db/dao/complex/game.py
+++ b/shvatka/infrastructure/db/dao/complex/game.py
@@ -7,7 +7,6 @@
from shvatka.core.interfaces.dal.game import GameUpserter, GameCreator
from shvatka.core.models import dto
from shvatka.core.models.dto import scn
-from shvatka.infrastructure.db.dao import GameDao, LevelDao, FileInfoDao
if typing.TYPE_CHECKING:
from shvatka.infrastructure.db.dao.holder import HolderDao
@@ -15,12 +14,10 @@
@dataclass
class GameUpserterImpl(GameUpserter):
- game: GameDao
- level: LevelDao
- file_info: FileInfoDao
+ dao: "HolderDao"
async def upsert_game(self, author: dto.Player, scenario: scn.GameScenario) -> dto.Game:
- return await self.game.upsert_game(author, scenario)
+ return await self.dao.game.upsert_game(author, scenario)
async def upsert(
self,
@@ -29,46 +26,45 @@ async def upsert(
game: dto.Game | None = None,
no_in_game: int | None = None,
) -> dto.Level:
- return await self.level.upsert(author, scenario, game, no_in_game)
+ return await self.dao.level.upsert(author, scenario, game, no_in_game)
async def unlink_all(self, game: dto.Game) -> None:
- return await self.level.unlink_all(game)
+ return await self.dao.level.unlink_all(game)
async def upsert_file(self, file: scn.FileMeta, author: dto.Player) -> scn.SavedFileMeta:
- return await self.file_info.upsert(file, author)
+ return await self.dao.file_info.upsert(file, author)
async def check_author_can_own_guid(self, author: dto.Player, guid: str) -> None:
- return await self.file_info.check_author_can_own_guid(author, guid)
+ return await self.dao.file_info.check_author_can_own_guid(author, guid)
async def is_name_available(self, name: str) -> bool:
- return await self.game.is_name_available(name)
+ return await self.dao.game.is_name_available(name)
async def is_author_game_by_name(self, name: str, author: dto.Player) -> bool:
- return await self.game.is_author_game_by_name(name, author)
+ return await self.dao.game.is_author_game_by_name(name, author)
async def get_game_by_name(self, name: str, author: dto.Player) -> dto.Game:
- return await self.game.get_game_by_name(name=name, author=author)
+ return await self.dao.game.get_game_by_name(name=name, author=author)
async def commit(self) -> None:
- await self.level.commit()
+ await self.dao.commit()
@dataclass
class GameCreatorImpl(GameCreator):
- game: GameDao
- level: LevelDao
+ dao: "HolderDao"
async def create_game(self, author: dto.Player, name: str) -> dto.Game:
- return await self.game.create_game(author, name)
+ return await self.dao.game.create_game(author, name)
async def link_to_game(self, level: dto.Level, game: dto.Game) -> dto.Level:
- return await self.level.link_to_game(level, game)
+ return await self.dao.level.link_to_game(level, game)
async def commit(self) -> None:
- return await self.game.commit()
+ return await self.dao.commit()
async def is_name_available(self, name: str) -> bool:
- return await self.game.is_name_available(name)
+ return await self.dao.game.is_name_available(name)
@dataclass
@@ -150,6 +146,6 @@ async def get_level_by_game_and_number(self, game: dto.Game, number: int) -> dto
return await self.dao.level.get_by_number(game, number)
async def get_team_typed_keys(
- self, game: dto.Game, team: dto.Team, level_number: int
+ self, game: dto.Game, team: dto.Team, level_time: dto.LevelTime
) -> list[dto.KeyTime]:
- return await self.dao.key_time.get_team_typed_keys(game, team, level_number)
+ return await self.dao.key_time.get_team_typed_keys(game, team, level_time)
diff --git a/shvatka/infrastructure/db/dao/complex/game_play.py b/shvatka/infrastructure/db/dao/complex/game_play.py
index c451b12f..4ee9507c 100644
--- a/shvatka/infrastructure/db/dao/complex/game_play.py
+++ b/shvatka/infrastructure/db/dao/complex/game_play.py
@@ -1,82 +1,74 @@
+import typing
from dataclasses import dataclass
from typing import Iterable
+
from shvatka.core.interfaces.dal.game_play import GamePreparer, GamePlayerDao
from shvatka.core.interfaces.dal.level_times import GameStarter
from shvatka.core.models import dto, enums
-from shvatka.infrastructure.db.dao import (
- PollDao,
- WaiverDao,
- OrganizerDao,
- GameDao,
- LevelTimeDao,
- LevelDao,
- KeyTimeDao,
-)
+
+if typing.TYPE_CHECKING:
+ from shvatka.infrastructure.db.dao.holder import HolderDao
@dataclass
class GamePreparerImpl(GamePreparer):
- poll: PollDao
- waiver: WaiverDao
- org: OrganizerDao
+ dao: "HolderDao"
async def delete_poll_data(self) -> None:
- return await self.poll.delete_all()
+ return await self.dao.poll.delete_all()
async def get_agree_teams(self, game: dto.Game) -> Iterable[dto.Team]:
- return await self.waiver.get_played_teams(game)
+ return await self.dao.waiver.get_played_teams(game)
async def get_orgs(
self, game: dto.Game, with_deleted: bool = False
) -> list[dto.SecondaryOrganizer]:
- return await self.org.get_orgs(game)
+ return await self.dao.organizer.get_orgs(game)
async def get_poll_msg(self, team: dto.Team, game: dto.Game) -> int | None:
chat_id = team.get_chat_id()
assert chat_id is not None
- return await self.poll.get_poll_msg_id(chat_id=chat_id, game_id=game.id)
+ return await self.dao.poll.get_poll_msg_id(chat_id=chat_id, game_id=game.id)
@dataclass
class GameStarterImpl(GameStarter):
- game: GameDao
- waiver: WaiverDao
- level_times: LevelTimeDao
- level: LevelDao
+ dao: "HolderDao"
async def set_game_started(self, game: dto.Game) -> None:
- return await self.game.set_started(game)
+ return await self.dao.game.set_started(game)
async def get_played_teams(self, game: dto.Game) -> Iterable[dto.Team]:
- return await self.waiver.get_played_teams(game)
+ return await self.dao.waiver.get_played_teams(game)
- async def set_teams_to_first_level(self, game: dto.Game, teams: Iterable[dto.Team]) -> None:
- for team in teams:
- await self.level_times.set_to_level(team=team, game=game, level_number=0)
+ async def set_to_level(
+ self,
+ team: dto.Team,
+ game: dto.Game,
+ level_number: int,
+ ) -> dto.LevelTime:
+ return await self.dao.level_time.set_to_level(
+ team=team, game=game, level_number=level_number
+ )
async def commit(self) -> None:
- await self.game.commit()
+ await self.dao.commit()
@dataclass
class GamePlayerDaoImpl(GamePlayerDao):
- level_time: LevelTimeDao
- level: LevelDao
- key_time: KeyTimeDao
- waiver: WaiverDao
- game: GameDao
- organizer: OrganizerDao
+ dao: "HolderDao"
async def check_waiver(self, player: dto.Player, team: dto.Team, game: dto.Game) -> bool:
- return await self.waiver.check_waiver(player, team, game)
+ return await self.dao.waiver.check_waiver(player, team, game)
async def is_team_finished(self, team: dto.Team, game: dto.FullGame) -> bool:
- level_number = await self.level_time.get_current_level(team, game)
+ level_number = await self.dao.level_time.get_current_level(team, game)
return level_number == len(game.levels)
async def get_played_teams(self, game: dto.Game) -> Iterable[dto.Team]:
- return await self.waiver.get_played_teams(game)
+ return await self.dao.waiver.get_played_teams(game)
async def is_all_team_finished(self, game: dto.FullGame) -> bool:
for team in await self.get_played_teams(game):
@@ -84,36 +76,37 @@ async def is_all_team_finished(self, game: dto.FullGame) -> bool:
return False
return True
- async def is_key_duplicate(self, level: dto.Level, team: dto.Team, key: str) -> bool:
- return await self.key_time.is_duplicate(level, team, key)
+ async def is_key_duplicate(self, level: dto.LevelTime, team: dto.Team, key: str) -> bool:
+ return await self.dao.key_time.is_duplicate(level, team, key)
async def get_current_level(self, team: dto.Team, game: dto.Game) -> dto.Level:
- return await self.level.get_by_number(
- game=game, level_number=await self.level_time.get_current_level(team=team, game=game)
+ return await self.dao.level.get_by_number(
+ game=game,
+ level_number=await self.dao.level_time.get_current_level(team=team, game=game),
)
async def get_correct_typed_keys(
self,
- level: dto.Level,
+ level_time: dto.LevelTime,
game: dto.Game,
team: dto.Team,
) -> set[str]:
- return await self.key_time.get_correct_typed_keys(level, game, team)
+ return await self.dao.key_time.get_correct_typed_keys(level_time, game, team)
async def save_key(
self,
key: str,
team: dto.Team,
- level: dto.Level,
+ level_time: dto.LevelTime,
game: dto.Game,
player: dto.Player,
type_: enums.KeyType,
is_duplicate: bool,
) -> dto.KeyTime:
- return await self.key_time.save_key(
+ return await self.dao.key_time.save_key(
key=key,
team=team,
- level=level,
+ level_time=level_time,
game=game,
player=player,
type_=type_,
@@ -121,28 +114,36 @@ async def save_key(
)
async def get_team_typed_keys(
- self, game: dto.Game, team: dto.Team, level_number: int
+ self, game: dto.Game, team: dto.Team, level_time: dto.LevelTime
) -> list[dto.KeyTime]:
- return await self.key_time.get_team_typed_keys(game, team, level_number)
+ return await self.dao.key_time.get_team_typed_keys(game, team, level_time)
- async def level_up(self, team: dto.Team, level: dto.Level, game: dto.Game) -> None:
- assert level.number_in_game is not None
- await self.level_time.set_to_level(
+ async def level_up(
+ self, team: dto.Team, level: dto.Level, game: dto.Game, next_level_number: int
+ ) -> None:
+ await self.dao.level_time.set_to_level(
team=team,
game=game,
- level_number=level.number_in_game + 1,
+ level_number=next_level_number,
)
async def finish(self, game: dto.Game) -> None:
- await self.game.set_finished(game)
+ await self.dao.game.set_finished(game)
async def get_current_level_time(self, team: dto.Team, game: dto.Game) -> dto.LevelTime:
- return await self.level_time.get_current_level_time(team=team, game=game)
+ return await self.dao.level_time.get_current_level_time(team=team, game=game)
async def get_orgs(
self, game: dto.Game, with_deleted: bool = False
) -> list[dto.SecondaryOrganizer]:
- return await self.organizer.get_orgs(game)
+ return await self.dao.organizer.get_orgs(game)
+
+ async def get_next_level(self, level: dto.Level, game: dto.Game) -> dto.Level:
+ assert level.number_in_game is not None
+ return await self.dao.level.get_by_number(game=game, level_number=level.number_in_game + 1)
+
+ async def get_level_by_name(self, level_name: str, game: dto.Game) -> dto.Level | None:
+ return await self.dao.level.get_by_author_and_name_id(game.author, level_name)
async def commit(self) -> None:
- await self.key_time.commit()
+ await self.dao.commit()
diff --git a/shvatka/infrastructure/db/dao/complex/level_testing.py b/shvatka/infrastructure/db/dao/complex/level_testing.py
index 6c7934e4..97c576e3 100644
--- a/shvatka/infrastructure/db/dao/complex/level_testing.py
+++ b/shvatka/infrastructure/db/dao/complex/level_testing.py
@@ -1,43 +1,44 @@
+import typing
from dataclasses import dataclass
from datetime import datetime
from shvatka.core.interfaces.dal.level_testing import LevelTestingDao
from shvatka.core.models import dto
-from shvatka.infrastructure.db.dao import GameDao
-from shvatka.infrastructure.db.dao.memory.level_testing import LevelTestingData
+
+if typing.TYPE_CHECKING:
+ from shvatka.infrastructure.db.dao.holder import HolderDao
@dataclass
class LevelTestComplex(LevelTestingDao):
- game: GameDao
- level_testing: LevelTestingData
+ dao: "HolderDao"
async def save_started_level_test(self, suite: dto.LevelTestSuite, now: datetime):
- return await self.level_testing.save_started_level_test(suite, now)
+ return await self.dao.level_test.save_started_level_test(suite, now)
async def is_still_testing(self, suite: dto.LevelTestSuite) -> bool:
- return await self.level_testing.is_still_testing(suite)
+ return await self.dao.level_test.is_still_testing(suite)
async def save_key(self, key: str, suite: dto.LevelTestSuite, is_correct: bool):
- return await self.level_testing.save_key(key, suite, is_correct)
+ return await self.dao.level_test.save_key(key, suite, is_correct)
async def get_correct_tested_keys(self, suite: dto.LevelTestSuite) -> set[str]:
- return await self.level_testing.get_correct_tested_keys(suite)
+ return await self.dao.level_test.get_correct_tested_keys(suite)
async def complete_test(self, suite: dto.LevelTestSuite):
- return await self.level_testing.complete_test(suite)
+ return await self.dao.level_test.complete_test(suite)
async def get_testing_result(self, suite: dto.LevelTestSuite) -> dto.LevelTestingResult:
- return await self.level_testing.get_testing_result(suite)
+ return await self.dao.level_test.get_testing_result(suite)
async def get_by_id(self, id_: int, author: dto.Player | None = None) -> dto.Game:
- return await self.game.get_by_id(id_=id_, author=author)
+ return await self.dao.game.get_by_id(id_=id_, author=author)
async def get_full(self, id_: int) -> dto.FullGame:
- return await self.game.get_full(id_=id_)
+ return await self.dao.game.get_full(id_=id_)
async def add_levels(self, game: dto.Game) -> dto.FullGame:
- return await self.game.add_levels(game)
+ return await self.dao.game.add_levels(game)
async def commit(self) -> None:
- return await self.level_testing.commit()
+ return await self.dao.level_test.commit()
diff --git a/shvatka/infrastructure/db/dao/complex/orgs.py b/shvatka/infrastructure/db/dao/complex/orgs.py
index cb985eb9..e87d3dfe 100644
--- a/shvatka/infrastructure/db/dao/complex/orgs.py
+++ b/shvatka/infrastructure/db/dao/complex/orgs.py
@@ -1,38 +1,39 @@
+import typing
from dataclasses import dataclass
from shvatka.core.interfaces.dal.organizer import OrgAdder
from shvatka.core.models import dto
-from shvatka.infrastructure.db.dao import GameDao, OrganizerDao, SecureInvite
+
+if typing.TYPE_CHECKING:
+ from shvatka.infrastructure.db.dao.holder import HolderDao
@dataclass
class OrgAdderImpl(OrgAdder):
- game: GameDao
- organizer: OrganizerDao
- secure_invite: SecureInvite
+ dao: "HolderDao"
async def add_new_org(self, game: dto.Game, player: dto.Player) -> dto.SecondaryOrganizer:
- return await self.organizer.add_new(game, player)
+ return await self.dao.organizer.add_new(game, player)
async def commit(self) -> None:
- await self.game.commit()
+ await self.dao.commit()
async def get_invite(self, token: str) -> dict:
- return await self.secure_invite.get_invite(token)
+ return await self.dao.secure_invite.get_invite(token)
async def remove_invite(self, token: str) -> None:
- return await self.secure_invite.remove_invite(token)
+ return await self.dao.secure_invite.remove_invite(token)
async def get_by_id(self, id_: int, author: dto.Player | None = None) -> dto.Game:
- return await self.game.get_by_id(id_=id_, author=author)
+ return await self.dao.game.get_by_id(id_=id_, author=author)
async def get_full(self, id_: int) -> dto.FullGame:
- return await self.game.get_full(id_=id_)
+ return await self.dao.game.get_full(id_=id_)
async def get_orgs(
self, game: dto.Game, with_deleted: bool = False
) -> list[dto.SecondaryOrganizer]:
- return await self.organizer.get_orgs(game)
+ return await self.dao.organizer.get_orgs(game)
async def add_levels(self, game: dto.Game) -> dto.FullGame:
- return await self.game.add_levels(game)
+ return await self.dao.game.add_levels(game)
diff --git a/shvatka/infrastructure/db/dao/complex/waiver.py b/shvatka/infrastructure/db/dao/complex/waiver.py
index a0af7fc8..8eae0f69 100644
--- a/shvatka/infrastructure/db/dao/complex/waiver.py
+++ b/shvatka/infrastructure/db/dao/complex/waiver.py
@@ -1,17 +1,18 @@
+import typing
from dataclasses import dataclass
from typing import Iterable, Sequence
from shvatka.core.interfaces.dal.waiver import WaiverVoteAdder, WaiverVoteGetter, WaiverApprover
from shvatka.core.models import dto
from shvatka.core.models.enums.played import Played
-from shvatka.infrastructure.db.dao import PollDao, WaiverDao, PlayerDao, TeamPlayerDao
+
+if typing.TYPE_CHECKING:
+ from shvatka.infrastructure.db.dao.holder import HolderDao
@dataclass
class WaiverVoteAdderImpl(WaiverVoteAdder):
- poll: PollDao
- waiver: WaiverDao
- team_player: TeamPlayerDao
+ dao: "HolderDao"
async def is_excluded(
self,
@@ -19,43 +20,41 @@ async def is_excluded(
player: dto.Player,
team: dto.Team,
) -> bool:
- return await self.waiver.is_excluded(game, player, team)
+ return await self.dao.waiver.is_excluded(game, player, team)
async def add_player_vote(self, team_id: int, player_id: int, vote_var: str) -> None:
- return await self.poll.add_player_vote(team_id, player_id, vote_var)
+ return await self.dao.poll.add_player_vote(team_id, player_id, vote_var)
async def get_team_player(self, player: dto.Player) -> dto.TeamPlayer:
- return await self.team_player.get_team_player(player)
+ return await self.dao.team_player.get_team_player(player)
@dataclass
class WaiverVoteGetterImpl(WaiverVoteGetter):
- poll: PollDao
- player: PlayerDao
+ dao: "HolderDao"
async def get_dict_player_vote(self, team_id: int) -> dict[int, Played]:
- return await self.poll.get_dict_player_vote(team_id)
+ return await self.dao.poll.get_dict_player_vote(team_id)
async def get_by_ids_with_user_and_pit(self, ids: Iterable[int]) -> list[dto.VotedPlayer]:
- return await self.player.get_by_ids_with_user_and_pit(ids)
+ return await self.dao.player.get_by_ids_with_user_and_pit(ids)
@dataclass
class WaiverApproverImpl(WaiverApprover, WaiverVoteGetterImpl):
- waiver: WaiverDao
- team_player: TeamPlayerDao
+ dao: "HolderDao"
async def upsert(self, waiver: dto.Waiver) -> None:
- return await self.waiver.upsert(waiver)
+ return await self.dao.waiver.upsert(waiver)
async def commit(self) -> None:
- return await self.waiver.commit()
+ return await self.dao.waiver.commit()
async def get_team_player(self, player: dto.Player) -> dto.TeamPlayer:
- return await self.team_player.get_team_player(player)
+ return await self.dao.team_player.get_team_player(player)
async def get_players(self, team: dto.Team) -> Sequence[dto.FullTeamPlayer]:
- return await self.team_player.get_players(team)
+ return await self.dao.team_player.get_players(team)
async def del_player_vote(self, team_id: int, player_id: int) -> None:
- return await self.poll.del_player_vote(team_id, player_id)
+ return await self.dao.poll.del_player_vote(team_id, player_id)
diff --git a/shvatka/infrastructure/db/dao/holder.py b/shvatka/infrastructure/db/dao/holder.py
index b37f638d..f47aec36 100644
--- a/shvatka/infrastructure/db/dao/holder.py
+++ b/shvatka/infrastructure/db/dao/holder.py
@@ -83,27 +83,23 @@ async def commit(self):
@property
def waiver_vote_adder(self) -> WaiverVoteAdder:
- return WaiverVoteAdderImpl(
- poll=self.poll, waiver=self.waiver, team_player=self.team_player
- )
+ return WaiverVoteAdderImpl(dao=self)
@property
def waiver_vote_getter(self) -> WaiverVoteGetter:
- return WaiverVoteGetterImpl(poll=self.poll, player=self.player)
+ return WaiverVoteGetterImpl(dao=self)
@property
def waiver_approver(self) -> WaiverApprover:
- return WaiverApproverImpl(
- poll=self.poll, player=self.player, waiver=self.waiver, team_player=self.team_player
- )
+ return WaiverApproverImpl(dao=self)
@property
def game_upserter(self) -> GameUpserter:
- return GameUpserterImpl(game=self.game, level=self.level, file_info=self.file_info)
+ return GameUpserterImpl(dao=self)
@property
def game_creator(self) -> GameCreator:
- return GameCreatorImpl(game=self.game, level=self.level)
+ return GameCreatorImpl(dao=self)
@property
def game_packager(self) -> GamePackager:
@@ -123,33 +119,19 @@ def team_merger(self) -> TeamMerger:
@property
def game_preparer(self) -> GamePreparer:
- return GamePreparerImpl(poll=self.poll, waiver=self.waiver, org=self.organizer)
+ return GamePreparerImpl(dao=self)
@property
def game_starter(self) -> GameStarter:
- return GameStarterImpl(
- game=self.game,
- waiver=self.waiver,
- level_times=self.level_time,
- level=self.level,
- )
+ return GameStarterImpl(dao=self)
@property
def game_player(self) -> GamePlayerDao:
- return GamePlayerDaoImpl(
- level_time=self.level_time,
- level=self.level,
- key_time=self.key_time,
- waiver=self.waiver,
- game=self.game,
- organizer=self.organizer,
- )
+ return GamePlayerDaoImpl(dao=self)
@property
def org_adder(self) -> OrgAdder:
- return OrgAdderImpl(
- game=self.game, organizer=self.organizer, secure_invite=self.secure_invite
- )
+ return OrgAdderImpl(dao=self)
@property
def player_promoter(self) -> PlayerPromoter:
@@ -161,7 +143,7 @@ def player_merger(self) -> PlayerMerger:
@property
def level_testing_complex(self) -> LevelTestingDao:
- return LevelTestComplex(level_testing=self.level_test, game=self.game)
+ return LevelTestComplex(dao=self)
@property
def game_stat(self) -> GameStatDao:
diff --git a/shvatka/infrastructure/db/dao/rdb/level_times.py b/shvatka/infrastructure/db/dao/rdb/level_times.py
index f55171d5..3cd74000 100644
--- a/shvatka/infrastructure/db/dao/rdb/level_times.py
+++ b/shvatka/infrastructure/db/dao/rdb/level_times.py
@@ -18,10 +18,14 @@ def __init__(
super().__init__(models.LevelTime, session, clock=clock)
async def set_to_level(
- self, team: dto.Team, game: dto.Game, level_number: int, at: datetime | None = None
- ):
+ self,
+ team: dto.Team,
+ game: dto.Game,
+ level_number: int,
+ at: datetime | None = None,
+ ) -> dto.LevelTime:
if at is None:
- at = datetime.now(tz=tz_utc)
+ at = self.clock(tz_utc)
level_time = models.LevelTime(
game_id=game.id,
team_id=team.id,
@@ -29,12 +33,8 @@ async def set_to_level(
start_at=at,
)
self._save(level_time)
-
- async def is_team_on_level(self, team: dto.Team, level: dto.Level) -> bool:
- assert level.game_id is not None
- return (
- await self._get_current(team.id, level.game_id)
- ).level_number == level.number_in_game
+ await self._flush(level_time)
+ return level_time.to_dto(team=team, game=game)
async def get_current_level(self, team: dto.Team, game: dto.Game) -> int:
return (await self.get_current_level_time(team=team, game=game)).level_number
@@ -50,7 +50,7 @@ async def _get_current(self, team_id: int, game_id: int) -> models.LevelTime:
models.LevelTime.team_id == team_id,
models.LevelTime.game_id == game_id,
)
- .order_by(models.LevelTime.level_number.desc())
+ .order_by(models.LevelTime.start_at.desc())
.limit(1)
)
return result.scalar_one()
@@ -71,7 +71,7 @@ async def get_game_level_times(self, game: dto.Game) -> list[dto.LevelTime]:
)
.order_by(
models.LevelTime.team_id,
- models.LevelTime.level_number,
+ models.LevelTime.start_at,
)
)
return [
diff --git a/shvatka/infrastructure/db/dao/rdb/log_keys.py b/shvatka/infrastructure/db/dao/rdb/log_keys.py
index f75355f8..00c8d675 100644
--- a/shvatka/infrastructure/db/dao/rdb/log_keys.py
+++ b/shvatka/infrastructure/db/dao/rdb/log_keys.py
@@ -20,14 +20,14 @@ def __init__(
async def get_correct_typed_keys(
self,
- level: dto.Level,
+ level_time: dto.LevelTime,
game: dto.Game,
team: dto.Team,
) -> set[str]:
result: ScalarResult[models.KeyTime] = await self.session.scalars(
select(models.KeyTime).where(
models.KeyTime.game_id == game.id,
- models.KeyTime.level_number == level.number_in_game,
+ models.KeyTime.level_time_id == level_time.id,
models.KeyTime.team_id == team.id,
models.KeyTime.type_ == enums.KeyType.simple,
)
@@ -38,7 +38,7 @@ async def get_team_typed_keys(
self,
game: dto.Game,
team: dto.Team,
- level_number: int,
+ level_time: dto.LevelTime,
) -> list[dto.KeyTime]:
result: ScalarResult[models.KeyTime] = await self.session.scalars(
select(models.KeyTime)
@@ -49,7 +49,7 @@ async def get_team_typed_keys(
)
.where(
models.KeyTime.game_id == game.id,
- models.KeyTime.level_number == level_number,
+ models.KeyTime.level_time_id == level_time.id,
models.KeyTime.team_id == team.id,
)
)
@@ -58,12 +58,12 @@ async def get_team_typed_keys(
for key in result.all()
]
- async def is_duplicate(self, level: dto.Level, team: dto.Team, key: str) -> bool:
+ async def is_duplicate(self, level_time: dto.LevelTime, team: dto.Team, key: str) -> bool:
result: ScalarResult[int] = await self.session.scalars(
select(self.model.id)
.where(
- models.KeyTime.game_id == level.game_id,
- models.KeyTime.level_number == level.number_in_game,
+ models.KeyTime.game_id == level_time.game.id,
+ models.KeyTime.level_time_id == level_time.id,
models.KeyTime.team_id == team.id,
models.KeyTime.key_text == key,
)
@@ -75,7 +75,7 @@ async def save_key(
self,
key: str,
team: dto.Team,
- level: dto.Level,
+ level_time: dto.LevelTime,
game: dto.Game,
player: dto.Player,
type_: enums.KeyType,
@@ -87,7 +87,8 @@ async def save_key(
key_time = models.KeyTime(
key_text=key,
team_id=team.id,
- level_number=level.number_in_game,
+ level_number=level_time.level_number,
+ level_time_id=level_time.id,
game_id=game.id,
player_id=player.id,
type_=type_,
diff --git a/shvatka/infrastructure/db/migrations/README.md b/shvatka/infrastructure/db/migrations/README.md
index b30a6fab..3f11aa71 100644
--- a/shvatka/infrastructure/db/migrations/README.md
+++ b/shvatka/infrastructure/db/migrations/README.md
@@ -1,16 +1,16 @@
Generic single-database configuration with an async dbapi.
For create new migration
-```
+```sh
alembic revision --autogenerate -m "add new table"
```
for migrate
-```
+```sh
alembic upgrade +1
```
for rollback
-```
+```sh
alembic downgrade -1
```
diff --git a/shvatka/infrastructure/db/migrations/versions/20250224-222423_009b59123fdf_fixes_namings.py b/shvatka/infrastructure/db/migrations/versions/20250224-222423_009b59123fdf_fixes_namings.py
new file mode 100644
index 00000000..d905162f
--- /dev/null
+++ b/shvatka/infrastructure/db/migrations/versions/20250224-222423_009b59123fdf_fixes_namings.py
@@ -0,0 +1,106 @@
+"""fixes namings
+
+Revision ID: 009b59123fdf
+Revises: 1659768228ec
+Create Date: 2025-02-24 22:24:23.158449
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+from shvatka.infrastructure.db.models.level import ScenarioField
+
+# revision identifiers, used by Alembic.
+revision = "009b59123fdf"
+down_revision = "1659768228ec"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ op.alter_column(
+ "forum_teams", "name", existing_type=sa.TEXT(), type_=sa.String(), existing_nullable=False
+ )
+ op.alter_column(
+ "forum_teams", "url", existing_type=sa.TEXT(), type_=sa.String(), nullable=False
+ )
+ op.create_unique_constraint(op.f("uq__forum_teams__forum_id"), "forum_teams", ["forum_id"])
+ op.alter_column(
+ "forum_users", "name", existing_type=sa.TEXT(), type_=sa.String(), existing_nullable=False
+ )
+ op.alter_column(
+ "forum_users", "url", existing_type=sa.TEXT(), type_=sa.String(), existing_nullable=True
+ )
+ op.alter_column("forum_users", "player_id", existing_type=sa.BIGINT(), nullable=False)
+ op.alter_column(
+ "games", "name", existing_type=sa.TEXT(), type_=sa.String(), existing_nullable=False
+ )
+ op.alter_column("games", "manage_token", existing_type=sa.TEXT(), nullable=False)
+ op.alter_column(
+ "levels",
+ "scenario",
+ existing_type=postgresql.JSON(astext_type=sa.Text()),
+ type_=ScenarioField(astext_type=sa.Text()),
+ nullable=False,
+ )
+ op.alter_column("levels_times", "level_number", existing_type=sa.INTEGER(), nullable=False)
+ op.alter_column(
+ "team_players", "role", existing_type=sa.TEXT(), type_=sa.String(), nullable=False
+ )
+ op.alter_column(
+ "team_players", "emoji", existing_type=sa.TEXT(), type_=sa.String(), existing_nullable=True
+ )
+ op.alter_column(
+ "teams", "name", existing_type=sa.TEXT(), type_=sa.String(), existing_nullable=False
+ )
+ op.alter_column(
+ "teams", "description", existing_type=sa.TEXT(), type_=sa.String(), existing_nullable=True
+ )
+ op.alter_column(
+ "waivers", "role", existing_type=sa.TEXT(), type_=sa.String(), existing_nullable=True
+ )
+
+
+def downgrade():
+ op.alter_column(
+ "waivers", "role", existing_type=sa.String(), type_=sa.TEXT(), existing_nullable=True
+ )
+ op.alter_column(
+ "teams", "description", existing_type=sa.String(), type_=sa.TEXT(), existing_nullable=True
+ )
+ op.alter_column(
+ "teams", "name", existing_type=sa.String(), type_=sa.TEXT(), existing_nullable=False
+ )
+ op.alter_column(
+ "team_players", "emoji", existing_type=sa.String(), type_=sa.TEXT(), existing_nullable=True
+ )
+ op.alter_column(
+ "team_players", "role", existing_type=sa.String(), type_=sa.TEXT(), nullable=True
+ )
+ op.alter_column("levels_times", "level_number", existing_type=sa.INTEGER(), nullable=True)
+ op.alter_column(
+ "levels",
+ "scenario",
+ existing_type=ScenarioField(astext_type=sa.Text()),
+ type_=postgresql.JSON(astext_type=sa.Text()),
+ nullable=True,
+ )
+ op.alter_column("games", "manage_token", existing_type=sa.TEXT(), nullable=True)
+ op.alter_column(
+ "games", "name", existing_type=sa.String(), type_=sa.TEXT(), existing_nullable=False
+ )
+ op.alter_column("forum_users", "player_id", existing_type=sa.BIGINT(), nullable=True)
+ op.alter_column(
+ "forum_users", "url", existing_type=sa.String(), type_=sa.TEXT(), existing_nullable=True
+ )
+ op.alter_column(
+ "forum_users", "name", existing_type=sa.String(), type_=sa.TEXT(), existing_nullable=False
+ )
+ op.drop_constraint(op.f("uq__forum_teams__forum_id"), "forum_teams", type_="unique")
+ op.alter_column(
+ "forum_teams", "url", existing_type=sa.String(), type_=sa.TEXT(), nullable=True
+ )
+ op.alter_column(
+ "forum_teams", "name", existing_type=sa.String(), type_=sa.TEXT(), existing_nullable=False
+ )
diff --git a/shvatka/infrastructure/db/migrations/versions/20250224-223103_149de95bb84e_allow_level_times_cycles.py b/shvatka/infrastructure/db/migrations/versions/20250224-223103_149de95bb84e_allow_level_times_cycles.py
new file mode 100644
index 00000000..29b2977e
--- /dev/null
+++ b/shvatka/infrastructure/db/migrations/versions/20250224-223103_149de95bb84e_allow_level_times_cycles.py
@@ -0,0 +1,25 @@
+"""allow level times cycles
+
+Revision ID: 149de95bb84e
+Revises: 009b59123fdf
+Create Date: 2025-02-24 22:31:03.293138
+
+"""
+from alembic import op
+
+
+# revision identifiers, used by Alembic.
+revision = "149de95bb84e"
+down_revision = "009b59123fdf"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ op.drop_constraint("uq__levels_times__game_id", "levels_times", type_="unique")
+
+
+def downgrade():
+ op.create_unique_constraint(
+ "uq__levels_times__game_id", "levels_times", ["game_id", "team_id", "level_number"]
+ )
diff --git a/shvatka/infrastructure/db/migrations/versions/20250301-024158_f3157300bc04_added_level_time_fk.py b/shvatka/infrastructure/db/migrations/versions/20250301-024158_f3157300bc04_added_level_time_fk.py
new file mode 100644
index 00000000..b01bdce1
--- /dev/null
+++ b/shvatka/infrastructure/db/migrations/versions/20250301-024158_f3157300bc04_added_level_time_fk.py
@@ -0,0 +1,48 @@
+"""added level-time fk
+
+Revision ID: f3157300bc04
+Revises: 149de95bb84e
+Create Date: 2025-03-01 02:41:58.385413
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = "f3157300bc04"
+down_revision = "149de95bb84e"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ op.add_column("log_keys", sa.Column("level_time_id", sa.Integer(), nullable=True))
+ # next line update existing log_keys with level_time_id
+ # by same game_id, team_id and level_number
+ op.execute(
+ sa.text(
+ """
+ UPDATE log_keys
+ SET level_time_id = (
+ SELECT id
+ FROM levels_times
+ WHERE game_id = log_keys.game_id
+ AND team_id = log_keys.team_id
+ AND level_number = log_keys.level_number
+ AND start_at <= log_keys.enter_time
+ ORDER BY start_at DESC
+ LIMIT 1
+ )
+ WHERE level_time_id IS NULL
+ """
+ )
+ )
+ op.create_foreign_key(
+ op.f("log_keys_level_time_id_fkey"), "log_keys", "levels_times", ["level_time_id"], ["id"]
+ )
+
+
+def downgrade():
+ op.drop_constraint(op.f("log_keys_level_time_id_fkey"), "log_keys", type_="foreignkey")
+ op.drop_column("log_keys", "level_time_id")
diff --git a/shvatka/infrastructure/db/models/forum_team.py b/shvatka/infrastructure/db/models/forum_team.py
index 612a88dc..e29a3ea7 100644
--- a/shvatka/infrastructure/db/models/forum_team.py
+++ b/shvatka/infrastructure/db/models/forum_team.py
@@ -1,4 +1,4 @@
-from sqlalchemy import ForeignKey
+from sqlalchemy import ForeignKey, BigInteger
from sqlalchemy.orm import relationship, mapped_column, Mapped
from shvatka.core.models import dto
@@ -8,7 +8,7 @@
class ForumTeam(Base):
__tablename__ = "forum_teams"
__mapper_args__ = {"eager_defaults": True}
- id: Mapped[int] = mapped_column(primary_key=True)
+ id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
forum_id: Mapped[int] = mapped_column(unique=True, nullable=False)
name: Mapped[str] = mapped_column(nullable=False, unique=True)
url: Mapped[str]
diff --git a/shvatka/infrastructure/db/models/forum_user.py b/shvatka/infrastructure/db/models/forum_user.py
index d0fe69bb..7582e2d9 100644
--- a/shvatka/infrastructure/db/models/forum_user.py
+++ b/shvatka/infrastructure/db/models/forum_user.py
@@ -1,6 +1,6 @@
from datetime import date
-from sqlalchemy import ForeignKey
+from sqlalchemy import BigInteger, ForeignKey
from sqlalchemy.orm import relationship, mapped_column, Mapped
from shvatka.core.models import dto
@@ -10,11 +10,11 @@
class ForumUser(Base):
__tablename__ = "forum_users"
__mapper_args__ = {"eager_defaults": True}
- id: Mapped[int] = mapped_column(primary_key=True)
- forum_id: Mapped[int] = mapped_column(unique=True)
+ id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
+ forum_id: Mapped[int | None] = mapped_column(BigInteger, unique=True)
name: Mapped[str] = mapped_column(nullable=False, unique=True)
- registered: Mapped[date]
- url: Mapped[str]
+ registered: Mapped[date | None]
+ url: Mapped[str | None]
player_id: Mapped[int] = mapped_column(ForeignKey("players.id"), unique=True)
player = relationship(
diff --git a/shvatka/infrastructure/db/models/level.py b/shvatka/infrastructure/db/models/level.py
index 393085ff..52d37876 100644
--- a/shvatka/infrastructure/db/models/level.py
+++ b/shvatka/infrastructure/db/models/level.py
@@ -3,8 +3,9 @@
from typing import Any
from adaptix import Retort
-from sqlalchemy import Integer, Text, ForeignKey, JSON, TypeDecorator, UniqueConstraint
+from sqlalchemy import Integer, Text, ForeignKey, TypeDecorator, UniqueConstraint
from sqlalchemy.engine import Dialect
+from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import relationship, mapped_column, Mapped
from shvatka.common.factory import REQUIRED_GAME_RECIPES
@@ -20,7 +21,7 @@
class ScenarioField(TypeDecorator):
- impl = JSON
+ impl = JSONB
cache_ok = True
retort = Retort(
recipe=[
diff --git a/shvatka/infrastructure/db/models/levels_times.py b/shvatka/infrastructure/db/models/levels_times.py
index 474d6ddb..61800bd2 100644
--- a/shvatka/infrastructure/db/models/levels_times.py
+++ b/shvatka/infrastructure/db/models/levels_times.py
@@ -1,6 +1,6 @@
from datetime import datetime
-from sqlalchemy import Integer, ForeignKey, DateTime, UniqueConstraint, func
+from sqlalchemy import Integer, ForeignKey, DateTime, func
from sqlalchemy.orm import relationship, mapped_column, Mapped
from shvatka.core.models import dto
@@ -32,8 +32,6 @@ class LevelTime(Base):
nullable=False,
)
- __table_args__ = (UniqueConstraint("game_id", "team_id", "level_number"),)
-
def to_dto(self, game: dto.Game, team: dto.Team) -> dto.LevelTime:
return dto.LevelTime(
id=self.id,
diff --git a/shvatka/infrastructure/db/models/log_keys.py b/shvatka/infrastructure/db/models/log_keys.py
index bd32cbd0..7c1f5114 100644
--- a/shvatka/infrastructure/db/models/log_keys.py
+++ b/shvatka/infrastructure/db/models/log_keys.py
@@ -31,6 +31,7 @@ class KeyTime(Base):
back_populates="log_keys",
)
level_number = mapped_column(Integer, nullable=False)
+ level_time_id = mapped_column(ForeignKey("levels_times.id"), nullable=False)
enter_time = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.now(tz=tz_utc),
diff --git a/shvatka/infrastructure/db/models/player.py b/shvatka/infrastructure/db/models/player.py
index 45bf10fe..109084c1 100644
--- a/shvatka/infrastructure/db/models/player.py
+++ b/shvatka/infrastructure/db/models/player.py
@@ -10,7 +10,7 @@ class Player(Base):
__mapper_args__ = {"eager_defaults": True}
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
can_be_author: Mapped[bool] = mapped_column(Boolean, server_default="f", nullable=False)
- promoted_by_id: Mapped[int] = mapped_column(ForeignKey("players.id"))
+ promoted_by_id: Mapped[int | None] = mapped_column(ForeignKey("players.id"))
is_dummy: Mapped[bool] = mapped_column(nullable=False, default=False, server_default="f")
user = relationship(
"User",
diff --git a/shvatka/infrastructure/db/models/team.py b/shvatka/infrastructure/db/models/team.py
index 006fa555..6bc7f111 100644
--- a/shvatka/infrastructure/db/models/team.py
+++ b/shvatka/infrastructure/db/models/team.py
@@ -23,13 +23,13 @@ class Team(Base):
back_populates="team",
uselist=False,
)
- captain_id: Mapped[int] = mapped_column(ForeignKey("players.id"))
- captain: Mapped[Player] = relationship(
+ captain_id: Mapped[int | None] = mapped_column(ForeignKey("players.id"))
+ captain: Mapped[Player | None] = relationship(
"Player",
foreign_keys=captain_id,
back_populates="captain_by_team",
)
- description: Mapped[str]
+ description: Mapped[str | None]
completed_levels = relationship(
"LevelTime",
diff --git a/shvatka/infrastructure/db/models/team_player.py b/shvatka/infrastructure/db/models/team_player.py
index 17eb75e6..068ced39 100644
--- a/shvatka/infrastructure/db/models/team_player.py
+++ b/shvatka/infrastructure/db/models/team_player.py
@@ -31,7 +31,7 @@ class TeamPlayer(Base):
nullable=False,
)
role: Mapped[str]
- emoji: Mapped[str]
+ emoji: Mapped[str | None]
date_left: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
can_manage_waivers: Mapped[bool] = mapped_column(default=False, nullable=False)
diff --git a/shvatka/infrastructure/db/models/waiver.py b/shvatka/infrastructure/db/models/waiver.py
index 0df4df5f..8b51ec8d 100644
--- a/shvatka/infrastructure/db/models/waiver.py
+++ b/shvatka/infrastructure/db/models/waiver.py
@@ -29,7 +29,7 @@ class Waiver(Base):
foreign_keys=game_id,
back_populates="waivers",
)
- role: Mapped[str]
+ role: Mapped[str | None]
played: Mapped[Played] = mapped_column(Enum(Played), nullable=False)
def to_dto(self, player: dto.Player, team: dto.Team, game: dto.Game) -> dto.Waiver:
diff --git a/shvatka/infrastructure/scheduler/scheduler.py b/shvatka/infrastructure/scheduler/scheduler.py
index 94b28a9f..847a29b2 100644
--- a/shvatka/infrastructure/scheduler/scheduler.py
+++ b/shvatka/infrastructure/scheduler/scheduler.py
@@ -84,11 +84,17 @@ async def plain_hint(
level: dto.Level,
team: dto.Team,
hint_number: int,
+ lt_id: int,
run_at: datetime,
):
self.scheduler.add_job(
func="shvatka.infrastructure.scheduler.wrappers:send_hint_wrapper",
- kwargs={"level_id": level.db_id, "team_id": team.id, "hint_number": hint_number},
+ kwargs={
+ "level_id": level.db_id,
+ "team_id": team.id,
+ "hint_number": hint_number,
+ "lt_id": lt_id,
+ },
trigger="date",
run_date=run_at,
timezone=tz_utc,
diff --git a/shvatka/infrastructure/scheduler/wrappers.py b/shvatka/infrastructure/scheduler/wrappers.py
index 5e815194..55994ea4 100644
--- a/shvatka/infrastructure/scheduler/wrappers.py
+++ b/shvatka/infrastructure/scheduler/wrappers.py
@@ -58,6 +58,7 @@ async def send_hint_wrapper(
level_id: int,
team_id: int,
hint_number: int,
+ lt_id: int,
dao: FromDishka[HolderDao],
game_view: FromDishka[GameView],
scheduler: FromDishka[Scheduler],
@@ -66,11 +67,15 @@ async def send_hint_wrapper(
try:
level = await dao.level.get_by_id(level_id)
team = await dao.team.get_by_id(team_id)
+ game = await dao.game.get_active_game()
+ assert game is not None
await send_hint(
level=level,
hint_number=hint_number,
+ lt_id=lt_id,
team=team,
+ game=game,
dao=dao.level_time,
view=game_view,
scheduler=scheduler,
diff --git a/shvatka/tgbot/dialogs/game_spy/dialogs.py b/shvatka/tgbot/dialogs/game_spy/dialogs.py
index 652de6d2..c6db1758 100644
--- a/shvatka/tgbot/dialogs/game_spy/dialogs.py
+++ b/shvatka/tgbot/dialogs/game_spy/dialogs.py
@@ -38,15 +38,17 @@
Window(
Const("Актуальные сведения с полей схватки:"),
Jinja(
- "{% for lt in stat %}"
- "{% if lt.is_finished %}"
+ "{% for lt in finished %}"
"🏁{{ lt.team.name }} - финишировала в "
"{{ lt.start_at|time_user_timezone }} ({{(now - lt.start_at) | timedelta}} назад)\n"
- "{% else %}"
- "🚩{{ lt.team.name }} - ур {{ lt.level_number + 1 }} начат в "
+ "{% endfor %}"
+ "{% for i, lts in stat.items() %}"
+ "Уровень {{ i + 1 }}\n"
+ "{% for lt in lts %}"
+ "🚩{{ lt.team.name }} - начат в "
"{{ lt.start_at|time_user_timezone }} ({{(now - lt.start_at) | timedelta}} назад)\n"
"Подсказка №{{lt.hint.number}} — {{lt.hint.time}} мин.\n"
- "{% endif %}"
+ "{% endfor %}"
"{% endfor %}",
when=F["org"].can_spy,
),
diff --git a/shvatka/tgbot/dialogs/game_spy/getters.py b/shvatka/tgbot/dialogs/game_spy/getters.py
index b88cbfd5..2598f977 100644
--- a/shvatka/tgbot/dialogs/game_spy/getters.py
+++ b/shvatka/tgbot/dialogs/game_spy/getters.py
@@ -1,3 +1,4 @@
+from collections import defaultdict
from datetime import datetime
from aiogram_dialog import DialogManager
@@ -26,9 +27,20 @@ async def get_org(
async def get_spy(
dao: HolderDao, player: dto.Player, game: dto.Game, dialog_manager: DialogManager, **_
):
- stat = await get_game_spy(game, player, dao.game_stat)
+ stat = sorted(
+ await get_game_spy(game, player, dao.game_stat),
+ key=lambda x: (-x.level_number, x.start_at),
+ )
+ result = defaultdict(list)
+ finished = []
+ for s in stat:
+ if s.is_finished:
+ finished.append(s)
+ else:
+ result[s.level_number].append(s)
return {
- "stat": stat,
+ "stat": result,
+ "finished": finished,
"now": datetime.now(tz=tz_utc),
}
diff --git a/shvatka/tgbot/dialogs/level_manage/handlers.py b/shvatka/tgbot/dialogs/level_manage/handlers.py
index 6a2a0158..28d60d27 100644
--- a/shvatka/tgbot/dialogs/level_manage/handlers.py
+++ b/shvatka/tgbot/dialogs/level_manage/handlers.py
@@ -23,6 +23,7 @@
from shvatka.tgbot.views.hint_sender import HintSender
from shvatka.tgbot.views.user import render_small_card_link
from .getters import get_level_and_org, get_org
+from shvatka.tgbot.views.keys import render_level_keys
async def edit_level(c: CallbackQuery, button: Button, manager: DialogManager):
@@ -43,13 +44,7 @@ async def show_level(c: CallbackQuery, button: Button, manager: DialogManager):
async def show_all_hints(author: dto.Player, hint_sender: HintSender, bot: Bot, level: dto.Level):
- keys_text = "Ключи уровня:\n🔑{keys}".format(keys="\n🔑".join(level.get_keys()))
- if level.get_bonus_keys():
- keys_text += "\n\nБонусные ключи:\n💰{bonus_keys}".format(
- bonus_keys="\n💰".join(
- [f"{key.text} ({key.bonus_minutes:+.2f}) мин." for key in level.get_bonus_keys()]
- )
- )
+ keys_text = f"Ключи уровня:\n{render_level_keys(level.scenario)}"
await bot.send_message(author.get_chat_id(), keys_text)
for hint in level.scenario.time_hints:
await hint_sender.send_hints(
diff --git a/shvatka/tgbot/dialogs/level_scn/handlers.py b/shvatka/tgbot/dialogs/level_scn/handlers.py
index 5ad890f5..f4b03172 100644
--- a/shvatka/tgbot/dialogs/level_scn/handlers.py
+++ b/shvatka/tgbot/dialogs/level_scn/handlers.py
@@ -13,7 +13,6 @@
is_multiple_keys_normal,
normalize_key,
validate_level_id,
- is_key_valid,
)
from shvatka.infrastructure.db.dao.holder import HolderDao
from shvatka.tgbot import states
@@ -70,18 +69,11 @@ async def on_correct_keys(m: Message, dialog_: Any, manager: DialogManager, keys
await manager.done({"keys": keys})
-def convert_bonus_keys(text: str) -> list[shvatka.core.models.dto.action.keys.BonusKey]:
+def convert_bonus_keys(text: str) -> list[action.BonusKey]:
result = []
for key_str in text.splitlines():
key, bonus = key_str.split(maxsplit=1)
- if not is_key_valid(key):
- raise ValueError
- parsed_bonus = float(bonus)
- if not (-600 < parsed_bonus < 60):
- raise ValueError("bonus out of available range")
- parsed_key = shvatka.core.models.dto.action.keys.BonusKey(
- text=key, bonus_minutes=parsed_bonus
- )
+ parsed_key = action.BonusKey(text=key, bonus_minutes=float(bonus))
result.append(parsed_key)
return result
diff --git a/shvatka/tgbot/views/keys.py b/shvatka/tgbot/views/keys.py
index ae328e17..95dc32f6 100644
--- a/shvatka/tgbot/views/keys.py
+++ b/shvatka/tgbot/views/keys.py
@@ -6,6 +6,7 @@
from telegraph.aio import Telegraph
from shvatka.core.models import dto, enums
+from shvatka.core.models.dto import scn, action
from shvatka.core.services.game_stat import get_typed_keys
from shvatka.core.utils.datetime_utils import tz_game, DATETIME_FORMAT
from shvatka.infrastructure.db.dao.holder import HolderDao
@@ -33,12 +34,12 @@ def from_key(cls, key: dto.KeyTime) -> "KeyEmoji":
def render_log_keys(log_keys: dict[dto.Team, list[dto.KeyTime]]) -> str:
- text = f"Лог ключей на {datetime.now(tz=tz_game).strftime(DATETIME_FORMAT)}:
"
+ text = f"