Finish Tic-Tac-Toe Game Backend
This commit is contained in:
parent
0508db6742
commit
7fb10cc728
@ -3,6 +3,7 @@
|
||||
public enum GameStatus
|
||||
{
|
||||
WaitingForPlayer,
|
||||
SelectingSymbol,
|
||||
Ongoing,
|
||||
P1Win,
|
||||
P2Win,
|
||||
|
@ -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,110 +12,90 @@ namespace qtc_api.Hubs
|
||||
{
|
||||
private IUserService _userService;
|
||||
private ILogger<TicTacToeHub> _logger;
|
||||
|
||||
private Dictionary<Guid, GameRoom> GameRooms = [];
|
||||
public TicTacToeHub(ILogger<TicTacToeHub> logger, IUserService userService)
|
||||
private GameRoomService _gameRoomService;
|
||||
public TicTacToeHub(ILogger<TicTacToeHub> 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));
|
||||
|
||||
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
|
||||
// just end the game
|
||||
room.Status = GameStatus.PlayerDisconnected;
|
||||
|
||||
// schedule the game to be ended if player does not return
|
||||
await ScheduleGameEnd(roomKVP, Context.UserIdentifier);
|
||||
string usernameDisconnected = string.Empty;
|
||||
|
||||
// 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; }
|
||||
|
||||
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);
|
||||
|
||||
// remove the room
|
||||
_gameRoomService.RemoveRoom(room);
|
||||
}
|
||||
}
|
||||
|
||||
[HubMethodName("FindRoom")]
|
||||
public async Task FindRoom()
|
||||
{
|
||||
// find user from useridentifier
|
||||
var user = await _userService.GetUserById(Context.UserIdentifier!);
|
||||
|
||||
if (user != null && user.Success && user.Data != null)
|
||||
{
|
||||
// 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);
|
||||
|
||||
// set player two
|
||||
roomWOP.Player2 = user.Data;
|
||||
|
||||
// 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);
|
||||
|
||||
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)
|
||||
{
|
||||
var roomId = roomKVP.Key.ToString();
|
||||
|
||||
// set player one symbol
|
||||
room.P1Symbol = symbol;
|
||||
|
||||
@ -130,21 +111,23 @@ 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)
|
||||
{
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
private Task ScheduleGameEnd(KeyValuePair<Guid, GameRoom> roomKVP, string? userId)
|
||||
// check for draw
|
||||
if(room.Board.IsDraw())
|
||||
{
|
||||
System.Timers.Timer tmr = new System.Timers.Timer(TimeSpan.FromSeconds(60));
|
||||
tmr.Start();
|
||||
room.Status = GameStatus.NoWin;
|
||||
await Clients.Group(room.Id).SendAsync("GameEnd", room, string.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
tmr.Elapsed += async (sender, args) =>
|
||||
// check for winner
|
||||
var winningSymbol = room.Board.GetWinner();
|
||||
if(winningSymbol != TicTacToeSymbol.Blank)
|
||||
{
|
||||
var client = Clients.User(userId!);
|
||||
if (client == null)
|
||||
if (winningSymbol == room.P1Symbol)
|
||||
{
|
||||
// user never reconnected, end game
|
||||
roomKVP.Value.Status = GameStatus.NoWin;
|
||||
await Clients.Group(roomKVP.Key.ToString()).SendAsync("GameEnd", roomKVP.Value.Status);
|
||||
// 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);
|
||||
}
|
||||
|
||||
GameRooms.Remove(roomKVP.Key);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return Task.CompletedTask;
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HubMethodName("RestartGame")]
|
||||
public async Task RestartGame(string roomId)
|
||||
{
|
||||
// find the room
|
||||
var room = _gameRoomService.GameRooms.FirstOrDefault(e => e.Id == roomId);
|
||||
if(room != null)
|
||||
{
|
||||
// room still exists and the game can be restarted
|
||||
room.Status = GameStatus.Ongoing;
|
||||
room.Board = new TicTacToeBoard(); // clears the board
|
||||
|
||||
await Clients.Group(room.Id).SendAsync("RestartGame", room);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<IRoomService, RoomService>();
|
||||
builder.Services.AddScoped<IContactService, ContactService>();
|
||||
|
||||
builder.Services.AddSingleton<CurrencyGamesService>();
|
||||
builder.Services.AddSingleton<GameRoomService>();
|
||||
|
||||
builder.Services.AddHostedService(provider => provider.GetService<CurrencyGamesService>()!);
|
||||
|
||||
var app = builder.Build();
|
||||
|
@ -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; }
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
12
qtc-net-server/Services/GameRoomService/GameRoomService.cs
Normal file
12
qtc-net-server/Services/GameRoomService/GameRoomService.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using qtc_api.Schema;
|
||||
|
||||
namespace qtc_api.Services.GameRoomService
|
||||
{
|
||||
public class GameRoomService
|
||||
{
|
||||
public List<GameRoom> GameRooms { get; set; } = [];
|
||||
|
||||
public void AddRoom(GameRoom room) => GameRooms.Add(room);
|
||||
public void RemoveRoom(GameRoom room) => GameRooms.Remove(room);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user