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.
func (s *Service) SubmitAnswer(ctx context.Context, in SubmitAnswerInput) (*SubmitAnswerResult, error) {
sessionID := strings.TrimSpace(in.SessionID)
if sessionID == "" {
return nil, domain.ErrInvalidState
}
answer := sharedsecurity.SanitizeAnswer(in.Answer)
if answer == "" {
return nil, sharederrors.New(sharederrors.CodeInvalidAnswer, "answer is required")
sessionID, answer, err := s.normalizeSubmitAnswerInput(in)
if err != nil {
return nil, err
}
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)
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)
if err != nil {
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 {
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())
if latencyMs < s.cfg.MinAnswerLatencyMs {
_ = s.repo.CreateEvent(ctx, &domain.SessionEvent{
@ -177,40 +233,61 @@ func (s *Service) SubmitAnswer(ctx context.Context, in SubmitAnswerInput) (*Subm
EventType: "rapid_answer",
Metadata: fmt.Sprintf(`{"latency_ms":%d}`, latencyMs),
})
return nil, domain.ErrRapidAnswer
return 0, domain.ErrRapidAnswer
}
validation, err := s.qb.ValidateAnswer(ctx, session.CurrentQuestionID, answer)
if err != nil {
return nil, err
return latencyMs, nil
}
attemptNum := session.CurrentAttempts + 1
score := valueobjects.CalculateQuestionScore(validation.Matched, session.CurrentHintUsed)
if err := s.repo.CreateAttempt(ctx, &domain.SessionAttempt{
func (s *Service) recordAttempt(
ctx context.Context,
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,
QuestionID: session.CurrentQuestionID,
AttemptNumber: attemptNum,
ProvidedAnswer: answer,
IsCorrect: validation.Matched,
IsCorrect: matched,
UsedHint: session.CurrentHintUsed,
AwardedScore: score,
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.TotalScore += score
if validation.Matched {
if matched {
session.QuestionsCorrect++
}
}
result := &SubmitAnswerResult{Correct: validation.Matched, AwardedScore: score}
if validation.Matched || attemptNum >= s.cfg.MaxAttempts {
func (s *Service) progressAfterAnswer(
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)
if err != nil {
return nil, err
return err
}
if completed {
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.ClearTimer(ctx, session.ID)
result.SessionCompleted = true
} else {
return nil
}
now := time.Now().UTC()
session.CurrentQuestionID = next.ID
session.CurrentAttempts = 0
@ -227,16 +305,7 @@ func (s *Service) SubmitAnswer(ctx context.Context, in SubmitAnswerInput) (*Subm
session.QuestionStartedAt = &now
session.QuestionsAsked++
result.NextQuestion = next
}
}
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
return nil
}
// RequestHint marks hint usage and returns hint content.

Loading…
Cancel
Save