package valueobjects import ( "fmt" "knowfoolery/backend/shared/types" ) // AttemptResult represents the outcome of a single answer attempt type AttemptResult struct { result types.AttemptResult isCorrect bool pointsAwarded int hintWasUsed bool timeTaken types.Duration playerAnswer string correctAnswer string similarity float64 // For fuzzy matches isFuzzyMatch bool // True if matched via fuzzy logic } // NewAttemptResult creates a new AttemptResult func NewAttemptResult( isCorrect bool, pointsAwarded int, hintWasUsed bool, timeTaken types.Duration, playerAnswer string, correctAnswer string, similarity float64, isFuzzyMatch bool, ) *AttemptResult { var result types.AttemptResult if isCorrect { result = types.AttemptResultCorrect } else { result = types.AttemptResultIncorrect } return &AttemptResult{ result: result, isCorrect: isCorrect, pointsAwarded: pointsAwarded, hintWasUsed: hintWasUsed, timeTaken: timeTaken, playerAnswer: playerAnswer, correctAnswer: correctAnswer, similarity: similarity, isFuzzyMatch: isFuzzyMatch, } } // NewCorrectAttemptResult creates a result for a correct answer func NewCorrectAttemptResult( pointsAwarded int, hintWasUsed bool, timeTaken types.Duration, playerAnswer string, correctAnswer string, similarity float64, isFuzzyMatch bool, ) *AttemptResult { return NewAttemptResult( true, pointsAwarded, hintWasUsed, timeTaken, playerAnswer, correctAnswer, similarity, isFuzzyMatch, ) } // NewIncorrectAttemptResult creates a result for an incorrect answer func NewIncorrectAttemptResult( hintWasUsed bool, timeTaken types.Duration, playerAnswer string, correctAnswer string, similarity float64, ) *AttemptResult { return NewAttemptResult( false, types.PointsIncorrect, hintWasUsed, timeTaken, playerAnswer, correctAnswer, similarity, false, // Incorrect answers are never fuzzy matches ) } // NewTimeoutAttemptResult creates a result for a timeout func NewTimeoutAttemptResult(correctAnswer string) *AttemptResult { return &AttemptResult{ result: types.AttemptResultTimeout, isCorrect: false, pointsAwarded: types.PointsIncorrect, hintWasUsed: false, timeTaken: types.NewDuration(0), playerAnswer: "", correctAnswer: correctAnswer, similarity: 0.0, isFuzzyMatch: false, } } // Result returns the attempt result type func (ar *AttemptResult) Result() types.AttemptResult { return ar.result } // IsCorrect returns true if the attempt was correct func (ar *AttemptResult) IsCorrect() bool { return ar.isCorrect } // IsIncorrect returns true if the attempt was incorrect func (ar *AttemptResult) IsIncorrect() bool { return !ar.isCorrect && ar.result != types.AttemptResultTimeout } // IsTimeout returns true if the attempt timed out func (ar *AttemptResult) IsTimeout() bool { return ar.result == types.AttemptResultTimeout } // PointsAwarded returns the points awarded for this attempt func (ar *AttemptResult) PointsAwarded() int { return ar.pointsAwarded } // HintWasUsed returns true if a hint was used for this attempt func (ar *AttemptResult) HintWasUsed() bool { return ar.hintWasUsed } // TimeTaken returns the time taken for this attempt func (ar *AttemptResult) TimeTaken() types.Duration { return ar.timeTaken } // PlayerAnswer returns the answer provided by the player func (ar *AttemptResult) PlayerAnswer() string { return ar.playerAnswer } // CorrectAnswer returns the correct answer func (ar *AttemptResult) CorrectAnswer() string { return ar.correctAnswer } // Similarity returns the similarity score (0-1) between player and correct answer func (ar *AttemptResult) Similarity() float64 { return ar.similarity } // IsFuzzyMatch returns true if the answer was matched via fuzzy logic func (ar *AttemptResult) IsFuzzyMatch() bool { return ar.isFuzzyMatch } // IsExactMatch returns true if the answer was an exact match func (ar *AttemptResult) IsExactMatch() bool { return ar.isCorrect && !ar.isFuzzyMatch } // GetMatchType returns a string describing the match type func (ar *AttemptResult) GetMatchType() string { switch { case ar.IsTimeout(): return "timeout" case ar.IsIncorrect(): return "incorrect" case ar.IsExactMatch(): return "exact_match" case ar.IsFuzzyMatch(): return "fuzzy_match" default: return "unknown" } } // GetResultDescription returns a human-readable description of the result func (ar *AttemptResult) GetResultDescription() string { switch { case ar.IsTimeout(): return "Time ran out" case ar.IsIncorrect(): return "Incorrect answer" case ar.IsExactMatch(): if ar.hintWasUsed { return "Correct answer with hint (exact match)" } return "Correct answer (exact match)" case ar.IsFuzzyMatch(): if ar.hintWasUsed { return "Correct answer with hint (close match)" } return "Correct answer (close match)" default: return "Unknown result" } } // GetScoreExplanation returns an explanation of the scoring for this attempt func (ar *AttemptResult) GetScoreExplanation() string { switch { case ar.IsTimeout(): return "No points awarded - time expired" case ar.IsIncorrect(): return "No points awarded - incorrect answer" case ar.hintWasUsed: return "1 point awarded - correct answer with hint" default: return "2 points awarded - correct answer without hint" } } // Equals checks if two AttemptResults are equal func (ar *AttemptResult) Equals(other *AttemptResult) bool { if other == nil { return false } return ar.result == other.result && ar.isCorrect == other.isCorrect && ar.pointsAwarded == other.pointsAwarded && ar.hintWasUsed == other.hintWasUsed && ar.playerAnswer == other.playerAnswer && ar.correctAnswer == other.correctAnswer && ar.similarity == other.similarity && ar.isFuzzyMatch == other.isFuzzyMatch } // IsWorthPoints returns true if this attempt result awards points func (ar *AttemptResult) IsWorthPoints() bool { return ar.pointsAwarded > 0 } // GetTimeTakenFormatted returns the time taken formatted for display func (ar *AttemptResult) GetTimeTakenFormatted() string { if ar.IsTimeout() { return "timeout" } seconds := ar.timeTaken.Seconds() if seconds < 1 { return "< 1s" } return fmt.Sprintf("%.1fs", seconds) } // AttempResultSummary provides a summary of the attempt result type AttemptResultSummary struct { MatchType string `json:"match_type"` IsCorrect bool `json:"is_correct"` PointsAwarded int `json:"points_awarded"` HintWasUsed bool `json:"hint_was_used"` TimeTaken string `json:"time_taken"` PlayerAnswer string `json:"player_answer"` CorrectAnswer string `json:"correct_answer"` Similarity float64 `json:"similarity"` ResultDescription string `json:"result_description"` ScoreExplanation string `json:"score_explanation"` } // ToSummary converts the attempt result to a summary structure func (ar *AttemptResult) ToSummary() *AttemptResultSummary { return &AttemptResultSummary{ MatchType: ar.GetMatchType(), IsCorrect: ar.IsCorrect(), PointsAwarded: ar.PointsAwarded(), HintWasUsed: ar.HintWasUsed(), TimeTaken: ar.GetTimeTakenFormatted(), PlayerAnswer: ar.PlayerAnswer(), CorrectAnswer: ar.CorrectAnswer(), Similarity: ar.Similarity(), ResultDescription: ar.GetResultDescription(), ScoreExplanation: ar.GetScoreExplanation(), } }