Finish Tic-Tac-Toe Game Backend
This commit is contained in:
parent
0508db6742
commit
7fb10cc728
@ -3,6 +3,7 @@
|
|||||||
public enum GameStatus
|
public enum GameStatus
|
||||||
{
|
{
|
||||||
WaitingForPlayer,
|
WaitingForPlayer,
|
||||||
|
SelectingSymbol,
|
||||||
Ongoing,
|
Ongoing,
|
||||||
P1Win,
|
P1Win,
|
||||||
P2Win,
|
P2Win,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using qtc_api.Enums;
|
using qtc_api.Enums;
|
||||||
using qtc_api.Models;
|
using qtc_api.Models;
|
||||||
using qtc_api.Schema;
|
using qtc_api.Schema;
|
||||||
|
using qtc_api.Services.GameRoomService;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Timers;
|
using System.Timers;
|
||||||
|
|
||||||
@ -11,115 +12,95 @@ namespace qtc_api.Hubs
|
|||||||
{
|
{
|
||||||
private IUserService _userService;
|
private IUserService _userService;
|
||||||
private ILogger<TicTacToeHub> _logger;
|
private ILogger<TicTacToeHub> _logger;
|
||||||
|
private GameRoomService _gameRoomService;
|
||||||
private Dictionary<Guid, GameRoom> GameRooms = [];
|
public TicTacToeHub(ILogger<TicTacToeHub> logger, IUserService userService, GameRoomService gameRoomService)
|
||||||
public TicTacToeHub(ILogger<TicTacToeHub> logger, IUserService userService)
|
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
|
_gameRoomService = gameRoomService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async override Task OnConnectedAsync()
|
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||||
{
|
{
|
||||||
// tell the client that the server is finding a room
|
// find any existing room user was in
|
||||||
await Clients.Client(Context.ConnectionId).SendAsync("FindingRoom");
|
var room = _gameRoomService.GameRooms.FirstOrDefault(e => (e.Player1?.Id == Context.UserIdentifier) || (e.Player2?.Id == Context.UserIdentifier));
|
||||||
|
if (room != null)
|
||||||
// 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
|
// just end the game
|
||||||
var roomKVP = GameRooms.FirstOrDefault(e => (e.Value.Status == GameStatus.WaitingForPlayer) || (e.Value.Status == GameStatus.PlayerDisconnected));
|
room.Status = GameStatus.PlayerDisconnected;
|
||||||
|
|
||||||
var room = roomKVP.Value;
|
string usernameDisconnected = string.Empty;
|
||||||
var roomId = roomKVP.Key.ToString();
|
|
||||||
|
|
||||||
if (room != null && room.Status == GameStatus.WaitingForPlayer)
|
// determine which player to null out
|
||||||
{
|
if (room.Player1?.Id == Context.UserIdentifier) { usernameDisconnected = room.Player1!.Username; room.Player1 = null; }
|
||||||
// get user from user identifier
|
else { usernameDisconnected = room.Player2!.Username; room.Player2 = null; }
|
||||||
var user = await _userService.GetUserById(Context.UserIdentifier ?? "");
|
|
||||||
|
|
||||||
if (user != null && user.Success && user.Data != null)
|
bool isWin = room.Status == GameStatus.P1Win || room.Status == GameStatus.P2Win || room.Status == GameStatus.NoWin;
|
||||||
{
|
if(!isWin)
|
||||||
// set player 2
|
await Clients.Group(room.Id).SendAsync("GameEnd", room, usernameDisconnected);
|
||||||
room.Player2 = user.Data;
|
|
||||||
|
|
||||||
// add player to room
|
// remove the room
|
||||||
await Groups.AddToGroupAsync(Context.ConnectionId, roomId);
|
_gameRoomService.RemoveRoom(room);
|
||||||
|
|
||||||
// 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)
|
[HubMethodName("FindRoom")]
|
||||||
|
public async Task FindRoom()
|
||||||
{
|
{
|
||||||
// find the game room the user is in
|
// find user from useridentifier
|
||||||
var roomKVP = GameRooms.FirstOrDefault(e => (e.Value.Player1?.Id == Context.UserIdentifier) || (e.Value.Player2?.Id == Context.UserIdentifier));
|
var user = await _userService.GetUserById(Context.UserIdentifier!);
|
||||||
|
|
||||||
if(roomKVP.Value != null)
|
if (user != null && user.Success && user.Data != null)
|
||||||
{
|
{
|
||||||
var room = roomKVP.Value;
|
// find any room waiting on a player
|
||||||
var roomId = roomKVP.Key.ToString();
|
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
|
// set player two
|
||||||
if (room.Player1?.Id == Context.UserIdentifier) await Clients.Group(roomId).SendAsync("HostDisconnected", exception);
|
roomWOP.Player2 = user.Data;
|
||||||
else await Clients.Group(roomId).SendAsync("PlayerDisconnected", exception);
|
|
||||||
|
|
||||||
// set room status to discconnect
|
// this game can now start symbol selection
|
||||||
room.Status = GameStatus.PlayerDisconnected;
|
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
|
return;
|
||||||
await ScheduleGameEnd(roomKVP, Context.UserIdentifier);
|
}
|
||||||
|
|
||||||
|
// 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")]
|
[HubMethodName("SetSymbol")]
|
||||||
public async Task SetPlayerSymbol(User user, TicTacToeSymbol symbol)
|
public async Task SetPlayerSymbol(string roomId, TicTacToeSymbol symbol)
|
||||||
{
|
{
|
||||||
// find the room
|
// find the room
|
||||||
var roomKVP = GameRooms.FirstOrDefault(e => (e.Value.Player1?.Id == user.Id) || (e.Value.Player2?.Id == user.Id));
|
var room = _gameRoomService.GameRooms.FirstOrDefault(e => e.Id == roomId);
|
||||||
var room = roomKVP.Value;
|
|
||||||
|
|
||||||
if(room != null)
|
if (room != null)
|
||||||
{
|
{
|
||||||
var roomId = roomKVP.Key.ToString();
|
|
||||||
|
|
||||||
// set player one symbol
|
// set player one symbol
|
||||||
room.P1Symbol = symbol;
|
room.P1Symbol = symbol;
|
||||||
|
|
||||||
// set player two symbol based on what player one picked
|
// set player two symbol based on what player one picked
|
||||||
switch(room.P1Symbol)
|
switch (room.P1Symbol)
|
||||||
{
|
{
|
||||||
case TicTacToeSymbol.X:
|
case TicTacToeSymbol.X:
|
||||||
room.P2Symbol = TicTacToeSymbol.O;
|
room.P2Symbol = TicTacToeSymbol.O;
|
||||||
@ -130,25 +111,27 @@ namespace qtc_api.Hubs
|
|||||||
}
|
}
|
||||||
|
|
||||||
// the game can now start
|
// 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")]
|
[HubMethodName("MakeMove")]
|
||||||
public async Task MakeMoveAsync(TicTacToeMove move)
|
public async Task MakeMove(string roomId, TicTacToeMove move)
|
||||||
{
|
{
|
||||||
// find the room
|
// find the room
|
||||||
var roomKVP = GameRooms.FirstOrDefault(e => (e.Value.Player1?.Id == move.User.Id) || (e.Value.Player2?.Id == move.User.Id));
|
var room = _gameRoomService.GameRooms.FirstOrDefault(e => e.Id == roomId);
|
||||||
var room = roomKVP.Value;
|
|
||||||
|
|
||||||
if (room != null)
|
if (room != null)
|
||||||
{
|
{
|
||||||
var roomId = roomKVP.Key.ToString();
|
|
||||||
|
|
||||||
// update board based on move (TOOD - figure out a better way to do this)
|
// 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:
|
case 1:
|
||||||
room.Board.Square1 = room.P1Symbol;
|
room.Board.Square1 = room.P1Symbol;
|
||||||
@ -178,7 +161,8 @@ namespace qtc_api.Hubs
|
|||||||
room.Board.Square9 = room.P1Symbol;
|
room.Board.Square9 = room.P1Symbol;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
switch (move.Point)
|
switch (move.Point)
|
||||||
{
|
{
|
||||||
@ -213,31 +197,78 @@ namespace qtc_api.Hubs
|
|||||||
}
|
}
|
||||||
|
|
||||||
// send board update
|
// 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<Guid, GameRoom> roomKVP, string? userId)
|
[HubMethodName("RestartGame")]
|
||||||
|
public async Task RestartGame(string roomId)
|
||||||
{
|
{
|
||||||
System.Timers.Timer tmr = new System.Timers.Timer(TimeSpan.FromSeconds(60));
|
// find the room
|
||||||
tmr.Start();
|
var room = _gameRoomService.GameRooms.FirstOrDefault(e => e.Id == roomId);
|
||||||
|
if(room != null)
|
||||||
tmr.Elapsed += async (sender, args) =>
|
|
||||||
{
|
{
|
||||||
var client = Clients.User(userId!);
|
// room still exists and the game can be restarted
|
||||||
if (client == null)
|
room.Status = GameStatus.Ongoing;
|
||||||
{
|
room.Board = new TicTacToeBoard(); // clears the board
|
||||||
// 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);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ global using qtc_api.Hubs;
|
|||||||
using qtc_api.Services.RoomService;
|
using qtc_api.Services.RoomService;
|
||||||
using qtc_api.Services.ContactService;
|
using qtc_api.Services.ContactService;
|
||||||
using qtc_api.Services.CurrencyGamesService;
|
using qtc_api.Services.CurrencyGamesService;
|
||||||
|
using qtc_api.Services.GameRoomService;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@ -58,6 +59,8 @@ builder.Services.AddScoped<IRoomService, RoomService>();
|
|||||||
builder.Services.AddScoped<IContactService, ContactService>();
|
builder.Services.AddScoped<IContactService, ContactService>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<CurrencyGamesService>();
|
builder.Services.AddSingleton<CurrencyGamesService>();
|
||||||
|
builder.Services.AddSingleton<GameRoomService>();
|
||||||
|
|
||||||
builder.Services.AddHostedService(provider => provider.GetService<CurrencyGamesService>()!);
|
builder.Services.AddHostedService(provider => provider.GetService<CurrencyGamesService>()!);
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
@ -4,6 +4,7 @@ namespace qtc_api.Schema
|
|||||||
{
|
{
|
||||||
public class GameRoom
|
public class GameRoom
|
||||||
{
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
public GameStatus Status { get; set; }
|
public GameStatus Status { get; set; }
|
||||||
public TicTacToeBoard Board { get; set; } = new();
|
public TicTacToeBoard Board { get; set; } = new();
|
||||||
public User? Player1 { get; set; }
|
public User? Player1 { get; set; }
|
||||||
|
@ -13,5 +13,52 @@ namespace qtc_api.Schema
|
|||||||
public TicTacToeSymbol Square7 { get; set; } = TicTacToeSymbol.Blank;
|
public TicTacToeSymbol Square7 { get; set; } = TicTacToeSymbol.Blank;
|
||||||
public TicTacToeSymbol Square8 { get; set; } = TicTacToeSymbol.Blank;
|
public TicTacToeSymbol Square8 { get; set; } = TicTacToeSymbol.Blank;
|
||||||
public TicTacToeSymbol Square9 { 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