Files
031-flash-card/src/flash_card/application/use_cases/study_session.py
T
Tirsvad f45d6036e4 MIL-008: Choose level
Review record RC-041 (Go, autonomous mode, not independent).

Task: MIL-008#1
Task: MIL-008#2
Task: MIL-008#3
Task: MIL-008#4
Task: MIL-008#5
Task: MIL-008#6
Task: MIL-008#7
Task: MIL-008#8
2026-10-14 23:58:31 +08:00

118 lines
3.9 KiB
Python

"""One study session.
@file study_session.py
@brief The StudySession use case: draws the cards the user studies.
"""
from collections.abc import Callable, Sequence
from flash_card.application.dto.card_dto import CardDto
from flash_card.application.ports.speech_port import SpeechPort
from flash_card.domain.entities.card import Card
from flash_card.domain.entities.deck import Deck
from flash_card.domain.repositories.progress_repository import ProgressRepository
class StudySession:
"""Draws cards from a deck, one after another."""
def __init__(
self,
deck: Deck,
choose: Callable[[Sequence[Card]], Card],
progress: ProgressRepository,
speech: SpeechPort,
level_cards: Sequence[Card] | None = None,
) -> None:
"""Create a session.
@param deck The cards still to learn.
@param choose Picks one card from a non-empty sequence.
@param progress Store of the words still to learn.
@param speech Speaks the front text of each drawn card.
@param level_cards The cards of the chosen level, or None for no limit.
"""
## @brief The cards of the chosen level, or None for no limit.
self._level_cards = None if level_cards is None else tuple(level_cards)
## @brief Speaks the front text of each drawn card.
self._speech = speech
## @brief Store of the words still to learn.
self._progress = progress
## @brief The cards still to learn.
self._deck = deck
## @brief Picks one card from a non-empty sequence.
self._choose = choose
## @brief The card shown now, or None.
self._current: Card | None = None
@property
def current_card(self) -> CardDto | None:
"""Return the card shown now.
@return The card, or None before the first draw or when none is left.
"""
return self._to_dto(self._current)
@property
def remaining(self) -> int:
"""Return the number of cards still to learn, of every level.
@return The card count.
"""
return len(self._deck)
@property
def remaining_in_level(self) -> int:
"""Return the number of cards still to learn in the chosen level.
@return The card count.
"""
return len(self._candidates())
def next_card(self) -> CardDto | None:
"""Draw the next card, different from the current one when possible.
@return The new card, or None when no card of the level is left.
"""
candidates = self._candidates()
if not candidates:
self._current = None
return None
card = self._deck.draw(self._choose, self._current, candidates)
self._current = card
self._speech.speak(card.front, self._deck.language_pair.front_code)
return self._to_dto(card)
def mark_known(self) -> None:
"""Remove the current card from the whole deck and save the whole deck.
Does nothing and saves nothing when no card is shown.
@return Nothing.
"""
if self._current is None:
return
self._deck.remove(self._current)
self._progress.save(self._deck)
self._current = None
def _candidates(self) -> tuple[Card, ...]:
"""Return the cards that may be drawn.
@return The deck cards of the level, or all deck cards without a level.
"""
if self._level_cards is None:
return self._deck.cards
return self._deck.within(self._level_cards)
def _to_dto(self, card: Card | None) -> CardDto | None:
"""Map a card to its DTO.
@param card The card, or None.
@return The DTO, or None when there is no card.
"""
if card is None:
return None
pair = self._deck.language_pair
return CardDto(card.front, card.back, pair.front_code, pair.back_code)