premier app version beta
This commit is contained in:
220
lib/services/score_calculator_service.dart
Normal file
220
lib/services/score_calculator_service.dart
Normal file
@@ -0,0 +1,220 @@
|
||||
import 'dart:math' as math;
|
||||
import '../data/models/shot.dart';
|
||||
import '../data/models/target_type.dart';
|
||||
import '../core/constants/app_constants.dart';
|
||||
|
||||
class ScoreResult {
|
||||
final int totalScore;
|
||||
final int maxPossibleScore;
|
||||
final double percentage;
|
||||
final Map<int, int> scoreDistribution; // score -> count
|
||||
final int shotCount;
|
||||
|
||||
ScoreResult({
|
||||
required this.totalScore,
|
||||
required this.maxPossibleScore,
|
||||
required this.percentage,
|
||||
required this.scoreDistribution,
|
||||
required this.shotCount,
|
||||
});
|
||||
}
|
||||
|
||||
class ScoreCalculatorService {
|
||||
/// Calculate score for a single shot on a concentric target
|
||||
/// ringCount determines the number of scoring zones (default 10)
|
||||
/// Center is always 10 (bullseye), each ring decrements by 1
|
||||
/// Example: 5 rings = 10, 9, 8, 7, 6
|
||||
/// imageAspectRatio is width/height to account for non-square images
|
||||
int calculateConcentricScore({
|
||||
required double shotX,
|
||||
required double shotY,
|
||||
required double targetCenterX,
|
||||
required double targetCenterY,
|
||||
required double targetRadius,
|
||||
int ringCount = 10,
|
||||
double imageAspectRatio = 1.0,
|
||||
List<double>? ringRadii, // Optional individual ring radii multipliers
|
||||
}) {
|
||||
final dx = shotX - targetCenterX;
|
||||
final dy = shotY - targetCenterY;
|
||||
|
||||
// Account for aspect ratio to match visual representation
|
||||
// The visual uses min(width, height) for the radius
|
||||
double normalizedDistance;
|
||||
if (imageAspectRatio >= 1.0) {
|
||||
// Landscape or square: scale x by aspectRatio
|
||||
normalizedDistance = math.sqrt(dx * dx * imageAspectRatio * imageAspectRatio + dy * dy) / targetRadius;
|
||||
} else {
|
||||
// Portrait: scale y by 1/aspectRatio
|
||||
normalizedDistance = math.sqrt(dx * dx + dy * dy / (imageAspectRatio * imageAspectRatio)) / targetRadius;
|
||||
}
|
||||
|
||||
// Use custom ring radii if provided, otherwise use equal spacing
|
||||
if (ringRadii != null && ringRadii.length == ringCount) {
|
||||
for (int i = 0; i < ringCount; i++) {
|
||||
if (normalizedDistance <= ringRadii[i]) {
|
||||
// Center = 10, decrement by 1 for each ring
|
||||
return 10 - i;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Generate dynamic zone radii based on ring count
|
||||
// Each zone has equal width
|
||||
for (int i = 0; i < ringCount; i++) {
|
||||
final zoneRadius = (i + 1) / ringCount;
|
||||
if (normalizedDistance <= zoneRadius) {
|
||||
// Center = 10, decrement by 1 for each ring
|
||||
return 10 - i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // Outside target
|
||||
}
|
||||
|
||||
/// Calculate score for a single shot on a silhouette target
|
||||
int calculateSilhouetteScore({
|
||||
required double shotX,
|
||||
required double shotY,
|
||||
required double targetCenterX,
|
||||
required double targetCenterY,
|
||||
required double targetWidth,
|
||||
required double targetHeight,
|
||||
}) {
|
||||
// Check if shot is within silhouette bounds
|
||||
final relativeX = (shotX - targetCenterX).abs() / (targetWidth / 2);
|
||||
final relativeY = (shotY - (targetCenterY - targetHeight / 2)) / targetHeight;
|
||||
|
||||
// Outside horizontal bounds
|
||||
if (relativeX > 1.0) return 0;
|
||||
|
||||
// Check vertical zones (from top of silhouette)
|
||||
if (relativeY < 0) return 0; // Above silhouette
|
||||
|
||||
if (relativeY <= AppConstants.silhouetteZones['head']!) {
|
||||
// Head zone - 5 points, but narrower
|
||||
if (relativeX <= 0.4) return 5;
|
||||
} else if (relativeY <= AppConstants.silhouetteZones['center']!) {
|
||||
// Center mass - 5 points
|
||||
if (relativeX <= 0.6) return 5;
|
||||
} else if (relativeY <= AppConstants.silhouetteZones['body']!) {
|
||||
// Body - 4 points
|
||||
if (relativeX <= 0.7) return 4;
|
||||
} else if (relativeY <= AppConstants.silhouetteZones['lower']!) {
|
||||
// Lower body - 3 points
|
||||
if (relativeX <= 0.5) return 3;
|
||||
}
|
||||
|
||||
return 0; // Outside target
|
||||
}
|
||||
|
||||
/// Calculate scores for all shots
|
||||
ScoreResult calculateScores({
|
||||
required List<Shot> shots,
|
||||
required TargetType targetType,
|
||||
required double targetCenterX,
|
||||
required double targetCenterY,
|
||||
double? targetRadius, // For concentric
|
||||
double? targetWidth, // For silhouette
|
||||
double? targetHeight, // For silhouette
|
||||
int ringCount = 10, // For concentric
|
||||
double imageAspectRatio = 1.0, // For concentric
|
||||
List<double>? ringRadii, // For concentric with individual ring radii
|
||||
}) {
|
||||
final scoreDistribution = <int, int>{};
|
||||
int totalScore = 0;
|
||||
|
||||
for (final shot in shots) {
|
||||
int score;
|
||||
|
||||
if (targetType == TargetType.concentric) {
|
||||
score = calculateConcentricScore(
|
||||
shotX: shot.x,
|
||||
shotY: shot.y,
|
||||
targetCenterX: targetCenterX,
|
||||
targetCenterY: targetCenterY,
|
||||
targetRadius: targetRadius ?? 0.4,
|
||||
ringCount: ringCount,
|
||||
imageAspectRatio: imageAspectRatio,
|
||||
ringRadii: ringRadii,
|
||||
);
|
||||
} else {
|
||||
score = calculateSilhouetteScore(
|
||||
shotX: shot.x,
|
||||
shotY: shot.y,
|
||||
targetCenterX: targetCenterX,
|
||||
targetCenterY: targetCenterY,
|
||||
targetWidth: targetWidth ?? 0.3,
|
||||
targetHeight: targetHeight ?? 0.7,
|
||||
);
|
||||
}
|
||||
|
||||
totalScore += score;
|
||||
scoreDistribution[score] = (scoreDistribution[score] ?? 0) + 1;
|
||||
}
|
||||
|
||||
final maxScore = targetType == TargetType.concentric ? 10 : 5;
|
||||
final maxPossibleScore = shots.length * maxScore;
|
||||
final percentage = maxPossibleScore > 0
|
||||
? (totalScore / maxPossibleScore) * 100
|
||||
: 0.0;
|
||||
|
||||
return ScoreResult(
|
||||
totalScore: totalScore,
|
||||
maxPossibleScore: maxPossibleScore,
|
||||
percentage: percentage,
|
||||
scoreDistribution: scoreDistribution,
|
||||
shotCount: shots.length,
|
||||
);
|
||||
}
|
||||
|
||||
/// Recalculate a shot's score
|
||||
Shot recalculateShot({
|
||||
required Shot shot,
|
||||
required TargetType targetType,
|
||||
required double targetCenterX,
|
||||
required double targetCenterY,
|
||||
double? targetRadius,
|
||||
double? targetWidth,
|
||||
double? targetHeight,
|
||||
int ringCount = 10,
|
||||
double imageAspectRatio = 1.0,
|
||||
List<double>? ringRadii,
|
||||
}) {
|
||||
int newScore;
|
||||
|
||||
if (targetType == TargetType.concentric) {
|
||||
newScore = calculateConcentricScore(
|
||||
shotX: shot.x,
|
||||
shotY: shot.y,
|
||||
targetCenterX: targetCenterX,
|
||||
targetCenterY: targetCenterY,
|
||||
targetRadius: targetRadius ?? 0.4,
|
||||
ringCount: ringCount,
|
||||
imageAspectRatio: imageAspectRatio,
|
||||
ringRadii: ringRadii,
|
||||
);
|
||||
} else {
|
||||
newScore = calculateSilhouetteScore(
|
||||
shotX: shot.x,
|
||||
shotY: shot.y,
|
||||
targetCenterX: targetCenterX,
|
||||
targetCenterY: targetCenterY,
|
||||
targetWidth: targetWidth ?? 0.3,
|
||||
targetHeight: targetHeight ?? 0.7,
|
||||
);
|
||||
}
|
||||
|
||||
return shot.copyWith(score: newScore);
|
||||
}
|
||||
|
||||
/// Get score color index (0-9 for zone colors)
|
||||
int getScoreColorIndex(int score, TargetType targetType) {
|
||||
if (targetType == TargetType.concentric) {
|
||||
return (10 - score).clamp(0, 9);
|
||||
} else {
|
||||
// Map silhouette scores (0-5) to color indices
|
||||
return ((5 - score) * 2).clamp(0, 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user