You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
716 lines
20 KiB
Go
716 lines
20 KiB
Go
package gamesession
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"knowfoolery/backend/shared/types"
|
|
"knowfoolery/backend/shared/errors"
|
|
"knowfoolery/backend/shared/valueobjects"
|
|
sessionValueObjects "knowfoolery/backend/services/game-session-service/internal/domain/valueobjects"
|
|
)
|
|
|
|
// GameSession represents a quiz game session for a player
|
|
type GameSession struct {
|
|
id types.GameSessionID
|
|
userID types.UserID
|
|
playerName *valueobjects.PlayerName
|
|
status types.SessionStatus
|
|
score *sessionValueObjects.Score
|
|
timer *sessionValueObjects.SessionTimer
|
|
currentQuestionID *types.QuestionID
|
|
questionsAsked []types.QuestionID // Track all questions asked
|
|
attempts map[types.QuestionID][]*sessionValueObjects.Attempt // Attempts per question
|
|
questionsCorrect int
|
|
hintsUsed int
|
|
hintUsedForCurrentQ bool // Track if hint was used for current question
|
|
lastActivityAt types.Timestamp
|
|
createdAt types.Timestamp
|
|
updatedAt types.Timestamp
|
|
}
|
|
|
|
// NewGameSession creates a new game session
|
|
func NewGameSession(
|
|
userID types.UserID,
|
|
playerName *valueobjects.PlayerName,
|
|
) (*GameSession, error) {
|
|
if userID.IsEmpty() {
|
|
return nil, errors.ErrValidationFailed("userID", "user ID cannot be empty")
|
|
}
|
|
|
|
if playerName == nil || playerName.IsEmpty() {
|
|
return nil, errors.ErrInvalidPlayerName(playerName.String(), "player name cannot be empty")
|
|
}
|
|
|
|
now := types.NewTimestamp()
|
|
|
|
gameSession := &GameSession{
|
|
id: types.NewGameSessionID(),
|
|
userID: userID,
|
|
playerName: playerName,
|
|
status: types.SessionStatusCreated,
|
|
score: sessionValueObjects.NewZeroScore(),
|
|
timer: nil, // Will be set when session starts
|
|
currentQuestionID: nil,
|
|
questionsAsked: make([]types.QuestionID, 0),
|
|
attempts: make(map[types.QuestionID][]*sessionValueObjects.Attempt),
|
|
questionsCorrect: 0,
|
|
hintsUsed: 0,
|
|
hintUsedForCurrentQ: false,
|
|
lastActivityAt: now,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
}
|
|
|
|
return gameSession, nil
|
|
}
|
|
|
|
// NewGameSessionWithID creates a GameSession with specific ID (for loading from persistence)
|
|
func NewGameSessionWithID(
|
|
id types.GameSessionID,
|
|
userID types.UserID,
|
|
playerName *valueobjects.PlayerName,
|
|
status types.SessionStatus,
|
|
score *sessionValueObjects.Score,
|
|
timer *sessionValueObjects.SessionTimer,
|
|
currentQuestionID *types.QuestionID,
|
|
questionsAsked []types.QuestionID,
|
|
attempts map[types.QuestionID][]*sessionValueObjects.Attempt,
|
|
questionsCorrect int,
|
|
hintsUsed int,
|
|
lastActivityAt types.Timestamp,
|
|
createdAt types.Timestamp,
|
|
updatedAt types.Timestamp,
|
|
) (*GameSession, error) {
|
|
if id.IsEmpty() {
|
|
return nil, errors.ErrValidationFailed("id", "session ID cannot be empty")
|
|
}
|
|
|
|
if userID.IsEmpty() {
|
|
return nil, errors.ErrValidationFailed("userID", "user ID cannot be empty")
|
|
}
|
|
|
|
if playerName == nil {
|
|
return nil, errors.ErrInvalidPlayerName("", "player name cannot be nil")
|
|
}
|
|
|
|
if score == nil {
|
|
return nil, errors.ErrValidationFailed("score", "score cannot be nil")
|
|
}
|
|
|
|
if questionsAsked == nil {
|
|
questionsAsked = make([]types.QuestionID, 0)
|
|
}
|
|
|
|
if attempts == nil {
|
|
attempts = make(map[types.QuestionID][]*sessionValueObjects.Attempt)
|
|
}
|
|
|
|
return &GameSession{
|
|
id: id,
|
|
userID: userID,
|
|
playerName: playerName,
|
|
status: status,
|
|
score: score,
|
|
timer: timer,
|
|
currentQuestionID: currentQuestionID,
|
|
questionsAsked: questionsAsked,
|
|
attempts: attempts,
|
|
questionsCorrect: questionsCorrect,
|
|
hintsUsed: hintsUsed,
|
|
lastActivityAt: lastActivityAt,
|
|
createdAt: createdAt,
|
|
updatedAt: updatedAt,
|
|
}, nil
|
|
}
|
|
|
|
// ID returns the session ID
|
|
func (gs *GameSession) ID() types.GameSessionID {
|
|
return gs.id
|
|
}
|
|
|
|
// UserID returns the user ID
|
|
func (gs *GameSession) UserID() types.UserID {
|
|
return gs.userID
|
|
}
|
|
|
|
// PlayerName returns the player name
|
|
func (gs *GameSession) PlayerName() *valueobjects.PlayerName {
|
|
return gs.playerName
|
|
}
|
|
|
|
// Status returns the session status
|
|
func (gs *GameSession) Status() types.SessionStatus {
|
|
return gs.status
|
|
}
|
|
|
|
// Score returns the current score
|
|
func (gs *GameSession) Score() *sessionValueObjects.Score {
|
|
return gs.score
|
|
}
|
|
|
|
// Timer returns the session timer
|
|
func (gs *GameSession) Timer() *sessionValueObjects.SessionTimer {
|
|
return gs.timer
|
|
}
|
|
|
|
// CurrentQuestionID returns the current question ID
|
|
func (gs *GameSession) CurrentQuestionID() *types.QuestionID {
|
|
return gs.currentQuestionID
|
|
}
|
|
|
|
// QuestionsAsked returns all questions asked in this session
|
|
func (gs *GameSession) QuestionsAsked() []types.QuestionID {
|
|
return gs.questionsAsked
|
|
}
|
|
|
|
// QuestionsCorrect returns the number of questions answered correctly
|
|
func (gs *GameSession) QuestionsCorrect() int {
|
|
return gs.questionsCorrect
|
|
}
|
|
|
|
// HintsUsed returns the total number of hints used
|
|
func (gs *GameSession) HintsUsed() int {
|
|
return gs.hintsUsed
|
|
}
|
|
|
|
// CreatedAt returns when the session was created
|
|
func (gs *GameSession) CreatedAt() types.Timestamp {
|
|
return gs.createdAt
|
|
}
|
|
|
|
// UpdatedAt returns when the session was last updated
|
|
func (gs *GameSession) UpdatedAt() types.Timestamp {
|
|
return gs.updatedAt
|
|
}
|
|
|
|
// LastActivityAt returns the last activity timestamp
|
|
func (gs *GameSession) LastActivityAt() types.Timestamp {
|
|
return gs.lastActivityAt
|
|
}
|
|
|
|
// Start starts the game session
|
|
func (gs *GameSession) Start(firstQuestionID types.QuestionID) error {
|
|
// Validate state transition
|
|
if !gs.status.CanTransitionTo(types.SessionStatusActive) {
|
|
return errors.ErrInvalidSessionTransition(gs.status, types.SessionStatusActive)
|
|
}
|
|
|
|
if firstQuestionID.IsEmpty() {
|
|
return errors.ErrValidationFailed("firstQuestionID", "first question ID cannot be empty")
|
|
}
|
|
|
|
// Start the timer
|
|
gs.timer = sessionValueObjects.NewSessionTimerNow()
|
|
|
|
// Set first question
|
|
gs.currentQuestionID = &firstQuestionID
|
|
gs.questionsAsked = append(gs.questionsAsked, firstQuestionID)
|
|
|
|
// Update status and timestamps
|
|
gs.status = types.SessionStatusActive
|
|
now := types.NewTimestamp()
|
|
gs.lastActivityAt = now
|
|
gs.updatedAt = now
|
|
|
|
return nil
|
|
}
|
|
|
|
// PresentQuestion presents a new question to the player
|
|
func (gs *GameSession) PresentQuestion(questionID types.QuestionID) error {
|
|
if !gs.IsActive() {
|
|
return errors.ErrSessionNotActive(gs.id, gs.status)
|
|
}
|
|
|
|
if questionID.IsEmpty() {
|
|
return errors.ErrValidationFailed("questionID", "question ID cannot be empty")
|
|
}
|
|
|
|
// Check if question was already asked
|
|
if gs.wasQuestionAsked(questionID) {
|
|
return errors.ErrQuestionAlreadyAnswered(questionID, gs.id)
|
|
}
|
|
|
|
// Set new current question
|
|
gs.currentQuestionID = &questionID
|
|
gs.questionsAsked = append(gs.questionsAsked, questionID)
|
|
gs.hintUsedForCurrentQ = false // Reset hint usage for new question
|
|
|
|
// Update activity
|
|
now := types.NewTimestamp()
|
|
gs.lastActivityAt = now
|
|
gs.updatedAt = now
|
|
|
|
return nil
|
|
}
|
|
|
|
// RequestHint requests a hint for the current question
|
|
func (gs *GameSession) RequestHint() error {
|
|
if !gs.IsActive() {
|
|
return errors.ErrSessionNotActive(gs.id, gs.status)
|
|
}
|
|
|
|
if gs.currentQuestionID == nil {
|
|
return errors.ErrOperationNotAllowed("request hint", "no current question")
|
|
}
|
|
|
|
if gs.hintUsedForCurrentQ {
|
|
return errors.ErrHintAlreadyUsed(*gs.currentQuestionID)
|
|
}
|
|
|
|
// Mark hint as used
|
|
gs.hintUsedForCurrentQ = true
|
|
gs.hintsUsed++
|
|
|
|
// Update activity
|
|
now := types.NewTimestamp()
|
|
gs.lastActivityAt = now
|
|
gs.updatedAt = now
|
|
|
|
return nil
|
|
}
|
|
|
|
// SubmitAnswer submits an answer for the current question
|
|
func (gs *GameSession) SubmitAnswer(
|
|
playerAnswer string,
|
|
correctAnswer string,
|
|
similarity float64,
|
|
isFuzzyMatch bool,
|
|
timeSinceQuestionPresented types.Duration,
|
|
) (*sessionValueObjects.Attempt, error) {
|
|
|
|
// Validate session state
|
|
if !gs.IsActive() {
|
|
return nil, errors.ErrSessionNotActive(gs.id, gs.status)
|
|
}
|
|
|
|
if gs.currentQuestionID == nil {
|
|
return nil, errors.ErrOperationNotAllowed("submit answer", "no current question")
|
|
}
|
|
|
|
// Check if we've exceeded max attempts for this question
|
|
currentAttempts := gs.getAttemptsForQuestion(*gs.currentQuestionID)
|
|
if len(currentAttempts) >= types.MaxAttemptsPerQuestion {
|
|
return nil, errors.ErrMaxAttemptsExceeded(*gs.currentQuestionID, types.MaxAttemptsPerQuestion)
|
|
}
|
|
|
|
// Anti-cheat: Check minimum time between question presentation and answer
|
|
if timeSinceQuestionPresented.Duration < types.MinTimeBeforeAnswer {
|
|
return nil, errors.ErrAttemptTooFast(types.MinTimeBeforeAnswer.String())
|
|
}
|
|
|
|
// Determine if answer is correct
|
|
isCorrect := similarity >= types.FuzzyMatchThreshold
|
|
|
|
// Calculate points
|
|
pointsAwarded := sessionValueObjects.CalculatePointsForAttempt(isCorrect, gs.hintUsedForCurrentQ)
|
|
|
|
// Create attempt result
|
|
var attemptResult *sessionValueObjects.AttemptResult
|
|
if isCorrect {
|
|
attemptResult = sessionValueObjects.NewCorrectAttemptResult(
|
|
pointsAwarded,
|
|
gs.hintUsedForCurrentQ,
|
|
timeSinceQuestionPresented,
|
|
playerAnswer,
|
|
correctAnswer,
|
|
similarity,
|
|
isFuzzyMatch,
|
|
)
|
|
} else {
|
|
attemptResult = sessionValueObjects.NewIncorrectAttemptResult(
|
|
gs.hintUsedForCurrentQ,
|
|
timeSinceQuestionPresented,
|
|
playerAnswer,
|
|
correctAnswer,
|
|
similarity,
|
|
)
|
|
}
|
|
|
|
// Create attempt
|
|
attemptNumber := len(currentAttempts) + 1
|
|
attempt, err := sessionValueObjects.NewAttempt(
|
|
*gs.currentQuestionID,
|
|
attemptNumber,
|
|
playerAnswer,
|
|
attemptResult,
|
|
gs.hintUsedForCurrentQ,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Store the attempt
|
|
gs.attempts[*gs.currentQuestionID] = append(currentAttempts, attempt)
|
|
|
|
// Update score if correct
|
|
if isCorrect {
|
|
newScore, err := gs.score.Add(pointsAwarded)
|
|
if err != nil {
|
|
return nil, errors.ErrScoringFailed(fmt.Sprintf("failed to update score: %v", err))
|
|
}
|
|
gs.score = newScore
|
|
gs.questionsCorrect++
|
|
}
|
|
|
|
// Update activity
|
|
now := types.NewTimestamp()
|
|
gs.lastActivityAt = now
|
|
gs.updatedAt = now
|
|
|
|
return attempt, nil
|
|
}
|
|
|
|
// HandleTimeout handles a timeout for the current question
|
|
func (gs *GameSession) HandleTimeout() (*sessionValueObjects.Attempt, error) {
|
|
if !gs.IsActive() {
|
|
return nil, errors.ErrSessionNotActive(gs.id, gs.status)
|
|
}
|
|
|
|
if gs.currentQuestionID == nil {
|
|
return nil, errors.ErrOperationNotAllowed("handle timeout", "no current question")
|
|
}
|
|
|
|
// Create timeout attempt result
|
|
attemptResult := sessionValueObjects.NewTimeoutAttemptResult("") // Don't reveal correct answer on timeout
|
|
|
|
// Create timeout attempt
|
|
currentAttempts := gs.getAttemptsForQuestion(*gs.currentQuestionID)
|
|
attemptNumber := len(currentAttempts) + 1
|
|
|
|
attempt, err := sessionValueObjects.NewAttempt(
|
|
*gs.currentQuestionID,
|
|
attemptNumber,
|
|
"",
|
|
attemptResult,
|
|
gs.hintUsedForCurrentQ,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Store the attempt
|
|
gs.attempts[*gs.currentQuestionID] = append(currentAttempts, attempt)
|
|
|
|
// Update activity
|
|
now := types.NewTimestamp()
|
|
gs.lastActivityAt = now
|
|
gs.updatedAt = now
|
|
|
|
return attempt, nil
|
|
}
|
|
|
|
// End ends the game session
|
|
func (gs *GameSession) End() error {
|
|
if !gs.status.CanTransitionTo(types.SessionStatusCompleted) {
|
|
return errors.ErrInvalidSessionTransition(gs.status, types.SessionStatusCompleted)
|
|
}
|
|
|
|
// End the timer
|
|
if gs.timer != nil {
|
|
if err := gs.timer.End(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Update status and timestamps
|
|
gs.status = types.SessionStatusCompleted
|
|
now := types.NewTimestamp()
|
|
gs.lastActivityAt = now
|
|
gs.updatedAt = now
|
|
|
|
return nil
|
|
}
|
|
|
|
// Timeout marks the session as timed out
|
|
func (gs *GameSession) Timeout() error {
|
|
if !gs.status.CanTransitionTo(types.SessionStatusTimedOut) {
|
|
return errors.ErrInvalidSessionTransition(gs.status, types.SessionStatusTimedOut)
|
|
}
|
|
|
|
// End the timer
|
|
if gs.timer != nil {
|
|
if err := gs.timer.End(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Update status and timestamps
|
|
gs.status = types.SessionStatusTimedOut
|
|
now := types.NewTimestamp()
|
|
gs.lastActivityAt = now
|
|
gs.updatedAt = now
|
|
|
|
return nil
|
|
}
|
|
|
|
// Abandon marks the session as abandoned
|
|
func (gs *GameSession) Abandon() error {
|
|
if !gs.status.CanTransitionTo(types.SessionStatusAbandoned) {
|
|
return errors.ErrInvalidSessionTransition(gs.status, types.SessionStatusAbandoned)
|
|
}
|
|
|
|
// End the timer
|
|
if gs.timer != nil {
|
|
if err := gs.timer.End(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Update status and timestamps
|
|
gs.status = types.SessionStatusAbandoned
|
|
now := types.NewTimestamp()
|
|
gs.lastActivityAt = now
|
|
gs.updatedAt = now
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsActive returns true if the session is currently active
|
|
func (gs *GameSession) IsActive() bool {
|
|
return gs.status == types.SessionStatusActive
|
|
}
|
|
|
|
// IsCompleted returns true if the session is completed
|
|
func (gs *GameSession) IsCompleted() bool {
|
|
return gs.status == types.SessionStatusCompleted
|
|
}
|
|
|
|
// IsTimedOut returns true if the session timed out
|
|
func (gs *GameSession) IsTimedOut() bool {
|
|
return gs.status == types.SessionStatusTimedOut
|
|
}
|
|
|
|
// IsAbandoned returns true if the session was abandoned
|
|
func (gs *GameSession) IsAbandoned() bool {
|
|
return gs.status == types.SessionStatusAbandoned
|
|
}
|
|
|
|
// IsFinished returns true if the session is in a terminal state
|
|
func (gs *GameSession) IsFinished() bool {
|
|
return gs.IsCompleted() || gs.IsTimedOut() || gs.IsAbandoned()
|
|
}
|
|
|
|
// GetRemainingAttemptsForCurrentQuestion returns remaining attempts for current question
|
|
func (gs *GameSession) GetRemainingAttemptsForCurrentQuestion() int {
|
|
if gs.currentQuestionID == nil {
|
|
return 0
|
|
}
|
|
|
|
currentAttempts := gs.getAttemptsForQuestion(*gs.currentQuestionID)
|
|
return types.MaxAttemptsPerQuestion - len(currentAttempts)
|
|
}
|
|
|
|
// GetAttemptsForQuestion returns all attempts for a specific question
|
|
func (gs *GameSession) GetAttemptsForQuestion(questionID types.QuestionID) []*sessionValueObjects.Attempt {
|
|
return gs.getAttemptsForQuestion(questionID)
|
|
}
|
|
|
|
// GetAllAttempts returns all attempts made in this session
|
|
func (gs *GameSession) GetAllAttempts() map[types.QuestionID][]*sessionValueObjects.Attempt {
|
|
return gs.attempts
|
|
}
|
|
|
|
// GetTotalQuestionsAsked returns the total number of questions asked
|
|
func (gs *GameSession) GetTotalQuestionsAsked() int {
|
|
return len(gs.questionsAsked)
|
|
}
|
|
|
|
// GetSuccessRate returns the success rate as a percentage
|
|
func (gs *GameSession) GetSuccessRate() float64 {
|
|
totalQuestions := gs.GetTotalQuestionsAsked()
|
|
if totalQuestions == 0 {
|
|
return 0.0
|
|
}
|
|
return (float64(gs.questionsCorrect) / float64(totalQuestions)) * 100.0
|
|
}
|
|
|
|
// GetSessionDuration returns the total session duration
|
|
func (gs *GameSession) GetSessionDuration() types.Duration {
|
|
if gs.timer == nil {
|
|
return types.NewDuration(0)
|
|
}
|
|
return gs.timer.GetSessionDuration()
|
|
}
|
|
|
|
// CanRequestMoreQuestions returns true if the session can handle more questions
|
|
func (gs *GameSession) CanRequestMoreQuestions() bool {
|
|
return gs.IsActive() && !gs.IsExpired()
|
|
}
|
|
|
|
// IsExpired returns true if the session has expired due to time limit
|
|
func (gs *GameSession) IsExpired() bool {
|
|
if gs.timer == nil {
|
|
return false
|
|
}
|
|
return gs.timer.IsExpired()
|
|
}
|
|
|
|
// CheckForTimeout checks if session should be timed out and handles it
|
|
func (gs *GameSession) CheckForTimeout() error {
|
|
if gs.IsFinished() {
|
|
return nil // Already finished
|
|
}
|
|
|
|
if gs.IsExpired() {
|
|
return gs.Timeout()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// wasQuestionAsked checks if a question was already asked in this session
|
|
func (gs *GameSession) wasQuestionAsked(questionID types.QuestionID) bool {
|
|
for _, askedID := range gs.questionsAsked {
|
|
if askedID == questionID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// getAttemptsForQuestion returns attempts for a specific question
|
|
func (gs *GameSession) getAttemptsForQuestion(questionID types.QuestionID) []*sessionValueObjects.Attempt {
|
|
if attempts, exists := gs.attempts[questionID]; exists {
|
|
return attempts
|
|
}
|
|
return make([]*sessionValueObjects.Attempt, 0)
|
|
}
|
|
|
|
// Validate performs comprehensive validation on the game session
|
|
func (gs *GameSession) Validate() *types.ValidationResult {
|
|
result := types.NewValidationResult()
|
|
|
|
// Validate IDs
|
|
if gs.id.IsEmpty() {
|
|
result.AddError("session ID cannot be empty")
|
|
}
|
|
|
|
if gs.userID.IsEmpty() {
|
|
result.AddError("user ID cannot be empty")
|
|
}
|
|
|
|
// Validate player name
|
|
if gs.playerName == nil {
|
|
result.AddError("player name cannot be nil")
|
|
} else if gs.playerName.IsEmpty() {
|
|
result.AddError("player name cannot be empty")
|
|
}
|
|
|
|
// Validate score
|
|
if gs.score == nil {
|
|
result.AddError("score cannot be nil")
|
|
}
|
|
|
|
// Validate status
|
|
if !types.IsValidSessionStatus(gs.status) {
|
|
result.AddError("invalid session status")
|
|
}
|
|
|
|
// Validate counts
|
|
if gs.questionsCorrect < 0 {
|
|
result.AddError("questions correct cannot be negative")
|
|
}
|
|
|
|
if gs.hintsUsed < 0 {
|
|
result.AddError("hints used cannot be negative")
|
|
}
|
|
|
|
if gs.questionsCorrect > len(gs.questionsAsked) {
|
|
result.AddError("questions correct cannot exceed questions asked")
|
|
}
|
|
|
|
// Validate timestamps
|
|
if gs.createdAt.IsZero() {
|
|
result.AddError("created timestamp cannot be zero")
|
|
}
|
|
|
|
if gs.updatedAt.IsZero() {
|
|
result.AddError("updated timestamp cannot be zero")
|
|
}
|
|
|
|
if gs.createdAt.After(gs.updatedAt) {
|
|
result.AddError("created timestamp cannot be after updated timestamp")
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GameSessionSnapshot represents a read-only view of the game session
|
|
type GameSessionSnapshot struct {
|
|
ID types.GameSessionID `json:"id"`
|
|
UserID types.UserID `json:"user_id"`
|
|
PlayerName string `json:"player_name"`
|
|
Status types.SessionStatus `json:"status"`
|
|
Score int `json:"score"`
|
|
CurrentQuestionID *types.QuestionID `json:"current_question_id,omitempty"`
|
|
QuestionsAsked []types.QuestionID `json:"questions_asked"`
|
|
QuestionsCorrect int `json:"questions_correct"`
|
|
HintsUsed int `json:"hints_used"`
|
|
SuccessRate float64 `json:"success_rate"`
|
|
SessionDuration string `json:"session_duration"`
|
|
TimerStatus *sessionValueObjects.TimerStatus `json:"timer_status,omitempty"`
|
|
CreatedAt types.Timestamp `json:"created_at"`
|
|
UpdatedAt types.Timestamp `json:"updated_at"`
|
|
LastActivityAt types.Timestamp `json:"last_activity_at"`
|
|
}
|
|
|
|
// ToSnapshot creates a snapshot of the current session state
|
|
func (gs *GameSession) ToSnapshot() *GameSessionSnapshot {
|
|
var timerStatus *sessionValueObjects.TimerStatus
|
|
if gs.timer != nil {
|
|
timerStatus = gs.timer.GetStatus()
|
|
}
|
|
|
|
return &GameSessionSnapshot{
|
|
ID: gs.id,
|
|
UserID: gs.userID,
|
|
PlayerName: gs.playerName.Value(),
|
|
Status: gs.status,
|
|
Score: gs.score.Value(),
|
|
CurrentQuestionID: gs.currentQuestionID,
|
|
QuestionsAsked: gs.questionsAsked,
|
|
QuestionsCorrect: gs.questionsCorrect,
|
|
HintsUsed: gs.hintsUsed,
|
|
SuccessRate: gs.GetSuccessRate(),
|
|
SessionDuration: gs.GetSessionDuration().String(),
|
|
TimerStatus: timerStatus,
|
|
CreatedAt: gs.createdAt,
|
|
UpdatedAt: gs.updatedAt,
|
|
LastActivityAt: gs.lastActivityAt,
|
|
}
|
|
}
|
|
|
|
// Clone creates a deep copy of the game session (for testing or other purposes)
|
|
func (gs *GameSession) Clone() *GameSession {
|
|
// Deep copy attempts map
|
|
attemptsCopy := make(map[types.QuestionID][]*sessionValueObjects.Attempt)
|
|
for questionID, attempts := range gs.attempts {
|
|
attemptsCopy[questionID] = make([]*sessionValueObjects.Attempt, len(attempts))
|
|
for i, attempt := range attempts {
|
|
attemptsCopy[questionID][i] = attempt.Clone()
|
|
}
|
|
}
|
|
|
|
// Deep copy questions asked slice
|
|
questionsAskedCopy := make([]types.QuestionID, len(gs.questionsAsked))
|
|
copy(questionsAskedCopy, gs.questionsAsked)
|
|
|
|
return &GameSession{
|
|
id: gs.id,
|
|
userID: gs.userID,
|
|
playerName: gs.playerName, // PlayerName is immutable
|
|
status: gs.status,
|
|
score: gs.score, // Score is immutable (value object)
|
|
timer: gs.timer, // SessionTimer manages its own state
|
|
currentQuestionID: gs.currentQuestionID,
|
|
questionsAsked: questionsAskedCopy,
|
|
attempts: attemptsCopy,
|
|
questionsCorrect: gs.questionsCorrect,
|
|
hintsUsed: gs.hintsUsed,
|
|
hintUsedForCurrentQ: gs.hintUsedForCurrentQ,
|
|
lastActivityAt: gs.lastActivityAt,
|
|
createdAt: gs.createdAt,
|
|
updatedAt: gs.updatedAt,
|
|
}
|
|
} |