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.

285 lines
7.3 KiB
Go

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(),
}
}