Reduced cyclomatic complexity of game session service

master
oabrivard 1 month ago
parent 294f0acc60
commit 3cc74867a7

@ -145,13 +145,9 @@ func (s *Service) StartSession(ctx context.Context, in StartSessionInput) (*Star
// SubmitAnswer validates answer and transitions question/session state. // SubmitAnswer validates answer and transitions question/session state.
func (s *Service) SubmitAnswer(ctx context.Context, in SubmitAnswerInput) (*SubmitAnswerResult, error) { func (s *Service) SubmitAnswer(ctx context.Context, in SubmitAnswerInput) (*SubmitAnswerResult, error) {
sessionID := strings.TrimSpace(in.SessionID) sessionID, answer, err := s.normalizeSubmitAnswerInput(in)
if sessionID == "" { if err != nil {
return nil, domain.ErrInvalidState return nil, err
}
answer := sharedsecurity.SanitizeAnswer(in.Answer)
if answer == "" {
return nil, sharederrors.New(sharederrors.CodeInvalidAnswer, "answer is required")
} }
if !s.state.AcquireLock(ctx, sessionID, s.cfg.LockTTL) { if !s.state.AcquireLock(ctx, sessionID, s.cfg.LockTTL) {
@ -159,6 +155,60 @@ func (s *Service) SubmitAnswer(ctx context.Context, in SubmitAnswerInput) (*Subm
} }
defer s.state.ReleaseLock(ctx, sessionID) defer s.state.ReleaseLock(ctx, sessionID)
session, err := s.loadAnswerableSession(ctx, sessionID)
if err != nil {
return nil, err
}
latencyMs, err := s.enforceAnswerLatency(ctx, session)
if err != nil {
return nil, err
}
validation, err := s.qb.ValidateAnswer(ctx, session.CurrentQuestionID, answer)
if err != nil {
return nil, err
}
attemptNum, score, err := s.recordAttempt(ctx, session, answer, validation.Matched, latencyMs)
if err != nil {
return nil, err
}
s.applyAttemptOutcome(session, attemptNum, score, validation.Matched)
result := &SubmitAnswerResult{Correct: validation.Matched, AwardedScore: score}
if err := s.progressAfterAnswer(ctx, session, attemptNum, validation.Matched, result); err != nil {
return nil, err
}
updated, err := s.repo.UpdateSession(ctx, session)
if err != nil {
return nil, err
}
result.AttemptsRemaining = updated.Remaining(s.cfg.MaxAttempts)
result.Session = s.toSummary(updated)
return result, nil
}
func (s *Service) normalizeSubmitAnswerInput(
in SubmitAnswerInput,
) (sessionID string, answer string, err error) {
sessionID = strings.TrimSpace(in.SessionID)
if sessionID == "" {
return "", "", domain.ErrInvalidState
}
answer = sharedsecurity.SanitizeAnswer(in.Answer)
if answer == "" {
return "", "", sharederrors.New(sharederrors.CodeInvalidAnswer, "answer is required")
}
return sessionID, answer, nil
}
func (s *Service) loadAnswerableSession(
ctx context.Context,
sessionID string,
) (*domain.GameSession, error) {
session, err := s.repo.GetSessionByID(ctx, sessionID) session, err := s.repo.GetSessionByID(ctx, sessionID)
if err != nil { if err != nil {
return nil, err return nil, err
@ -169,7 +219,13 @@ func (s *Service) SubmitAnswer(ctx context.Context, in SubmitAnswerInput) (*Subm
if strings.TrimSpace(session.CurrentQuestionID) == "" || session.QuestionStartedAt == nil { if strings.TrimSpace(session.CurrentQuestionID) == "" || session.QuestionStartedAt == nil {
return nil, domain.ErrInvalidState return nil, domain.ErrInvalidState
} }
return session, nil
}
func (s *Service) enforceAnswerLatency(
ctx context.Context,
session *domain.GameSession,
) (int, error) {
latencyMs := int(time.Since(*session.QuestionStartedAt).Milliseconds()) latencyMs := int(time.Since(*session.QuestionStartedAt).Milliseconds())
if latencyMs < s.cfg.MinAnswerLatencyMs { if latencyMs < s.cfg.MinAnswerLatencyMs {
_ = s.repo.CreateEvent(ctx, &domain.SessionEvent{ _ = s.repo.CreateEvent(ctx, &domain.SessionEvent{
@ -177,40 +233,61 @@ func (s *Service) SubmitAnswer(ctx context.Context, in SubmitAnswerInput) (*Subm
EventType: "rapid_answer", EventType: "rapid_answer",
Metadata: fmt.Sprintf(`{"latency_ms":%d}`, latencyMs), Metadata: fmt.Sprintf(`{"latency_ms":%d}`, latencyMs),
}) })
return nil, domain.ErrRapidAnswer return 0, domain.ErrRapidAnswer
} }
return latencyMs, nil
validation, err := s.qb.ValidateAnswer(ctx, session.CurrentQuestionID, answer)
if err != nil {
return nil, err
} }
attemptNum := session.CurrentAttempts + 1 func (s *Service) recordAttempt(
score := valueobjects.CalculateQuestionScore(validation.Matched, session.CurrentHintUsed) ctx context.Context,
if err := s.repo.CreateAttempt(ctx, &domain.SessionAttempt{ session *domain.GameSession,
answer string,
matched bool,
latencyMs int,
) (attemptNum int, score int, err error) {
attemptNum = session.CurrentAttempts + 1
score = valueobjects.CalculateQuestionScore(matched, session.CurrentHintUsed)
err = s.repo.CreateAttempt(ctx, &domain.SessionAttempt{
SessionID: session.ID, SessionID: session.ID,
QuestionID: session.CurrentQuestionID, QuestionID: session.CurrentQuestionID,
AttemptNumber: attemptNum, AttemptNumber: attemptNum,
ProvidedAnswer: answer, ProvidedAnswer: answer,
IsCorrect: validation.Matched, IsCorrect: matched,
UsedHint: session.CurrentHintUsed, UsedHint: session.CurrentHintUsed,
AwardedScore: score, AwardedScore: score,
LatencyMs: latencyMs, LatencyMs: latencyMs,
}); err != nil { })
return nil, err if err != nil {
return 0, 0, err
}
return attemptNum, score, nil
} }
func (s *Service) applyAttemptOutcome(
session *domain.GameSession,
attemptNum, score int,
matched bool,
) {
session.CurrentAttempts = attemptNum session.CurrentAttempts = attemptNum
session.TotalScore += score session.TotalScore += score
if validation.Matched { if matched {
session.QuestionsCorrect++ session.QuestionsCorrect++
} }
}
result := &SubmitAnswerResult{Correct: validation.Matched, AwardedScore: score} func (s *Service) progressAfterAnswer(
if validation.Matched || attemptNum >= s.cfg.MaxAttempts { ctx context.Context,
session *domain.GameSession,
attemptNum int,
matched bool,
result *SubmitAnswerResult,
) error {
if !matched && attemptNum < s.cfg.MaxAttempts {
return nil
}
next, completed, err := s.advanceQuestion(ctx, session) next, completed, err := s.advanceQuestion(ctx, session)
if err != nil { if err != nil {
return nil, err return err
} }
if completed { if completed {
now := time.Now().UTC() now := time.Now().UTC()
@ -219,7 +296,8 @@ func (s *Service) SubmitAnswer(ctx context.Context, in SubmitAnswerInput) (*Subm
_ = s.state.ClearActiveSession(ctx, session.PlayerID) _ = s.state.ClearActiveSession(ctx, session.PlayerID)
_ = s.state.ClearTimer(ctx, session.ID) _ = s.state.ClearTimer(ctx, session.ID)
result.SessionCompleted = true result.SessionCompleted = true
} else { return nil
}
now := time.Now().UTC() now := time.Now().UTC()
session.CurrentQuestionID = next.ID session.CurrentQuestionID = next.ID
session.CurrentAttempts = 0 session.CurrentAttempts = 0
@ -227,16 +305,7 @@ func (s *Service) SubmitAnswer(ctx context.Context, in SubmitAnswerInput) (*Subm
session.QuestionStartedAt = &now session.QuestionStartedAt = &now
session.QuestionsAsked++ session.QuestionsAsked++
result.NextQuestion = next result.NextQuestion = next
} return nil
}
updated, err := s.repo.UpdateSession(ctx, session)
if err != nil {
return nil, err
}
result.AttemptsRemaining = updated.Remaining(s.cfg.MaxAttempts)
result.Session = s.toSummary(updated)
return result, nil
} }
// RequestHint marks hint usage and returns hint content. // RequestHint marks hint usage and returns hint content.

Loading…
Cancel
Save