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.
438 lines
16 KiB
Go
438 lines
16 KiB
Go
package dtos
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"knowfoolery/backend/shared/types"
|
|
"knowfoolery/backend/shared/errors"
|
|
"knowfoolery/backend/services/game-session-service/internal/domain/valueobjects"
|
|
)
|
|
|
|
// Request DTOs
|
|
|
|
// StartSessionRequest represents a request to start a new session
|
|
type StartSessionRequest struct {
|
|
UserID types.UserID `json:"user_id" validate:"required"`
|
|
PlayerName string `json:"player_name,omitempty" validate:"omitempty,min=2,max=50"`
|
|
}
|
|
|
|
// Validate validates the start session request
|
|
func (r *StartSessionRequest) Validate() error {
|
|
if r.UserID.IsEmpty() {
|
|
return errors.ErrValidationFailed("user_id", "user ID is required")
|
|
}
|
|
|
|
if r.PlayerName != "" {
|
|
playerName := strings.TrimSpace(r.PlayerName)
|
|
if len(playerName) < 2 {
|
|
return errors.ErrValidationFailed("player_name", "player name must be at least 2 characters")
|
|
}
|
|
if len(playerName) > 50 {
|
|
return errors.ErrValidationFailed("player_name", "player name must be at most 50 characters")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ResumeSessionRequest represents a request to resume a session
|
|
type ResumeSessionRequest struct {
|
|
SessionID types.GameSessionID `json:"session_id" validate:"required"`
|
|
}
|
|
|
|
// Validate validates the resume session request
|
|
func (r *ResumeSessionRequest) Validate() error {
|
|
if r.SessionID.IsEmpty() {
|
|
return errors.ErrValidationFailed("session_id", "session ID is required")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SubmitAnswerRequest represents a request to submit an answer
|
|
type SubmitAnswerRequest struct {
|
|
SessionID types.GameSessionID `json:"session_id" validate:"required"`
|
|
QuestionID types.QuestionID `json:"question_id" validate:"required"`
|
|
PlayerAnswer string `json:"player_answer" validate:"required,max=500"`
|
|
HintUsed bool `json:"hint_used"`
|
|
}
|
|
|
|
// Validate validates the submit answer request
|
|
func (r *SubmitAnswerRequest) Validate() error {
|
|
if r.SessionID.IsEmpty() {
|
|
return errors.ErrValidationFailed("session_id", "session ID is required")
|
|
}
|
|
|
|
if r.QuestionID.IsEmpty() {
|
|
return errors.ErrValidationFailed("question_id", "question ID is required")
|
|
}
|
|
|
|
playerAnswer := strings.TrimSpace(r.PlayerAnswer)
|
|
if playerAnswer == "" {
|
|
return errors.ErrValidationFailed("player_answer", "player answer is required")
|
|
}
|
|
|
|
if len(playerAnswer) > types.MaxAnswerLength {
|
|
return errors.ErrValidationFailed("player_answer",
|
|
fmt.Sprintf("player answer must be at most %d characters", types.MaxAnswerLength))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RequestHintRequest represents a request for a hint
|
|
type RequestHintRequest struct {
|
|
SessionID types.GameSessionID `json:"session_id" validate:"required"`
|
|
QuestionID types.QuestionID `json:"question_id" validate:"required"`
|
|
}
|
|
|
|
// Validate validates the request hint request
|
|
func (r *RequestHintRequest) Validate() error {
|
|
if r.SessionID.IsEmpty() {
|
|
return errors.ErrValidationFailed("session_id", "session ID is required")
|
|
}
|
|
|
|
if r.QuestionID.IsEmpty() {
|
|
return errors.ErrValidationFailed("question_id", "question ID is required")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CompleteSessionRequest represents a request to complete a session
|
|
type CompleteSessionRequest struct {
|
|
SessionID types.GameSessionID `json:"session_id" validate:"required"`
|
|
Reason string `json:"reason" validate:"required,max=100"`
|
|
}
|
|
|
|
// Validate validates the complete session request
|
|
func (r *CompleteSessionRequest) Validate() error {
|
|
if r.SessionID.IsEmpty() {
|
|
return errors.ErrValidationFailed("session_id", "session ID is required")
|
|
}
|
|
|
|
reason := strings.TrimSpace(r.Reason)
|
|
if reason == "" {
|
|
return errors.ErrValidationFailed("reason", "completion reason is required")
|
|
}
|
|
|
|
if len(reason) > 100 {
|
|
return errors.ErrValidationFailed("reason", "completion reason must be at most 100 characters")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AbandonSessionRequest represents a request to abandon a session
|
|
type AbandonSessionRequest struct {
|
|
SessionID types.GameSessionID `json:"session_id" validate:"required"`
|
|
}
|
|
|
|
// Validate validates the abandon session request
|
|
func (r *AbandonSessionRequest) Validate() error {
|
|
if r.SessionID.IsEmpty() {
|
|
return errors.ErrValidationFailed("session_id", "session ID is required")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetSessionStatusRequest represents a request to get session status
|
|
type GetSessionStatusRequest struct {
|
|
SessionID types.GameSessionID `json:"session_id" validate:"required"`
|
|
}
|
|
|
|
// Validate validates the get session status request
|
|
func (r *GetSessionStatusRequest) Validate() error {
|
|
if r.SessionID.IsEmpty() {
|
|
return errors.ErrValidationFailed("session_id", "session ID is required")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetUserSessionsRequest represents a request to get user sessions
|
|
type GetUserSessionsRequest struct {
|
|
UserID types.UserID `json:"user_id" validate:"required"`
|
|
Limit int `json:"limit" validate:"min=1,max=100"`
|
|
Offset int `json:"offset" validate:"min=0"`
|
|
}
|
|
|
|
// Validate validates the get user sessions request
|
|
func (r *GetUserSessionsRequest) Validate() error {
|
|
if r.UserID.IsEmpty() {
|
|
return errors.ErrValidationFailed("user_id", "user ID is required")
|
|
}
|
|
|
|
if r.Limit <= 0 {
|
|
r.Limit = 20 // Default limit
|
|
}
|
|
if r.Limit > 100 {
|
|
return errors.ErrValidationFailed("limit", "limit must be at most 100")
|
|
}
|
|
|
|
if r.Offset < 0 {
|
|
return errors.ErrValidationFailed("offset", "offset must be non-negative")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetActiveSessionRequest represents a request to get active session
|
|
type GetActiveSessionRequest struct {
|
|
UserID types.UserID `json:"user_id" validate:"required"`
|
|
}
|
|
|
|
// Validate validates the get active session request
|
|
func (r *GetActiveSessionRequest) Validate() error {
|
|
if r.UserID.IsEmpty() {
|
|
return errors.ErrValidationFailed("user_id", "user ID is required")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Response DTOs
|
|
|
|
// StartSessionResponse represents the response to starting a session
|
|
type StartSessionResponse struct {
|
|
SessionID types.GameSessionID `json:"session_id"`
|
|
Status string `json:"status"`
|
|
CreatedAt types.Timestamp `json:"created_at"`
|
|
PlayerName string `json:"player_name,omitempty"`
|
|
MaxDuration types.Duration `json:"max_duration"`
|
|
RemainingTime types.Duration `json:"remaining_time"`
|
|
}
|
|
|
|
// ResumeSessionResponse represents the response to resuming a session
|
|
type ResumeSessionResponse struct {
|
|
SessionID types.GameSessionID `json:"session_id"`
|
|
CanResume bool `json:"can_resume"`
|
|
TimeoutOccurred bool `json:"timeout_occurred"`
|
|
Status string `json:"status"`
|
|
RemainingTime *types.Duration `json:"remaining_time,omitempty"`
|
|
CurrentScore *ScoreDTO `json:"current_score,omitempty"`
|
|
QuestionsProgress map[string]QuestionProgressDTO `json:"questions_progress,omitempty"`
|
|
CompletionResult *SessionCompletionResultDTO `json:"completion_result,omitempty"`
|
|
}
|
|
|
|
// SubmitAnswerResponse represents the response to submitting an answer
|
|
type SubmitAnswerResponse struct {
|
|
Accepted bool `json:"accepted"`
|
|
ProcessingResult *AnswerProcessingResult `json:"processing_result,omitempty"`
|
|
AntiCheatViolations []string `json:"anti_cheat_violations,omitempty"`
|
|
UpdatedScore *ScoreDTO `json:"updated_score,omitempty"`
|
|
RemainingTime types.Duration `json:"remaining_time"`
|
|
TimeoutOccurred bool `json:"timeout_occurred"`
|
|
CompletionResult *SessionCompletionResultDTO `json:"completion_result,omitempty"`
|
|
RejectionReason *string `json:"rejection_reason,omitempty"`
|
|
}
|
|
|
|
// RequestHintResponse represents the response to requesting a hint
|
|
type RequestHintResponse struct {
|
|
HintText string `json:"hint_text"`
|
|
QuestionID types.QuestionID `json:"question_id"`
|
|
RequestedAt types.Timestamp `json:"requested_at"`
|
|
}
|
|
|
|
// CompleteSessionResponse represents the response to completing a session
|
|
type CompleteSessionResponse struct {
|
|
SessionID types.GameSessionID `json:"session_id"`
|
|
FinalScore int `json:"final_score"`
|
|
CompletedAt types.Timestamp `json:"completed_at"`
|
|
Reason string `json:"reason"`
|
|
Duration types.Duration `json:"duration"`
|
|
CompletionResult *SessionCompletionResultDTO `json:"completion_result"`
|
|
}
|
|
|
|
// AbandonSessionResponse represents the response to abandoning a session
|
|
type AbandonSessionResponse struct {
|
|
SessionID types.GameSessionID `json:"session_id"`
|
|
AbandonedAt types.Timestamp `json:"abandoned_at"`
|
|
FinalScore int `json:"final_score"`
|
|
}
|
|
|
|
// GetSessionStatusResponse represents the response to getting session status
|
|
type GetSessionStatusResponse struct {
|
|
SessionID types.GameSessionID `json:"session_id"`
|
|
Status string `json:"status"`
|
|
CurrentScore *ScoreDTO `json:"current_score"`
|
|
TimerStatus *TimerStatusDTO `json:"timer_status"`
|
|
QuestionsProgress map[string]QuestionProgressDTO `json:"questions_progress"`
|
|
TimeoutOccurred bool `json:"timeout_occurred"`
|
|
CompletionResult *SessionCompletionResultDTO `json:"completion_result,omitempty"`
|
|
}
|
|
|
|
// GetUserSessionsResponse represents the response to getting user sessions
|
|
type GetUserSessionsResponse struct {
|
|
Sessions []SessionSummary `json:"sessions"`
|
|
TotalCount int `json:"total_count"`
|
|
Offset int `json:"offset"`
|
|
Limit int `json:"limit"`
|
|
}
|
|
|
|
// GetActiveSessionResponse represents the response to getting active session
|
|
type GetActiveSessionResponse struct {
|
|
HasActiveSession bool `json:"has_active_session"`
|
|
SessionID types.GameSessionID `json:"session_id,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
CreatedAt types.Timestamp `json:"created_at,omitempty"`
|
|
PlayerName *string `json:"player_name,omitempty"`
|
|
CurrentScore *ScoreDTO `json:"current_score,omitempty"`
|
|
RemainingTime types.Duration `json:"remaining_time,omitempty"`
|
|
QuestionsProgress map[string]QuestionProgressDTO `json:"questions_progress,omitempty"`
|
|
}
|
|
|
|
// Nested DTOs
|
|
|
|
// ScoreDTO represents score information
|
|
type ScoreDTO struct {
|
|
Total int `json:"total"`
|
|
QuestionsAttempted int `json:"questions_attempted"`
|
|
QuestionsCorrect int `json:"questions_correct"`
|
|
SuccessRate float64 `json:"success_rate"`
|
|
PointsFromHints int `json:"points_from_hints"`
|
|
PointsWithoutHints int `json:"points_without_hints"`
|
|
HintsUsed int `json:"hints_used"`
|
|
AveragePointsPerQuestion float64 `json:"average_points_per_question"`
|
|
}
|
|
|
|
// TimerStatusDTO represents timer status information
|
|
type TimerStatusDTO struct {
|
|
IsActive bool `json:"is_active"`
|
|
IsExpired bool `json:"is_expired"`
|
|
IsInWarningPeriod bool `json:"is_in_warning_period"`
|
|
IsInCriticalPeriod bool `json:"is_in_critical_period"`
|
|
StartTime types.Timestamp `json:"start_time"`
|
|
EndTime *types.Timestamp `json:"end_time,omitempty"`
|
|
ElapsedTime types.Duration `json:"elapsed_time"`
|
|
RemainingTime types.Duration `json:"remaining_time"`
|
|
ElapsedFormatted string `json:"elapsed_formatted"`
|
|
RemainingFormatted string `json:"remaining_formatted"`
|
|
ElapsedPercentage float64 `json:"elapsed_percentage"`
|
|
RemainingPercentage float64 `json:"remaining_percentage"`
|
|
WarningsSent []valueobjects.TimerWarning `json:"warnings_sent"`
|
|
}
|
|
|
|
// QuestionProgressDTO represents progress on a single question
|
|
type QuestionProgressDTO struct {
|
|
QuestionID types.QuestionID `json:"question_id"`
|
|
PresentedAt types.Timestamp `json:"presented_at"`
|
|
HintUsed bool `json:"hint_used"`
|
|
IsCompleted bool `json:"is_completed"`
|
|
Attempts []AttemptDTO `json:"attempts"`
|
|
}
|
|
|
|
// AttemptDTO represents a single attempt
|
|
type AttemptDTO struct {
|
|
ID types.AttemptID `json:"id"`
|
|
AttemptNumber int `json:"attempt_number"`
|
|
PlayerAnswer string `json:"player_answer"`
|
|
IsCorrect bool `json:"is_correct"`
|
|
IsTimeout bool `json:"is_timeout"`
|
|
PointsAwarded int `json:"points_awarded"`
|
|
HintUsed bool `json:"hint_used"`
|
|
MatchType string `json:"match_type"`
|
|
Similarity float64 `json:"similarity"`
|
|
AttemptedAt types.Timestamp `json:"attempted_at"`
|
|
}
|
|
|
|
// AnswerProcessingResult represents the result of processing an answer
|
|
type AnswerProcessingResult struct {
|
|
IsCorrect bool `json:"is_correct"`
|
|
Score int `json:"score"`
|
|
AttemptNumber int `json:"attempt_number"`
|
|
AttemptsRemaining int `json:"attempts_remaining"`
|
|
Similarity float64 `json:"similarity"`
|
|
MatchType string `json:"match_type"`
|
|
IsQuestionComplete bool `json:"is_question_complete"`
|
|
}
|
|
|
|
// SessionCompletionResultDTO represents the result of session completion
|
|
type SessionCompletionResultDTO struct {
|
|
SessionID types.GameSessionID `json:"session_id"`
|
|
FinalScore int `json:"final_score"`
|
|
CompletedAt types.Timestamp `json:"completed_at"`
|
|
Reason string `json:"reason"`
|
|
Duration types.Duration `json:"duration"`
|
|
}
|
|
|
|
// SessionSummary represents a summary of a session
|
|
type SessionSummary struct {
|
|
SessionID types.GameSessionID `json:"session_id"`
|
|
Status string `json:"status"`
|
|
CreatedAt types.Timestamp `json:"created_at"`
|
|
UpdatedAt types.Timestamp `json:"updated_at"`
|
|
Duration types.Duration `json:"duration"`
|
|
PlayerName *string `json:"player_name,omitempty"`
|
|
Score *ScoreDTO `json:"score"`
|
|
QuestionsCount int `json:"questions_count"`
|
|
}
|
|
|
|
// Helper methods for DTOs
|
|
|
|
// GetScoreSummary returns a human-readable score summary
|
|
func (s *ScoreDTO) GetScoreSummary() string {
|
|
return fmt.Sprintf("%d points (%d/%d correct, %.1f%% success rate)",
|
|
s.Total, s.QuestionsCorrect, s.QuestionsAttempted, s.SuccessRate)
|
|
}
|
|
|
|
// GetTimerSummary returns a human-readable timer summary
|
|
func (t *TimerStatusDTO) GetTimerSummary() string {
|
|
if !t.IsActive {
|
|
return fmt.Sprintf("Inactive (Duration: %s)", t.ElapsedFormatted)
|
|
}
|
|
|
|
if t.IsExpired {
|
|
return "Session expired"
|
|
}
|
|
|
|
status := "Active"
|
|
if t.IsInCriticalPeriod {
|
|
status = "Critical"
|
|
} else if t.IsInWarningPeriod {
|
|
status = "Warning"
|
|
}
|
|
|
|
return fmt.Sprintf("%s (%s remaining)", status, t.RemainingFormatted)
|
|
}
|
|
|
|
// GetProgressSummary returns a summary of question progress
|
|
func (q *QuestionProgressDTO) GetProgressSummary() string {
|
|
if q.IsCompleted {
|
|
return fmt.Sprintf("Completed (%d attempts)", len(q.Attempts))
|
|
}
|
|
return fmt.Sprintf("In progress (%d attempts)", len(q.Attempts))
|
|
}
|
|
|
|
// GetAttemptSummary returns a summary of an attempt
|
|
func (a *AttemptDTO) GetAttemptSummary() string {
|
|
status := "incorrect"
|
|
if a.IsTimeout {
|
|
status = "timeout"
|
|
} else if a.IsCorrect {
|
|
status = "correct"
|
|
}
|
|
|
|
return fmt.Sprintf("Attempt %d: %s (%d points)",
|
|
a.AttemptNumber, status, a.PointsAwarded)
|
|
}
|
|
|
|
// GetSessionSummary returns a summary of the session
|
|
func (s *SessionSummary) GetSessionSummary() string {
|
|
playerInfo := ""
|
|
if s.PlayerName != nil {
|
|
playerInfo = fmt.Sprintf(" - %s", *s.PlayerName)
|
|
}
|
|
|
|
return fmt.Sprintf("Session %s: %s%s (%s, %d questions)",
|
|
s.SessionID, s.Status, playerInfo, s.Duration.String(), s.QuestionsCount)
|
|
}
|
|
|
|
// IsSuccessful returns true if the answer processing was successful
|
|
func (apr *AnswerProcessingResult) IsSuccessful() bool {
|
|
return apr.IsCorrect && apr.Score > 0
|
|
}
|
|
|
|
// GetCompletionSummary returns a summary of the completion result
|
|
func (scr *SessionCompletionResultDTO) GetCompletionSummary() string {
|
|
return fmt.Sprintf("Session completed: %d points in %s (reason: %s)",
|
|
scr.FinalScore, scr.Duration.String(), scr.Reason)
|
|
} |