diff --git a/qtc-net-server/Enums/GameStatus.cs b/qtc-net-server/Enums/GameStatus.cs new file mode 100644 index 0000000..46f1769 --- /dev/null +++ b/qtc-net-server/Enums/GameStatus.cs @@ -0,0 +1,12 @@ +namespace qtc_api.Enums +{ + public enum GameStatus + { + WaitingForPlayer, + Ongoing, + P1Win, + P2Win, + NoWin, + PlayerDisconnected + } +} diff --git a/qtc-net-server/Enums/TicTacToeSymbol.cs b/qtc-net-server/Enums/TicTacToeSymbol.cs new file mode 100644 index 0000000..4576a51 --- /dev/null +++ b/qtc-net-server/Enums/TicTacToeSymbol.cs @@ -0,0 +1,9 @@ +namespace qtc_api.Enums +{ + public enum TicTacToeSymbol + { + X, + O, + Blank + } +} diff --git a/qtc-net-server/Hubs/ChatHub.cs b/qtc-net-server/Hubs/ChatHub.cs index 75b2eec..4cb1dff 100644 --- a/qtc-net-server/Hubs/ChatHub.cs +++ b/qtc-net-server/Hubs/ChatHub.cs @@ -1,20 +1,22 @@ -using System.Text.Json; +using qtc_api.Services.RoomService; +using System.Text.Json; namespace qtc_api.Hubs { - [Authorize] public class ChatHub : Hub { private IUserService _userService; + private IRoomService _roomService; private ILogger _logger; private static List ConnectedUsers = new(); private static List OnlineUsers = new(); private static Dictionary> GroupUsers = new(); - public ChatHub(IUserService userService, ILogger logger) + public ChatHub(IUserService userService, IRoomService roomService, ILogger logger) { _userService = userService; + _roomService = roomService; _logger = logger; } @@ -35,8 +37,6 @@ namespace qtc_api.Hubs await LogoutAsync(user); } } - - await base.OnDisconnectedAsync(ex); } public async override Task OnConnectedAsync() @@ -55,11 +55,24 @@ namespace qtc_api.Hubs await LoginAsync(user.Data); } } + } - await base.OnConnectedAsync(); + [HubMethodName("JoinRoomGuest")] + public async Task JoinRoomGuestAsync(string roomId, string username) + { + // here we can just add the client to the room group and call it a day since the user isn't authenticated + var room = await _roomService.GetRoom(roomId); + + if(room != null && room.Success && room.Data != null) + { + await Groups.AddToGroupAsync(Context.ConnectionId, room.Data.Id); + + await Clients.Group(room.Data.Id).SendAsync("GuestJoin", username); + } } [HubMethodName("UpdateStatus")] + [Authorize] public async Task UpdateStatusAsync(User user, int status) { var statusDto = new UserStatusDto { Id = user.Id, Status = status }; @@ -72,6 +85,7 @@ namespace qtc_api.Hubs } [HubMethodName("JoinLobby")] + [Authorize] public async Task JoinLobbyAsync(User user) { await Groups.AddToGroupAsync(Context.ConnectionId, "LOBBY"); @@ -86,6 +100,7 @@ namespace qtc_api.Hubs } [HubMethodName("LeaveLobby")] + [Authorize] public async Task LeaveLobbyAsync(User user) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, "LOBBY"); @@ -99,6 +114,7 @@ namespace qtc_api.Hubs } [HubMethodName("JoinRoom")] + [Authorize] public async Task JoinRoomAsync(User user, Room room) { await Groups.AddToGroupAsync(Context.ConnectionId, room.Id); @@ -113,6 +129,7 @@ namespace qtc_api.Hubs } [HubMethodName("LeaveRoom")] + [Authorize] public async Task LeaveRoomAsync(User user, Room room) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, room.Id); @@ -126,6 +143,7 @@ namespace qtc_api.Hubs } [HubMethodName("HandleDeletedRoom")] + [Authorize] public async Task HandleDeletedRoomAsync(Room room) { await Clients.Group(room.Id).SendAsync("RoomMessage", $"[SERVER] This Room Has Been Deleted By An Administrator."); @@ -135,6 +153,7 @@ namespace qtc_api.Hubs } [HubMethodName("RefreshContactsListOnUser")] + [Authorize] public async Task RefreshContactsListForUser(UserInformationDto user, User execUser) { var connection = ConnectedUsers.FirstOrDefault(e => e.ConnectedUser.Id == user.Id); @@ -148,6 +167,7 @@ namespace qtc_api.Hubs } [HubMethodName("SendMessage")] + [Authorize] public async Task SendMessageAsync(User user, Message message, bool IsLobbyMsg, Room room = null!) { if(IsLobbyMsg == true) { await Clients.Group("LOBBY").SendAsync("RoomMessage", $"[{user.Username}] {message.Content}"); return; } @@ -155,6 +175,7 @@ namespace qtc_api.Hubs } [HubMethodName("SendDirectMessage")] + [Authorize] public async Task SendDirectMessageAsync(User user, UserInformationDto userToMsg, Message message) { // send direct message directly to connected user diff --git a/qtc-net-server/Hubs/TicTacToeHub.cs b/qtc-net-server/Hubs/TicTacToeHub.cs new file mode 100644 index 0000000..0849556 --- /dev/null +++ b/qtc-net-server/Hubs/TicTacToeHub.cs @@ -0,0 +1,243 @@ +using qtc_api.Enums; +using qtc_api.Models; +using qtc_api.Schema; +using System.Runtime.CompilerServices; +using System.Timers; + +namespace qtc_api.Hubs +{ + [Authorize] + public class TicTacToeHub : Hub + { + private IUserService _userService; + private ILogger _logger; + + private Dictionary GameRooms = []; + public TicTacToeHub(ILogger logger, IUserService userService) + { + _userService = userService; + _logger = logger; + } + + public async override Task OnConnectedAsync() + { + // 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)) + { + // get first available room waiting for a player + var roomKVP = GameRooms.FirstOrDefault(e => (e.Value.Status == GameStatus.WaitingForPlayer) || (e.Value.Status == GameStatus.PlayerDisconnected)); + + var room = roomKVP.Value; + var roomId = roomKVP.Key.ToString(); + + if (room != null && room.Status == GameStatus.WaitingForPlayer) + { + // get user from user identifier + var user = await _userService.GetUserById(Context.UserIdentifier ?? ""); + + if (user != null && user.Success && user.Data != null) + { + // set player 2 + room.Player2 = user.Data; + + // 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"); + } + } + } + + public async override Task OnDisconnectedAsync(Exception? exception) + { + // 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)); + + if(roomKVP.Value != null) + { + var room = roomKVP.Value; + var roomId = roomKVP.Key.ToString(); + + // 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 room status to discconnect + room.Status = GameStatus.PlayerDisconnected; + + // schedule the game to be ended if player does not return + await ScheduleGameEnd(roomKVP, Context.UserIdentifier); + } + } + + [HubMethodName("SetSymbol")] + public async Task SetPlayerSymbol(User user, 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; + + 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) + { + case TicTacToeSymbol.X: + room.P2Symbol = TicTacToeSymbol.O; + break; + case TicTacToeSymbol.O: + room.P2Symbol = TicTacToeSymbol.X; + break; + } + + // the game can now start + await Clients.Group(roomId).SendAsync("GameStart"); + } + } + + [HubMethodName("MakeMove")] + public async Task MakeMoveAsync(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; + + 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) + { + switch(move.Point) + { + case 1: + room.Board.Square1 = room.P1Symbol; + break; + case 2: + room.Board.Square2 = room.P1Symbol; + break; + case 3: + room.Board.Square3 = room.P1Symbol; + break; + case 4: + room.Board.Square4 = room.P1Symbol; + break; + case 5: + room.Board.Square5 = room.P1Symbol; + break; + case 6: + room.Board.Square6 = room.P1Symbol; + break; + case 7: + room.Board.Square7 = room.P1Symbol; + break; + case 8: + room.Board.Square8 = room.P1Symbol; + break; + case 9: + room.Board.Square9 = room.P1Symbol; + break; + } + } else + { + switch (move.Point) + { + case 1: + room.Board.Square1 = room.P2Symbol; + break; + case 2: + room.Board.Square2 = room.P2Symbol; + break; + case 3: + room.Board.Square3 = room.P2Symbol; + break; + case 4: + room.Board.Square4 = room.P2Symbol; + break; + case 5: + room.Board.Square5 = room.P2Symbol; + break; + case 6: + room.Board.Square6 = room.P2Symbol; + break; + case 7: + room.Board.Square7 = room.P2Symbol; + break; + case 8: + room.Board.Square8 = room.P2Symbol; + break; + case 9: + room.Board.Square9 = room.P2Symbol; + break; + } + } + + // send board update + await Clients.Group(roomId).SendAsync("UpdateBoard", room.Board); + + // TODO - win logic + } + } + + private Task ScheduleGameEnd(KeyValuePair roomKVP, string? userId) + { + System.Timers.Timer tmr = new System.Timers.Timer(TimeSpan.FromSeconds(60)); + tmr.Start(); + + tmr.Elapsed += async (sender, args) => + { + 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); + + GameRooms.Remove(roomKVP.Key); + } + }; + + return Task.CompletedTask; + } + } +} diff --git a/qtc-net-server/Program.cs b/qtc-net-server/Program.cs index d781424..830a071 100644 --- a/qtc-net-server/Program.cs +++ b/qtc-net-server/Program.cs @@ -71,5 +71,6 @@ app.UseAuthorization(); app.MapControllers(); app.MapHub("/chat"); +app.MapHub("/tttgame"); app.Run(); diff --git a/qtc-net-server/Schema/GameRoom.cs b/qtc-net-server/Schema/GameRoom.cs new file mode 100644 index 0000000..788247f --- /dev/null +++ b/qtc-net-server/Schema/GameRoom.cs @@ -0,0 +1,14 @@ +using qtc_api.Enums; + +namespace qtc_api.Schema +{ + public class GameRoom + { + public GameStatus Status { get; set; } + public TicTacToeBoard Board { get; set; } = new(); + public User? Player1 { get; set; } + public TicTacToeSymbol P1Symbol { get; set; } = TicTacToeSymbol.Blank; + public User? Player2 { get; set; } + public TicTacToeSymbol P2Symbol { get; set; } = TicTacToeSymbol.Blank; + } +} diff --git a/qtc-net-server/Schema/TicTacToeBoard.cs b/qtc-net-server/Schema/TicTacToeBoard.cs new file mode 100644 index 0000000..7408a7d --- /dev/null +++ b/qtc-net-server/Schema/TicTacToeBoard.cs @@ -0,0 +1,17 @@ +using qtc_api.Enums; + +namespace qtc_api.Schema +{ + public class TicTacToeBoard + { + public TicTacToeSymbol Square1 { get; set; } = TicTacToeSymbol.Blank; + public TicTacToeSymbol Square2 { get; set; } = TicTacToeSymbol.Blank; + public TicTacToeSymbol Square3 { get; set; } = TicTacToeSymbol.Blank; + public TicTacToeSymbol Square4 { get; set; } = TicTacToeSymbol.Blank; + public TicTacToeSymbol Square5 { get; set; } = TicTacToeSymbol.Blank; + public TicTacToeSymbol Square6 { get; set; } = TicTacToeSymbol.Blank; + public TicTacToeSymbol Square7 { get; set; } = TicTacToeSymbol.Blank; + public TicTacToeSymbol Square8 { get; set; } = TicTacToeSymbol.Blank; + public TicTacToeSymbol Square9 { get; set; } = TicTacToeSymbol.Blank; + } +} diff --git a/qtc-net-server/Schema/TicTacToeMove.cs b/qtc-net-server/Schema/TicTacToeMove.cs new file mode 100644 index 0000000..35182a6 --- /dev/null +++ b/qtc-net-server/Schema/TicTacToeMove.cs @@ -0,0 +1,10 @@ +using qtc_api.Enums; + +namespace qtc_api.Schema +{ + public class TicTacToeMove + { + public User User { get; set; } = new(); + public int Point { get; set; } + } +}