import 'dart:math' as math; import 'user_trading_state_db.dart'; /// Runtime scoring mode for prospective / guess-the-move answers. /// /// Set from [ServerEnv.prospectiveAnswerClosenessEnabled] at server startup. class ProspectiveAnswerScoring { ProspectiveAnswerScoring._(); /// When false (default): correct sign `+1`, wrong sign `-1`. /// /// When true: correct sign `+1` plus up to `+1` closeness; wrong sign `-2`. static bool closenessExtraPointsEnabled = false; } /// Breakdown of one prospective / guess-the-move answer grade. class ProspectiveAnswerGrade { const ProspectiveAnswerGrade({ required this.directionPoint, required this.closenessPoint, required this.answerScore, required this.absError, required this.sameDirection, }); final num directionPoint; final num closenessPoint; final num answerScore; final num absError; final bool sameDirection; } /// Full credit when absolute error is within this many percentage points. const num prospectiveAnswerFullCreditTolerance = 1; /// Grades a slider answer against the expected percent move. ProspectiveAnswerGrade gradeProspectiveAnswer({ required num userResponse, required num correctAnswer, }) { final bool sameDirection = _sameDirection( userResponse: userResponse, correctAnswer: correctAnswer, ); final num absError = (userResponse - correctAnswer).abs(); if (!ProspectiveAnswerScoring.closenessExtraPointsEnabled) { final num directionPoint = sameDirection ? 1 : -1; return ProspectiveAnswerGrade( directionPoint: directionPoint, closenessPoint: 0, answerScore: directionPoint, absError: absError, sameDirection: sameDirection, ); } final num directionPoint = sameDirection ? 1 : -2; final num closenessPoint = sameDirection ? _closenessPoint( userResponse: userResponse, correctAnswer: correctAnswer, ) : 0; return ProspectiveAnswerGrade( directionPoint: directionPoint, closenessPoint: closenessPoint, answerScore: directionPoint + closenessPoint, absError: absError, sameDirection: sameDirection, ); } /// Adds [grade] to cumulative `guess_score` in [user_trading_state]. Future recordProspectiveGuessScore({ required UserTradingStateDb tradingStateDb, required String firebaseUid, required ProspectiveAnswerGrade grade, required String symbol, required DateTime at, }) async { await tradingStateDb.recordGuessScore( firebaseUid: firebaseUid, scoreDelta: grade.answerScore, symbol: symbol, at: at, directionCorrect: grade.sameDirection, ); } bool _sameDirection({ required num userResponse, required num correctAnswer, }) { return userResponse.compareTo(0) == correctAnswer.compareTo(0); } num _closenessPoint({ required num userResponse, required num correctAnswer, }) { final num absError = (userResponse - correctAnswer).abs(); if (absError <= prospectiveAnswerFullCreditTolerance) { return 1; } final num scale = math.max(1, correctAnswer.abs()); final num normalizedError = (absError - prospectiveAnswerFullCreditTolerance) / scale; return math.max(0, 1 - normalizedError); }