package processor import ( "fmt" "time" "apskel-pos-be/internal/entities" ) // ValidateResult checks a result against a game's result_rules (docs/rfc-enakgame.md // ยง7.2 step 5) and returns why it is not believable, or nil when it is. A rule left // out does not apply, so a game without result_rules accepts every result. // // A failed result is not an error: the session completes, earns nothing and is // flagged, with these reasons in its reward_breakdown. func ValidateResult(rules entities.GameResultRules, result SessionResult, startedAt, submittedAt time.Time) []string { var reasons []string elapsed := submittedAt.Sub(startedAt).Seconds() if rules.MinDurationSeconds != nil && elapsed < float64(*rules.MinDurationSeconds) { reasons = append(reasons, fmt.Sprintf("played %.0f seconds, under the minimum of %d", elapsed, *rules.MinDurationSeconds)) } if result.Score != nil { score := *result.Score if score < 0 { reasons = append(reasons, "score cannot be negative") } if rules.MaxScore != nil && score > *rules.MaxScore { reasons = append(reasons, fmt.Sprintf("score %d is above the maximum of %d", score, *rules.MaxScore)) } if rules.MaxScorePerSecond != nil && score > 0 { // A score with no time played at all is never believable. if elapsed <= 0 || float64(score)/elapsed > *rules.MaxScorePerSecond { reasons = append(reasons, fmt.Sprintf("score %d in %.0f seconds is above %g per second", score, elapsed, *rules.MaxScorePerSecond)) } } } if result.Outcome != nil && len(rules.Outcomes) > 0 && !containsString(rules.Outcomes, *result.Outcome) { reasons = append(reasons, fmt.Sprintf("unknown outcome %q", *result.Outcome)) } return reasons }