332 lines
12 KiB
Python
332 lines
12 KiB
Python
"""
|
|
Analysis service for game statistics and strategy optimization.
|
|
|
|
This service provides comprehensive analysis capabilities with
|
|
dynamic data handling and LLM-friendly output formatting.
|
|
"""
|
|
|
|
import logging
|
|
from dataclasses import dataclass, field
|
|
from typing import Optional
|
|
|
|
from ..api.client import GeoGuessrClient
|
|
from ..models.Game import Game
|
|
from ..monitoring.schema_manager import schema_registry
|
|
from .game_service import GameService
|
|
from .profile_service import ProfileService
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class GameAnalysis:
|
|
"""Analysis results for a set of games."""
|
|
|
|
games_analyzed: int = 0
|
|
total_score: int = 0
|
|
average_score: float = 0.0
|
|
total_rounds: int = 0
|
|
perfect_rounds: int = 0
|
|
perfect_round_percentage: float = 0.0
|
|
average_distance_meters: float = 0.0
|
|
average_time_seconds: float = 0.0
|
|
best_game_score: int = 0
|
|
worst_game_score: int = 0
|
|
score_trend: str = "stable" # improving, declining, stable
|
|
weak_areas: list = field(default_factory=list)
|
|
strong_areas: list = field(default_factory=list)
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Convert to dictionary."""
|
|
return {
|
|
"games_analyzed": self.games_analyzed,
|
|
"total_score": self.total_score,
|
|
"average_score": round(self.average_score, 2),
|
|
"total_rounds": self.total_rounds,
|
|
"perfect_rounds": self.perfect_rounds,
|
|
"perfect_round_percentage": round(self.perfect_round_percentage, 2),
|
|
"average_distance_meters": round(self.average_distance_meters, 2),
|
|
"average_time_seconds": round(self.average_time_seconds, 2),
|
|
"best_game_score": self.best_game_score,
|
|
"worst_game_score": self.worst_game_score,
|
|
"score_trend": self.score_trend,
|
|
"weak_areas": self.weak_areas,
|
|
"strong_areas": self.strong_areas,
|
|
}
|
|
|
|
|
|
class AnalysisService:
|
|
"""Service for game analysis and strategy optimization."""
|
|
|
|
def __init__(
|
|
self,
|
|
client: GeoGuessrClient,
|
|
game_service: Optional[GameService] = None,
|
|
profile_service: Optional[ProfileService] = None,
|
|
):
|
|
self.client = client
|
|
self.game_service = game_service or GameService(client)
|
|
self.profile_service = profile_service or ProfileService(client)
|
|
|
|
@staticmethod
|
|
def analyze_games(games: list[Game]) -> GameAnalysis:
|
|
"""
|
|
Analyze a list of games and calculate statistics.
|
|
|
|
Args:
|
|
games: List of Game objects to analyze
|
|
|
|
Returns:
|
|
GameAnalysis with computed statistics
|
|
"""
|
|
if not games:
|
|
return GameAnalysis()
|
|
|
|
total_score = sum(g.total_score for g in games)
|
|
all_rounds = [r for g in games for r in g.rounds]
|
|
total_rounds = len(all_rounds)
|
|
perfect_rounds = sum(1 for r in all_rounds if r.score == 5000)
|
|
|
|
# Calculate averages
|
|
avg_distance = (
|
|
sum(r.distance_meters for r in all_rounds) / total_rounds if total_rounds > 0 else 0
|
|
)
|
|
avg_time = sum(r.time_seconds for r in all_rounds) / total_rounds if total_rounds > 0 else 0
|
|
|
|
# Find best and worst
|
|
scores = [g.total_score for g in games]
|
|
best_score = max(scores) if scores else 0
|
|
worst_score = min(scores) if scores else 0
|
|
|
|
# Determine trend (simple moving average comparison)
|
|
trend = "stable"
|
|
if len(games) >= 4:
|
|
first_half = sum(g.total_score for g in games[: len(games) // 2]) / (len(games) // 2)
|
|
second_half = sum(g.total_score for g in games[len(games) // 2 :]) / (
|
|
len(games) - len(games) // 2
|
|
)
|
|
if second_half > first_half * 1.05:
|
|
trend = "improving"
|
|
elif second_half < first_half * 0.95:
|
|
trend = "declining"
|
|
|
|
# Identify weak/strong areas based on scores
|
|
weak_areas = []
|
|
strong_areas = []
|
|
|
|
for game in games:
|
|
for round_guess in game.rounds:
|
|
if round_guess.score < 2000:
|
|
weak_areas.append(
|
|
{
|
|
"game": game.token,
|
|
"round": round_guess.round_number,
|
|
"score": round_guess.score,
|
|
"distance": round_guess.distance_meters,
|
|
}
|
|
)
|
|
elif round_guess.score >= 4500:
|
|
strong_areas.append(
|
|
{
|
|
"game": game.token,
|
|
"round": round_guess.round_number,
|
|
"score": round_guess.score,
|
|
}
|
|
)
|
|
|
|
return GameAnalysis(
|
|
games_analyzed=len(games),
|
|
total_score=total_score,
|
|
average_score=total_score / len(games),
|
|
total_rounds=total_rounds,
|
|
perfect_rounds=perfect_rounds,
|
|
perfect_round_percentage=(
|
|
(perfect_rounds / total_rounds * 100) if total_rounds > 0 else 0
|
|
),
|
|
average_distance_meters=avg_distance,
|
|
average_time_seconds=avg_time,
|
|
best_game_score=best_score,
|
|
worst_game_score=worst_score,
|
|
score_trend=trend,
|
|
weak_areas=weak_areas[:10], # Limit to 10
|
|
strong_areas=strong_areas[:10],
|
|
)
|
|
|
|
async def analyze_recent_games(
|
|
self,
|
|
count: int = 10,
|
|
session_token: Optional[str] = None,
|
|
) -> dict:
|
|
"""
|
|
Analyze recent games and provide statistics summary.
|
|
|
|
Args:
|
|
count: Number of recent games to analyze
|
|
session_token: Optional session token
|
|
|
|
Returns:
|
|
Dictionary with analysis results and raw game data
|
|
"""
|
|
games = await self.game_service.get_recent_games(count, session_token)
|
|
analysis = self.analyze_games(games)
|
|
|
|
return {
|
|
"analysis": analysis.to_dict(),
|
|
"games": [g.to_dict() for g in games],
|
|
"schema_info": {
|
|
"endpoints_used": ["/v4/feed/private", "/v3/games/{token}"],
|
|
"available_schemas": schema_registry.get_available_endpoints(),
|
|
},
|
|
}
|
|
|
|
async def get_performance_summary(
|
|
self,
|
|
session_token: Optional[str] = None,
|
|
) -> dict:
|
|
"""
|
|
Get a comprehensive performance summary.
|
|
|
|
Combines profile stats, achievements, season info, and recent game analysis.
|
|
"""
|
|
results = {
|
|
"profile": None,
|
|
"stats": None,
|
|
"season": None,
|
|
"recent_games_analysis": None,
|
|
"explorer": None,
|
|
"objectives": None,
|
|
"api_status": schema_registry.get_schema_summary(),
|
|
"errors": [],
|
|
}
|
|
|
|
# Get comprehensive profile
|
|
try:
|
|
results["profile"] = await self.profile_service.get_comprehensive_profile(session_token)
|
|
except Exception as e:
|
|
results["errors"].append(f"Profile: {str(e)}")
|
|
|
|
# Get season stats
|
|
try:
|
|
stats, response = await self.game_service.get_season_stats(session_token)
|
|
results["season"] = {
|
|
"data": {
|
|
"rank": stats.rank,
|
|
"rating": stats.rating,
|
|
"games_played": stats.games_played,
|
|
"division": stats.division,
|
|
},
|
|
"raw_fields": response.available_fields,
|
|
}
|
|
except Exception as e:
|
|
results["errors"].append(f"Season: {str(e)}")
|
|
|
|
# Analyze recent games
|
|
try:
|
|
results["recent_games_analysis"] = await self.analyze_recent_games(5, session_token)
|
|
except Exception as e:
|
|
results["errors"].append(f"Recent games: {str(e)}")
|
|
|
|
# Get explorer progress
|
|
try:
|
|
response = await self.client.get(self._create_endpoint("/v3/explorer"), session_token)
|
|
if response.is_success:
|
|
results["explorer"] = response.summarize()
|
|
except Exception as e:
|
|
results["errors"].append(f"Explorer: {str(e)}")
|
|
|
|
# Get objectives
|
|
try:
|
|
response = await self.client.get(self._create_endpoint("/v4/objectives"), session_token)
|
|
if response.is_success:
|
|
results["objectives"] = response.summarize()
|
|
except Exception as e:
|
|
results["errors"].append(f"Objectives: {str(e)}")
|
|
|
|
return results
|
|
|
|
async def get_strategy_recommendations(
|
|
self,
|
|
session_token: Optional[str] = None,
|
|
) -> dict:
|
|
"""
|
|
Generate strategy recommendations based on performance analysis.
|
|
|
|
This method analyzes the user's gameplay patterns and provides
|
|
actionable recommendations for improvement.
|
|
"""
|
|
# Get recent games for analysis
|
|
games = await self.game_service.get_recent_games(20, session_token)
|
|
analysis = self.analyze_games(games)
|
|
|
|
recommendations = []
|
|
|
|
# Analyze perfect round rate
|
|
if analysis.perfect_round_percentage < 20:
|
|
recommendations.append(
|
|
{
|
|
"category": "accuracy",
|
|
"priority": "high",
|
|
"recommendation": "Focus on improving pinpoint accuracy",
|
|
"detail": f"Your perfect round rate is {analysis.perfect_round_percentage:.1f}%. "
|
|
"Practice with familiar maps to build confidence.",
|
|
}
|
|
)
|
|
|
|
# Analyze time usage
|
|
if analysis.average_time_seconds < 30:
|
|
recommendations.append(
|
|
{
|
|
"category": "time_management",
|
|
"priority": "medium",
|
|
"recommendation": "Consider taking more time per round",
|
|
"detail": f"Average time: {analysis.average_time_seconds:.0f}s. "
|
|
"Taking a bit more time can improve accuracy.",
|
|
}
|
|
)
|
|
|
|
# Analyze score trend
|
|
if analysis.score_trend == "declining":
|
|
recommendations.append(
|
|
{
|
|
"category": "consistency",
|
|
"priority": "high",
|
|
"recommendation": "Your scores are trending downward",
|
|
"detail": "Consider taking breaks and reviewing your weak areas.",
|
|
}
|
|
)
|
|
|
|
# Check for weak areas pattern
|
|
if len(analysis.weak_areas) > 5:
|
|
recommendations.append(
|
|
{
|
|
"category": "practice",
|
|
"priority": "medium",
|
|
"recommendation": "Practice specific regions",
|
|
"detail": f"You had {len(analysis.weak_areas)} rounds under 2000 points. "
|
|
"Consider using region-specific practice maps.",
|
|
}
|
|
)
|
|
|
|
return {
|
|
"analysis_summary": {
|
|
"games_analyzed": analysis.games_analyzed,
|
|
"average_score": round(analysis.average_score, 0),
|
|
"trend": analysis.score_trend,
|
|
"perfect_rate": f"{analysis.perfect_round_percentage:.1f}%",
|
|
},
|
|
"recommendations": recommendations,
|
|
"data_sources": {
|
|
"endpoints_used": schema_registry.get_available_endpoints(),
|
|
"last_updated": schema_registry.get_schema_summary()
|
|
.get("endpoints", {})
|
|
.get("/v4/feed/private", {})
|
|
.get("last_updated"),
|
|
},
|
|
}
|
|
|
|
@staticmethod
|
|
def _create_endpoint(path: str):
|
|
"""Create simple endpoint info for raw requests."""
|
|
from ..api.endpoints import EndpointInfo
|
|
|
|
return EndpointInfo(path=path, description=f"Request to {path}")
|