This commit is contained in:
2026-10-04 13:23:26 +07:00
parent 88307ed402
commit c663fbb504
20 changed files with 2125 additions and 502 deletions
@@ -0,0 +1,29 @@
package screen_plan_service
import "evening_detective_server/internal/repos/game_screen_plans_repo"
func mapPlan(plan *game_screen_plans_repo.Plan) *Plan {
steps := make([]Step, 0, len(plan.Steps))
for _, step := range plan.Steps {
steps = append(steps, Step{
Mode: step.Mode,
Text: step.Text,
TimerLabel: step.TimerLabel,
TimerDurationSeconds: step.TimerDurationSeconds,
})
}
return &Plan{GameID: plan.GameID, Steps: steps, CurrentStep: plan.CurrentStep}
}
func mapSteps(steps []Step) []game_screen_plans_repo.Step {
mapped := make([]game_screen_plans_repo.Step, 0, len(steps))
for _, step := range steps {
mapped = append(mapped, game_screen_plans_repo.Step{
Mode: step.Mode,
Text: step.Text,
TimerLabel: step.TimerLabel,
TimerDurationSeconds: step.TimerDurationSeconds,
})
}
return mapped
}
@@ -0,0 +1,157 @@
// Package screen_plan_service handles game screen plans.
package screen_plan_service
import (
"context"
"errors"
"strings"
"evening_detective_server/internal/repos/game_screen_plans_repo"
"evening_detective_server/internal/services/screen_service"
)
var (
// ErrPlanNotFound indicates that a game has no screen plan.
ErrPlanNotFound = errors.New("план экранов не найден")
// ErrInvalidPlan indicates invalid screen plan data.
ErrInvalidPlan = errors.New("некорректный план экранов")
// ErrStepNotFound indicates that a screen plan step does not exist.
ErrStepNotFound = errors.New("шаг плана экранов не найден")
)
// Step is a screen plan step used by the application layer.
type Step struct {
Mode int32
Text string
TimerLabel string
TimerDurationSeconds int64
}
// Plan is a game screen plan used by the application layer.
type Plan struct {
GameID int
Steps []Step
CurrentStep int
}
// ScreenPlanService handles game screen plans and applies their current step.
type ScreenPlanService struct {
repo *game_screen_plans_repo.GameScreenPlansRepo
screenService *screen_service.ScreenService
}
// NewScreenPlanService creates a screen plan service.
func NewScreenPlanService(
repo *game_screen_plans_repo.GameScreenPlansRepo,
screenService *screen_service.ScreenService,
) *ScreenPlanService {
return &ScreenPlanService{repo: repo, screenService: screenService}
}
// Get returns a game's screen plan.
func (s *ScreenPlanService) Get(ctx context.Context, gameID int) (*Plan, error) {
stored, err := s.repo.Get(ctx, gameID)
if errors.Is(err, game_screen_plans_repo.ErrPlanNotFound) {
return nil, ErrPlanNotFound
}
if err != nil {
return nil, err
}
return mapPlan(stored), nil
}
// Save validates, stores and applies a game's screen plan.
func (s *ScreenPlanService) Save(ctx context.Context, gameID int, steps []Step, currentStep int) (*Plan, error) {
steps, err := normalizePlan(steps, currentStep)
if err != nil {
return nil, err
}
if len(steps) == 0 {
if err := s.repo.Delete(ctx, gameID); err != nil {
return nil, err
}
return nil, nil
}
stored, err := s.repo.Save(ctx, gameID, mapSteps(steps), currentStep)
if err != nil {
return nil, err
}
plan := mapPlan(stored)
if err := s.applyStep(ctx, gameID, plan); err != nil {
return nil, err
}
return plan, nil
}
// SetStep stores and applies the selected step.
func (s *ScreenPlanService) SetStep(ctx context.Context, gameID, currentStep int) (*Plan, error) {
plan, err := s.Get(ctx, gameID)
if err != nil {
return nil, err
}
if currentStep < 0 || currentStep >= len(plan.Steps) {
return nil, ErrStepNotFound
}
stored, err := s.repo.Save(ctx, gameID, mapSteps(plan.Steps), currentStep)
if err != nil {
return nil, err
}
plan = mapPlan(stored)
if err := s.applyStep(ctx, gameID, plan); err != nil {
return nil, err
}
return plan, nil
}
// ApplyCurrent applies the current step when a screen joins a game.
func (s *ScreenPlanService) ApplyCurrent(ctx context.Context, gameID int) error {
plan, err := s.Get(ctx, gameID)
if errors.Is(err, ErrPlanNotFound) {
return nil
}
if err != nil {
return err
}
return s.applyStep(ctx, gameID, plan)
}
func normalizePlan(steps []Step, currentStep int) ([]Step, error) {
if currentStep < 0 || (len(steps) > 0 && currentStep >= len(steps)) || (len(steps) == 0 && currentStep != 0) {
return nil, ErrInvalidPlan
}
normalized := make([]Step, 0, len(steps))
for _, step := range steps {
if step.Mode <= 0 || step.Mode > 8 {
return nil, ErrInvalidPlan
}
step.Text = strings.TrimSpace(step.Text)
step.TimerLabel = strings.TrimSpace(step.TimerLabel)
if step.Mode == 8 && (step.Text == "" || len(step.Text) > 10000) {
return nil, ErrInvalidPlan
}
if step.Mode == 7 {
if step.TimerDurationSeconds <= 0 || step.TimerDurationSeconds > 24*60*60 {
return nil, ErrInvalidPlan
}
if step.TimerLabel == "" {
step.TimerLabel = "Перерыв"
}
if len(step.TimerLabel) > 200 {
return nil, ErrInvalidPlan
}
}
normalized = append(normalized, step)
}
return normalized, nil
}
func (s *ScreenPlanService) applyStep(ctx context.Context, gameID int, plan *Plan) error {
step := plan.Steps[plan.CurrentStep]
if step.Mode == 8 {
return s.screenService.SetText(ctx, gameID, step.Text)
}
if step.Mode == 7 {
return s.screenService.SetTimer(ctx, gameID, step.TimerDurationSeconds, step.TimerLabel)
}
return s.screenService.SetMode(ctx, gameID, step.Mode)
}
@@ -0,0 +1,46 @@
package screen_plan_service
import "testing"
func TestNormalizePlan(t *testing.T) {
tests := []struct {
name string
steps []Step
currentStep int
wantErr bool
}{
{
name: "normalizes text and timer defaults",
steps: []Step{
{Mode: 8, Text: " Внимание "},
{Mode: 7, TimerDurationSeconds: 60},
},
},
{
name: "rejects empty text",
steps: []Step{{Mode: 8}},
wantErr: true,
},
{
name: "rejects missing current step",
steps: []Step{{Mode: 1}},
currentStep: 1,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
steps, err := normalizePlan(tt.steps, tt.currentStep)
if (err != nil) != tt.wantErr {
t.Fatalf("normalizePlan() error = %v, want error: %v", err, tt.wantErr)
}
if tt.wantErr {
return
}
if steps[0].Text != "Внимание" || steps[1].TimerLabel != "Перерыв" {
t.Fatalf("plan defaults were not normalized: %+v", steps)
}
})
}
}
@@ -23,6 +23,7 @@ type ScreenSession struct {
TimerRemainingSeconds int64
TimerState int32
TimerEndsAt *time.Time
Text string
}
func mapSession(session *screen_sessions_repo.ScreenSession) *ScreenSession {
@@ -41,5 +42,6 @@ func mapSession(session *screen_sessions_repo.ScreenSession) *ScreenSession {
TimerRemainingSeconds: session.TimerRemainingSeconds,
TimerState: session.TimerState,
TimerEndsAt: session.TimerEndsAt,
Text: session.Text,
}
}
@@ -57,6 +57,11 @@ func (s *ScreenService) SetTimer(ctx context.Context, gameID int, duration int64
return s.repo.SetTimer(ctx, gameID, duration, label)
}
// SetText updates the text screen for a game.
func (s *ScreenService) SetText(ctx context.Context, gameID int, text string) error {
return s.repo.SetText(ctx, gameID, text)
}
// SetTimerCommand updates the timer command for a game.
func (s *ScreenService) SetTimerCommand(ctx context.Context, gameID int, command int32) error {
return s.repo.SetTimerCommand(ctx, gameID, command)