TicTacToe Game Backend (WIP)

Implement `JoinRoomGuest` For Non-Authorization Communication
This commit is contained in:
Alan Moon 2025-07-02 13:58:07 -07:00
parent 8dcd38cc83
commit 0508db6742
8 changed files with 333 additions and 6 deletions

View File

@ -0,0 +1,12 @@
namespace qtc_api.Enums
{
public enum GameStatus
{
WaitingForPlayer,
Ongoing,
P1Win,
P2Win,
NoWin,
PlayerDisconnected
}
}

View File

@ -0,0 +1,9 @@
namespace qtc_api.Enums
{
public enum TicTacToeSymbol
{
X,
O,
Blank
}
}

View File

@ -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<ChatHub> _logger;
private static List<UserConnectionDto> ConnectedUsers = new();
private static List<User> OnlineUsers = new();
private static Dictionary<string, List<User>> GroupUsers = new();
public ChatHub(IUserService userService, ILogger<ChatHub> logger)
public ChatHub(IUserService userService, IRoomService roomService, ILogger<ChatHub> 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

View File

@ -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<TicTacToeHub> _logger;
private Dictionary<Guid, GameRoom> GameRooms = [];
public TicTacToeHub(ILogger<TicTacToeHub> 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<Guid, GameRoom> 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;
}
}
}

View File

@ -71,5 +71,6 @@ app.UseAuthorization();
app.MapControllers();
app.MapHub<ChatHub>("/chat");
app.MapHub<TicTacToeHub>("/tttgame");
app.Run();

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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; }
}
}