diff --git a/server/src/ApiGateways/WebSocket/Web.Socket/Hubs/QuizzerGateway.cs b/server/src/ApiGateways/WebSocket/Web.Socket/Hubs/QuizzerGateway.cs index 2c1913c..4ad5523 100644 --- a/server/src/ApiGateways/WebSocket/Web.Socket/Hubs/QuizzerGateway.cs +++ b/server/src/ApiGateways/WebSocket/Web.Socket/Hubs/QuizzerGateway.cs @@ -25,15 +25,27 @@ public override async Task OnConnectedAsync() if (!ulong.TryParse(queryRoomId, out ulong roomId)) throw new ArgumentException("Room Id should be ulong type"); - var exist = await _service.QuizExist(roomId); + var game = await _service.QuizExist(roomId); // TODO maybe changing it to simple Get request - if (exist == null) + if (game == null) { await Clients.Caller.SendAsync("Error", "Room does not exist"); Context.Abort(); + return; } - await _service.JoinGame(roomId, Context.User?.Identity?.Name ?? Context.ConnectionId); + // Temporary way to understand if its the owner + if (game.Users.Count <= 0) + game.IsOwner = true; + + // Reply with connected + await Clients.Caller.SendAsync("Connected", game); + await Clients.Caller.SendAsync("Ready", Context.ConnectionId); + + // Add user to the group + await Groups.AddToGroupAsync(Context.ConnectionId, roomId.ToString()); + // Add user to the game + await _service.JoinGame(roomId, Context.ConnectionId); await base.OnConnectedAsync(); } @@ -49,11 +61,11 @@ public async Task NextQuestion(ulong id) { await _service.NextQuestion(id); } - - public Task SendMessage(string user, string message) + + [HubMethodName("SubmitAnswer")] + public async Task SubmitAnswer(ulong id, string answer) { - return Clients.All.SendAsync("ReceivedMessage", user, message); + await _service.SubmitAnswer(id, Context.ConnectionId, answer); } - } } diff --git a/server/src/ApiGateways/WebSocket/Web.Socket/Services/QuizService.cs b/server/src/ApiGateways/WebSocket/Web.Socket/Services/QuizService.cs index 9d83e3c..48bc6ab 100644 --- a/server/src/ApiGateways/WebSocket/Web.Socket/Services/QuizService.cs +++ b/server/src/ApiGateways/WebSocket/Web.Socket/Services/QuizService.cs @@ -17,13 +17,13 @@ public QuizService(HttpClient http, IConfiguration config) _config = config; } - public Task QuizExist(ulong id) + public Task QuizExist(ulong id) { return GrpcCallerService.CallService(_config.GetValue("QuizGrpcApi"), async channel => { var client = new Quizer.QuizerClient(channel); - return await client.QuizExistAsync(new QuizExistRequest() + return await client.GetQuizAsync(new GetQuizRequest() { Id = id }); @@ -56,6 +56,20 @@ public Task JoinGame(ulong id, string user) }); }); } + public Task SubmitAnswer(ulong id, string user, string answer) + { + return GrpcCallerService.CallService(_config.GetValue("QuizGrpcApi"), async channel => + { + var client = new Quizer.QuizerClient(channel); + + return await client.SubmitAnswerAsync(new SubmitAnswerRequest() + { + Answer = answer, + UserId = user, + Id = id + }); + }); + } public Task NextQuestion(ulong id) { diff --git a/server/src/Services/Quizer/Quiz.API/Services/QuizerService.cs b/server/src/Services/Quizer/Quiz.API/Services/QuizerService.cs index 539f713..38dcb05 100644 --- a/server/src/Services/Quizer/Quiz.API/Services/QuizerService.cs +++ b/server/src/Services/Quizer/Quiz.API/Services/QuizerService.cs @@ -20,34 +20,64 @@ public QuizerService(QuizManager manager) _manager = manager; } - public override Task JoinGame(JoinGameRequest request, ServerCallContext context) + public override async Task JoinGame(JoinGameRequest request, ServerCallContext context) { - _manager.JoinGame(request.Id, request.User); + await _manager.JoinGame(request.Id, request.User).ConfigureAwait(false); - return Task.FromResult(new Empty()); + return new Empty(); } - public override Task QuizExist(QuizExistRequest request, ServerCallContext context) + public override Task GetQuiz(GetQuizRequest request, ServerCallContext context) { try { - var quiz = _manager.TryGetQuiz(request.Id); + var game = _manager.TryGetQuiz(request.Id); - return Task.FromResult(new QuizExistResponse() + return Task.FromResult(new QuizCreatedResponse() { + Id = game.Id, Quiz = new QuizData() { - Description = quiz.Quiz.Description, - ImageUrl = quiz.Quiz.ImageUrl, - Title = quiz.Quiz.Title - } + Title = game.Quiz.Title, + Description = game.Quiz.Description, + ImageUrl = game.Quiz.ImageUrl, + Questions = { } // TODO finish mapping this model + }, + Users = { game.Users.Select(x => x.Id)} }); } catch (GameNotFoundException) { context.Status = new Status(StatusCode.NotFound, "Quiz does not exist"); - return Task.FromResult(new QuizExistResponse()); + return Task.FromResult(new QuizCreatedResponse()); + } + } + + public override Task SubmitAnswer(SubmitAnswerRequest request, ServerCallContext context) + { + try + { + var game = _manager.TryGetQuiz(request.Id); + + var questions = game.Quiz.Questions[game.CurrentQuestion - 1]; + + var answer = questions?.Answer.FirstOrDefault(x => x.Description == request.Answer); + + if (answer != null && answer.IsCorrect) + { + var player = game.Users.FirstOrDefault(x => x.Id == request.UserId); + + if (player != null) player.Score += new Random().Next(5, 20); // XD + } + + return Task.FromResult(new Empty()); + } + catch (GameNotFoundException) + { + context.Status = new Status(StatusCode.NotFound, "Quiz does not exist"); + + return Task.FromResult(new Empty()); } } @@ -58,7 +88,6 @@ public override Task CreateGame(QuizCreateRequest request, Title = request.Quiz.Title, Description = request.Quiz.Description, ImageUrl = request.Quiz.ImageUrl, - Users = new List(), Questions = request.Quiz.Questions.Select(x => new Question() { Timeout = x.Timeout, @@ -79,7 +108,8 @@ public override Task CreateGame(QuizCreateRequest request, { Quiz = quiz, Id = id, - Started = DateTime.UtcNow + Started = DateTime.UtcNow, + Users = new List() }; var result = _manager.CreateNew(game); diff --git a/server/src/Services/Quizer/Quiz.Infrastructure/QuizManager.cs b/server/src/Services/Quizer/Quiz.Infrastructure/QuizManager.cs index df72ef9..c882a91 100644 --- a/server/src/Services/Quizer/Quiz.Infrastructure/QuizManager.cs +++ b/server/src/Services/Quizer/Quiz.Infrastructure/QuizManager.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using MassTransit; using Quizzer.Domain.Entities; +using Quizzer.Domain.Enums; using Quizzer.Domain.Events; using Quizzer.Domain.Exceptions; @@ -20,13 +21,17 @@ public QuizManager(IBusControl bus) private readonly ConcurrentDictionary _runningGame = new (); - private readonly ConcurrentDictionary _runningQuiz = new(); - //public bool Add(string game) => _runningGame.Add(userId); //public bool Remove(ulong userId) => _runningGame.TryRemove(userId); public void Clear() => _runningGame.Clear(); - public QuizGame TryGetQuiz(ulong id) + /// + /// Tries to get the quiz based on the given id + /// + /// + /// + /// Thrown when the game is not found + public QuizGame TryGetQuiz(ulong id) // TODO find a better name since Try indicate it should not throw an exception { if (!_runningGame.TryGetValue(id, out QuizGame game)) { @@ -41,11 +46,21 @@ public QuizGame TryGetQuiz(ulong id) /// /// /// - public void JoinGame(ulong id, string user) + public async Task JoinGame(ulong id, string user) { var game = TryGetQuiz(id); - game.Quiz.Users.Add(user); + game.Users.Add(new Player() + { + Id = user, + Score = 0 + }); + + await _bus.Publish(new UserJoinedGameEvent() + { + UserId = user, + GameId = id + }); } /// @@ -69,58 +84,89 @@ public QuizGame CreateNew(QuizGame game) public QuizGame EndGame(ulong id) { return TryGetQuiz(id); + + // TODO end the game } + /// + /// Starts a quiz game based on the given id + /// + /// + /// + /// Thrown when the game is in a state different than IDLE public async Task Start(ulong id) { var game = TryGetQuiz(id); - _runningQuiz.TryAdd(id, game.Quiz); + if (game.Status != GameStatus.Idle) + throw new Exception("Expected game to be idle but was in a different state"); // TODO add a custom exception + + game.Status = GameStatus.Running; var gameStartedEvent = new GameStartedEvent() { GameId = id, - Users = game.Quiz.Users + Users = game.Users.Select(x => x.Id).ToList(), + Status = game.Status, + CurrentQuestion = game.CurrentQuestion }; await _bus.Publish(gameStartedEvent); + // TODO find a better solution await Task.Delay(500); await StartQuestion(id, game.CurrentQuestion); } - public async Task StartQuestion(ulong quizId, int index) + /// + /// Start the given question in the specified game id + /// + /// + /// + /// + public async Task StartQuestion(ulong gameId, int index) { - var quiz = TryGetQuiz(quizId); + var game = TryGetQuiz(gameId); - if (quiz.Quiz.Questions.Count <= index) + if (game.Quiz.Questions.Count <= index) { - await _bus.Publish(new GameEndedEvent() {GameId = quizId}); + // End the game and remove it from running games + game.Status = GameStatus.Ended; + await _bus.Publish(new GameEndedEvent() {Game = game}); + _runningGame.TryRemove(game.Id, out _); return null; } - var question = quiz.Quiz.Questions[index]; + // TODO what if its out of index? + var question = game.Quiz.Questions[index]; - if (question == null) return null; // Throw exception + if (question == null) return null; - quiz.CurrentQuestion = quiz.CurrentQuestion += 1; + // TODO check if this is really necessary + game.CurrentQuestion = game.CurrentQuestion += 1; var questionStartedEvent = new QuestionStartedEvent() { Question = question.Title, - Answers = question.Answer.Select(x => x.Description).ToList() + Answers = question.Answer.Select(x => x.Description).ToList(), + GameId = gameId, + CurrentQuestion = game.CurrentQuestion, + EndAt = DateTimeOffset.UtcNow.AddSeconds(16) // TODO use the question timeout instead }; await _bus.Publish(questionStartedEvent); + // TODO add a scheduler _ = Task.Run(async () => { + // TODO respect timeout from the question entity await Task.Delay(TimeSpan.FromSeconds(15)); await _bus.Publish(new QuestionEndedEvent() { - CorrectAnswer = question.Answer.Where(x => x.IsCorrect).Select(x => x.Description).ToList() + Answers = question.Answer, + GameId = gameId }); });