package gameplay_service import ( "context" "errors" "evening_detective_server/internal/modules/cleaner" "evening_detective_server/internal/modules/storytelling" "evening_detective_server/internal/repos/actions_repo" "evening_detective_server/internal/repos/applications_repo" "evening_detective_server/internal/repos/games_repo" "evening_detective_server/internal/repos/teams_repo" "evening_detective_server/internal/services/game_model" "evening_detective_server/internal/services/scenarios_service" "fmt" ) var ( // ErrTeamNotFound indicates that a team was not found. ErrTeamNotFound = errors.New("команда не найдена") // ErrGameNotFound indicates that a game was not found. ErrGameNotFound = errors.New("игра не найдена") ) // GameplayService handles team gameplay operations. type GameplayService struct { gamesRepo *games_repo.GamesRepo teamsRepo *teams_repo.TeamsRepo scenarioService *scenarios_service.ScenarioService storyteller storytelling.IStory actionsRepo *actions_repo.ActionsRepo applicationsRepo *applications_repo.ApplicationsRepo cleaner cleaner.ICleaner domain string } // NewGameplayService creates a gameplay service. func NewGameplayService( gamesRepo *games_repo.GamesRepo, teamsRepo *teams_repo.TeamsRepo, scenarioService *scenarios_service.ScenarioService, storyteller storytelling.IStory, actionsRepo *actions_repo.ActionsRepo, applicationsRepo *applications_repo.ApplicationsRepo, cleaner cleaner.ICleaner, domain string, ) *GameplayService { return &GameplayService{ gamesRepo: gamesRepo, teamsRepo: teamsRepo, scenarioService: scenarioService, storyteller: storyteller, actionsRepo: actionsRepo, applicationsRepo: applicationsRepo, cleaner: cleaner, domain: domain, } } // GiveTeamApplications grants an application to a team. func (s *GameplayService) GiveTeamApplications(ctx context.Context, teamID int, name string) error { return mapGameplayError(s.applicationsRepo.UpdateApplicationStatus(ctx, teamID, name, "done")) } // GetTeamActions returns the current story, game, and available team actions. func (s *GameplayService) GetTeamActions(ctx context.Context, teamID int, password string) (*storytelling.Story, *game_model.Game, []actions_repo.Action, error) { team, err := s.teamsRepo.GetTeamByID(ctx, teamID) if err != nil { return nil, nil, nil, mapGameplayError(err) } if team.Password != password || team.ParticipationStatus == "cancelled" { return nil, nil, nil, fmt.Errorf("%w: %v", ErrTeamNotFound, teams_repo.ErrTeamNotFound) } game, err := s.gamesRepo.GetGameByID(ctx, team.GameID) if err != nil { return nil, nil, nil, mapGameplayError(err) } if game.Status == "finish" { return &storytelling.Story{}, game_model.MapGame(game, s.domain), nil, nil } actions, err := s.actionsRepo.GetActionsByTeamID(ctx, teamID) if err != nil { return nil, nil, nil, mapGameplayError(err) } scenario, err := s.scenarioService.GetFullScenarioByID(ctx, game.ScenarioID) if err != nil { return nil, nil, nil, mapGameplayError(err) } codes := make([]string, len(actions)) for i, action := range actions { codes[i] = action.Code } story := s.storyteller.GetStory(scenario.Story, codes) filterTeamStory(story, game.Format) return story, game_model.MapGame(game, s.domain), actions, nil } // AddTeamAction records an action performed by a team. func (s *GameplayService) AddTeamAction(ctx context.Context, teamID int, password string, actionCode string, viaMCP bool) error { team, err := s.teamsRepo.GetTeamByID(ctx, teamID) if err != nil { return mapGameplayError(err) } if team.Password != password || team.ParticipationStatus == "cancelled" { return fmt.Errorf("%w: %v", ErrTeamNotFound, teams_repo.ErrTeamNotFound) } _, err = s.actionsRepo.AddAction(ctx, teamID, actionCode, viaMCP) if err != nil { return mapGameplayError(err) } if team.ParticipationStatus == "pending" { if err := s.teamsRepo.ConfirmParticipationByFirstAction(ctx, teamID); err != nil { return mapGameplayError(err) } } game, err := s.gamesRepo.GetGameByID(ctx, team.GameID) if err != nil { return mapGameplayError(err) } actionDetails, err := s.actionsRepo.GetActionsByTeamID(ctx, teamID) if err != nil { return mapGameplayError(err) } scenario, err := s.scenarioService.GetFullScenarioByID(ctx, game.ScenarioID) if err != nil { return mapGameplayError(err) } actions := make([]string, len(actionDetails)) for i, action := range actionDetails { actions[i] = action.Code } teamStory := s.storyteller.GetStory(scenario.Story, actions) lastPlace := teamStory.Places[len(teamStory.Places)-1] for _, application := range lastPlace.Applications { app := newApplication(application, s.domain) app.Name = s.cleaner.ClearText(app.Name) if _, err := s.applicationsRepo.AddApplication(ctx, teamID, app.Name, app.Image, app.FileType); err != nil { return mapGameplayError(err) } } return nil } // DeleteLastTeamAction removes the latest action for a team. func (s *GameplayService) DeleteLastTeamAction(ctx context.Context, teamID int) error { team, err := s.teamsRepo.GetTeamByID(ctx, teamID) if err != nil { return mapGameplayError(err) } game, err := s.gamesRepo.GetGameByID(ctx, team.GameID) if err != nil { return mapGameplayError(err) } actionDetails, err := s.actionsRepo.GetActionsByTeamID(ctx, teamID) if err != nil { return mapGameplayError(err) } scenario, err := s.scenarioService.GetFullScenarioByID(ctx, game.ScenarioID) if err != nil { return mapGameplayError(err) } actions := make([]string, len(actionDetails)) for i, action := range actionDetails { actions[i] = action.Code } teamStory := s.storyteller.GetStory(scenario.Story, actions) lastPlace := teamStory.Places[len(teamStory.Places)-1] for _, application := range lastPlace.Applications { if err := s.applicationsRepo.DeleteApplicationByTeamIDAndName(ctx, teamID, application.Name); err != nil { return mapGameplayError(err) } } return mapGameplayError(s.actionsRepo.DeleteLastActionByTeamID(ctx, teamID)) } func mapGameplayError(err error) error { if errors.Is(err, teams_repo.ErrTeamNotFound) { return fmt.Errorf("%w: %v", ErrTeamNotFound, err) } if errors.Is(err, games_repo.ErrGameNotFound) { return fmt.Errorf("%w: %v", ErrGameNotFound, err) } return err }