diff --git a/qtc-net-server/Enums/GameStatus.cs b/qtc-net-server/Enums/GameStatus.cs index 46f1769..6839476 100644 --- a/qtc-net-server/Enums/GameStatus.cs +++ b/qtc-net-server/Enums/GameStatus.cs @@ -3,6 +3,7 @@ public enum GameStatus { WaitingForPlayer, + SelectingSymbol, Ongoing, P1Win, P2Win, diff --git a/qtc-net-server/Hubs/TicTacToeHub.cs b/qtc-net-server/Hubs/TicTacToeHub.cs index 0849556..ae4e7e4 100644 --- a/qtc-net-server/Hubs/TicTacToeHub.cs +++ b/qtc-net-server/Hubs/TicTacToeHub.cs @@ -1,6 +1,7 @@ using qtc_api.Enums; using qtc_api.Models; using qtc_api.Schema; +using qtc_api.Services.GameRoomService; using System.Runtime.CompilerServices; using System.Timers; @@ -11,115 +12,95 @@ namespace qtc_api.Hubs { private IUserService _userService; private ILogger _logger; - - private Dictionary GameRooms = []; - public TicTacToeHub(ILogger logger, IUserService userService) + private GameRoomService _gameRoomService; + public TicTacToeHub(ILogger logger, IUserService userService, GameRoomService gameRoomService) { _userService = userService; + _gameRoomService = gameRoomService; _logger = logger; } - public async override Task OnConnectedAsync() + public override async Task OnDisconnectedAsync(Exception? exception) { - // tell the client that the server is finding a room - await Clients.Client(Context.ConnectionId).SendAsync("FindingRoom"); - - // check game rooms dictionary for any open rooms (any rooms that are waiting on players) - if(GameRooms.Count > 0 && GameRooms.Any(e => e.Value.Status == GameStatus.WaitingForPlayer)) + // find any existing room user was in + var room = _gameRoomService.GameRooms.FirstOrDefault(e => (e.Player1?.Id == Context.UserIdentifier) || (e.Player2?.Id == Context.UserIdentifier)); + if (room != null) { - // get first available room waiting for a player - var roomKVP = GameRooms.FirstOrDefault(e => (e.Value.Status == GameStatus.WaitingForPlayer) || (e.Value.Status == GameStatus.PlayerDisconnected)); + // just end the game + room.Status = GameStatus.PlayerDisconnected; - var room = roomKVP.Value; - var roomId = roomKVP.Key.ToString(); + string usernameDisconnected = string.Empty; - if (room != null && room.Status == GameStatus.WaitingForPlayer) - { - // get user from user identifier - var user = await _userService.GetUserById(Context.UserIdentifier ?? ""); + // determine which player to null out + if (room.Player1?.Id == Context.UserIdentifier) { usernameDisconnected = room.Player1!.Username; room.Player1 = null; } + else { usernameDisconnected = room.Player2!.Username; room.Player2 = null; } - if (user != null && user.Success && user.Data != null) - { - // set player 2 - room.Player2 = user.Data; + bool isWin = room.Status == GameStatus.P1Win || room.Status == GameStatus.P2Win || room.Status == GameStatus.NoWin; + if(!isWin) + await Clients.Group(room.Id).SendAsync("GameEnd", room, usernameDisconnected); - // add player to room - await Groups.AddToGroupAsync(Context.ConnectionId, roomId); - - // start symbol selection (on player one) - await Clients.User(room.Player1.Id).SendAsync("SelectSymbol"); - - // set room status to ongoing - room.Status = GameStatus.Ongoing; - } - } else if (room != null && room.Status == GameStatus.PlayerDisconnected) - { - // add player back into group - await Groups.AddToGroupAsync(Context.ConnectionId, roomId); - - if (room.Player1?.Id == Context.UserIdentifier) await Clients.Client(roomId).SendAsync("HostReconnect"); - else await Clients.Client(roomId).SendAsync("PlayerReconnect"); - } - } else - { - // get user from user identifier - var user = await _userService.GetUserById(Context.UserIdentifier ?? ""); - - if(user != null && user.Success && user.Data != null) - { - // create a room - var guid = new Guid(); - GameRooms.Add(guid, new GameRoom { Status = GameStatus.WaitingForPlayer, Player1 = user.Data }); - - var guidString = guid.ToString(); - - // add player to new room - await Groups.AddToGroupAsync(Context.ConnectionId, guidString); - - // send waiting for player message - await Clients.Group(guidString).SendAsync("WaitingForPlayer"); - } + // remove the room + _gameRoomService.RemoveRoom(room); } } - public async override Task OnDisconnectedAsync(Exception? exception) + [HubMethodName("FindRoom")] + public async Task FindRoom() { - // find the game room the user is in - var roomKVP = GameRooms.FirstOrDefault(e => (e.Value.Player1?.Id == Context.UserIdentifier) || (e.Value.Player2?.Id == Context.UserIdentifier)); + // find user from useridentifier + var user = await _userService.GetUserById(Context.UserIdentifier!); - if(roomKVP.Value != null) + if (user != null && user.Success && user.Data != null) { - var room = roomKVP.Value; - var roomId = roomKVP.Key.ToString(); + // find any room waiting on a player + var roomWOP = _gameRoomService.GameRooms.FirstOrDefault(e => e.Status == GameStatus.WaitingForPlayer && e.Player2 == null && e.Player1?.Id != Context.UserIdentifier); + if (roomWOP != null) + { + // add player to said group + await Groups.AddToGroupAsync(Context.ConnectionId, roomWOP.Id); - // inform the room that one of the players has disconnected - if (room.Player1?.Id == Context.UserIdentifier) await Clients.Group(roomId).SendAsync("HostDisconnected", exception); - else await Clients.Group(roomId).SendAsync("PlayerDisconnected", exception); + // set player two + roomWOP.Player2 = user.Data; - // set room status to discconnect - room.Status = GameStatus.PlayerDisconnected; + // this game can now start symbol selection + roomWOP.Status = GameStatus.SelectingSymbol; + await Clients.User(roomWOP.Player1!.Id).SendAsync("SelectSymbol", roomWOP); + await Clients.Group(roomWOP.Id).SendAsync("SelectingSymbol", roomWOP); - // schedule the game to be ended if player does not return - await ScheduleGameEnd(roomKVP, Context.UserIdentifier); + return; + } + + // find any room who's player one matches the user and is stuck in 'WaitingForPlayer' status + var roomWFPP1 = _gameRoomService.GameRooms.FirstOrDefault(e => e.Player1?.Id == user.Data.Id && e.Status == GameStatus.WaitingForPlayer); + if (roomWFPP1 != null) + // just remove it + _gameRoomService.RemoveRoom(roomWFPP1); + + // if all if statements above don't match, make a new room + var gameRoom = new GameRoom { Id = Guid.NewGuid().ToString(), Player1 = user.Data, Status = GameStatus.WaitingForPlayer }; + _gameRoomService.AddRoom(gameRoom); + + // add user to group + await Groups.AddToGroupAsync(Context.ConnectionId, gameRoom.Id); + + // waiting for player + await Clients.Group(gameRoom.Id).SendAsync("WaitingForPlayer", gameRoom); } } [HubMethodName("SetSymbol")] - public async Task SetPlayerSymbol(User user, TicTacToeSymbol symbol) + public async Task SetPlayerSymbol(string roomId, TicTacToeSymbol symbol) { // find the room - var roomKVP = GameRooms.FirstOrDefault(e => (e.Value.Player1?.Id == user.Id) || (e.Value.Player2?.Id == user.Id)); - var room = roomKVP.Value; + var room = _gameRoomService.GameRooms.FirstOrDefault(e => e.Id == roomId); - if(room != null) + if (room != null) { - var roomId = roomKVP.Key.ToString(); - // set player one symbol room.P1Symbol = symbol; // set player two symbol based on what player one picked - switch(room.P1Symbol) + switch (room.P1Symbol) { case TicTacToeSymbol.X: room.P2Symbol = TicTacToeSymbol.O; @@ -130,29 +111,31 @@ namespace qtc_api.Hubs } // the game can now start - await Clients.Group(roomId).SendAsync("GameStart"); + room.Status = GameStatus.Ongoing; + await Clients.Group(room.Id).SendAsync("GameStart", room); + + // start player ones turn + await Clients.User(room.Player1!.Id).SendAsync("StartTurn", room); + await Clients.User(room.Player2!.Id).SendAsync("EndTurn", room); } } [HubMethodName("MakeMove")] - public async Task MakeMoveAsync(TicTacToeMove move) + public async Task MakeMove(string roomId, TicTacToeMove move) { // find the room - var roomKVP = GameRooms.FirstOrDefault(e => (e.Value.Player1?.Id == move.User.Id) || (e.Value.Player2?.Id == move.User.Id)); - var room = roomKVP.Value; + var room = _gameRoomService.GameRooms.FirstOrDefault(e => e.Id == roomId); if (room != null) { - var roomId = roomKVP.Key.ToString(); - // update board based on move (TOOD - figure out a better way to do this) - if(room.Player1?.Id == move.User.Id) + if (room.Player1?.Id == move.User.Id) { - switch(move.Point) + switch (move.Point) { case 1: room.Board.Square1 = room.P1Symbol; - break; + break; case 2: room.Board.Square2 = room.P1Symbol; break; @@ -178,7 +161,8 @@ namespace qtc_api.Hubs room.Board.Square9 = room.P1Symbol; break; } - } else + } + else { switch (move.Point) { @@ -213,31 +197,78 @@ namespace qtc_api.Hubs } // send board update - await Clients.Group(roomId).SendAsync("UpdateBoard", room.Board); + await Clients.Group(roomId).SendAsync("UpdateBoard", room, move); - // TODO - win logic + // check for draw + if(room.Board.IsDraw()) + { + room.Status = GameStatus.NoWin; + await Clients.Group(room.Id).SendAsync("GameEnd", room, string.Empty); + return; + } + + // check for winner + var winningSymbol = room.Board.GetWinner(); + if(winningSymbol != TicTacToeSymbol.Blank) + { + if (winningSymbol == room.P1Symbol) + { + // player one has won the game + room.Status = GameStatus.P1Win; + await Clients.Group(room.Id).SendAsync("GameEnd", room, string.Empty); + } else + { + // player two has won the game + room.Status = GameStatus.P2Win; + await Clients.Group(room.Id).SendAsync("GameEnd", room, string.Empty); + } + + return; + } + + // switch turns + if (move.User.Id == room.Player1?.Id) + { + await Clients.User(room.Player1!.Id).SendAsync("EndTurn", room); + await Clients.User(room.Player2!.Id).SendAsync("StartTurn", room); + } + else + { + await Clients.User(room.Player2!.Id).SendAsync("EndTurn", room); + await Clients.User(room.Player1!.Id).SendAsync("StartTurn", room); + } } } - private Task ScheduleGameEnd(KeyValuePair roomKVP, string? userId) + [HubMethodName("RestartGame")] + public async Task RestartGame(string roomId) { - System.Timers.Timer tmr = new System.Timers.Timer(TimeSpan.FromSeconds(60)); - tmr.Start(); - - tmr.Elapsed += async (sender, args) => + // find the room + var room = _gameRoomService.GameRooms.FirstOrDefault(e => e.Id == roomId); + if(room != null) { - var client = Clients.User(userId!); - if (client == null) - { - // user never reconnected, end game - roomKVP.Value.Status = GameStatus.NoWin; - await Clients.Group(roomKVP.Key.ToString()).SendAsync("GameEnd", roomKVP.Value.Status); + // room still exists and the game can be restarted + room.Status = GameStatus.Ongoing; + room.Board = new TicTacToeBoard(); // clears the board - GameRooms.Remove(roomKVP.Key); - } - }; + await Clients.Group(room.Id).SendAsync("RestartGame", room); - return Task.CompletedTask; + // have player one start their turn + await Clients.User(room.Player1!.Id).SendAsync("StartTurn", room); + await Clients.User(room.Player2!.Id).SendAsync("EndTurn", room); + } else + { + // the user trying to restart the game needs to be informed that the other user has left + await Clients.Client(Context.ConnectionId).SendAsync("CannotRestart"); + } + } + + [HubMethodName("SendRoomMessage")] + public async Task SendRoomMessage(string roomId, string message) + { + // find the room + var room = _gameRoomService.GameRooms.FirstOrDefault(e => e.Id == roomId); + if (room != null) await Clients.Group(room.Id).SendAsync("ReceiveMessage", message); } } } diff --git a/qtc-net-server/Program.cs b/qtc-net-server/Program.cs index 830a071..9aa0175 100644 --- a/qtc-net-server/Program.cs +++ b/qtc-net-server/Program.cs @@ -14,6 +14,7 @@ global using qtc_api.Hubs; using qtc_api.Services.RoomService; using qtc_api.Services.ContactService; using qtc_api.Services.CurrencyGamesService; +using qtc_api.Services.GameRoomService; var builder = WebApplication.CreateBuilder(args); @@ -58,6 +59,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + builder.Services.AddHostedService(provider => provider.GetService()!); var app = builder.Build(); diff --git a/qtc-net-server/Schema/GameRoom.cs b/qtc-net-server/Schema/GameRoom.cs index 788247f..a26d514 100644 --- a/qtc-net-server/Schema/GameRoom.cs +++ b/qtc-net-server/Schema/GameRoom.cs @@ -4,6 +4,7 @@ namespace qtc_api.Schema { public class GameRoom { + public string Id { get; set; } = string.Empty; public GameStatus Status { get; set; } public TicTacToeBoard Board { get; set; } = new(); public User? Player1 { get; set; } diff --git a/qtc-net-server/Schema/TicTacToeBoard.cs b/qtc-net-server/Schema/TicTacToeBoard.cs index 7408a7d..a923425 100644 --- a/qtc-net-server/Schema/TicTacToeBoard.cs +++ b/qtc-net-server/Schema/TicTacToeBoard.cs @@ -13,5 +13,52 @@ namespace qtc_api.Schema public TicTacToeSymbol Square7 { get; set; } = TicTacToeSymbol.Blank; public TicTacToeSymbol Square8 { get; set; } = TicTacToeSymbol.Blank; public TicTacToeSymbol Square9 { get; set; } = TicTacToeSymbol.Blank; + + public TicTacToeSymbol GetWinner() + { + // winning possibilities + var lines = new List<(TicTacToeSymbol, TicTacToeSymbol, TicTacToeSymbol)> + { + (Square1, Square2, Square3), + (Square4, Square5, Square6), + (Square7, Square8, Square9), + (Square1, Square4, Square7), + (Square2, Square5, Square8), + (Square3, Square6, Square9), + (Square1, Square5, Square9), + (Square3, Square5, Square7) + }; + + foreach(var (a, b, c) in lines) + { + if (a != TicTacToeSymbol.Blank && a == b && b == c) + { + return a; // winner + } + } + + return TicTacToeSymbol.Blank; + } + + public bool IsDraw() + { + // If there is a winner, it's not a draw + if (GetWinner() != TicTacToeSymbol.Blank) + return false; + + // Check if all squares are filled + bool allFilled = + Square1 != TicTacToeSymbol.Blank && + Square2 != TicTacToeSymbol.Blank && + Square3 != TicTacToeSymbol.Blank && + Square4 != TicTacToeSymbol.Blank && + Square5 != TicTacToeSymbol.Blank && + Square6 != TicTacToeSymbol.Blank && + Square7 != TicTacToeSymbol.Blank && + Square8 != TicTacToeSymbol.Blank && + Square9 != TicTacToeSymbol.Blank; + + return allFilled; + } } } diff --git a/qtc-net-server/Services/GameRoomService/GameRoomService.cs b/qtc-net-server/Services/GameRoomService/GameRoomService.cs new file mode 100644 index 0000000..9d8b13e --- /dev/null +++ b/qtc-net-server/Services/GameRoomService/GameRoomService.cs @@ -0,0 +1,12 @@ +using qtc_api.Schema; + +namespace qtc_api.Services.GameRoomService +{ + public class GameRoomService + { + public List GameRooms { get; set; } = []; + + public void AddRoom(GameRoom room) => GameRooms.Add(room); + public void RemoveRoom(GameRoom room) => GameRooms.Remove(room); + } +}