diff --git a/API/Controllers/GameHubSocket.cs b/API/Controllers/GameHubSocket.cs index 0842dae..2857139 100644 --- a/API/Controllers/GameHubSocket.cs +++ b/API/Controllers/GameHubSocket.cs @@ -38,15 +38,27 @@ public class GameHubSocket(IGameManager gameManager) : Hub await Groups.AddToGroupAsync(Context.ConnectionId, gameCode.ToString()); - await Clients.Caller.SendAsync("GameCreated", new + await Clients.Caller.SendAsync("GameJoined", new { GameId = result, GameCode = gameCode, }); } - public async Task Place(string gameId, int index) + public async Task RequestGameInformation(string gameId) { + await _gameManager.RequestGameInformation(gameId, Context.ConnectionId); + } + public async Task Place(string gameId, int column) + { + await _gameManager.Drop(gameId, column, Context.ConnectionId); + } + + public override async Task OnDisconnectedAsync(Exception? exception) + { + await _gameManager.DisconnectedPlayer(Context.ConnectionId); + + await base.OnDisconnectedAsync(exception); } } \ No newline at end of file diff --git a/API/Models/Game/Game.cs b/API/Models/Game/Game.cs index e7784d3..43ebb25 100644 --- a/API/Models/Game/Game.cs +++ b/API/Models/Game/Game.cs @@ -13,7 +13,7 @@ public class Game(Coordinates gFs, SixDigitInt gameCode) { public string Id { get; init; } = Guid.NewGuid().ToString(); public SixDigitInt GameCode { get; } = gameCode; - private List Players { get; set; } = new(); + public List Players { get; set; } = new(); public GameState State { get; private set; } = GameState.Lobby; public GameField Field { get; } = new(gFs); @@ -43,7 +43,12 @@ public class Game(Coordinates gFs, SixDigitInt gameCode) return Players.FirstOrDefault(x => x.ConnectionId == playerConnectionId); } - private void StartGame() + public Player? GetPlayerByTag(int playerTag) + { + return Players.FirstOrDefault(x => x.PlayerTag == playerTag); + } + + public void StartGame() { State = GameState.Running; } diff --git a/API/Models/Game/GameField.cs b/API/Models/Game/GameField.cs index e3e144f..e4c332b 100644 --- a/API/Models/Game/GameField.cs +++ b/API/Models/Game/GameField.cs @@ -6,12 +6,11 @@ public readonly struct Coordinates(int x, int y) public readonly int Y = y; } -public enum PlaceResult +public enum DropResult { OutOfGameField, NotAllowedPlayer, - OccupiedRed, - OccupiedYellow, + ColumnFull, InvalidFieldValue, Placed } @@ -27,33 +26,36 @@ public enum FieldState public class GameField(Coordinates gFs) { - private Coordinates _gFs = gFs; + public Coordinates GFs = gFs; public int[,] CurrentField { get; } = new int[gFs.Y, gFs.X]; public int[,] BackupField { get; } = new int[gFs.Y, gFs.X]; - public PlaceResult Place(Coordinates coordinates, int player) + public DropResult Drop(int column, int player) { - if (coordinates.X < 0 || coordinates.X >= CurrentField.GetLength(1) || - coordinates.Y < 0 || coordinates.Y >= CurrentField.GetLength(0)) - return PlaceResult.OutOfGameField; + if (column < 0 || column >= CurrentField.GetLength(1)) + return DropResult.OutOfGameField; if (player != 1 && player != 2) - return PlaceResult.NotAllowedPlayer; + return DropResult.NotAllowedPlayer; - var currentValue = CurrentField[coordinates.Y, coordinates.X]; + int rows = CurrentField.GetLength(0); - if (currentValue != 0) - return currentValue switch + for (int y = rows - 1; y >= 0; y--) + { + int currentValue = CurrentField[y, column]; + + if (currentValue == 0) { - 1 => PlaceResult.OccupiedRed, - 2 => PlaceResult.OccupiedYellow, - _ => PlaceResult.InvalidFieldValue - }; + CreateSave(); + CurrentField[y, column] = player; + return DropResult.Placed; + } - CreateSave(); - CurrentField[coordinates.Y, coordinates.X] = player; + if (currentValue != 1 && currentValue != 2) + return DropResult.InvalidFieldValue; + } - return PlaceResult.Placed; + return DropResult.ColumnFull; } public FieldState CheckField(Coordinates coordinates) @@ -73,6 +75,21 @@ public class GameField(Coordinates gFs) }; } + public bool IsFull() + { + int rows = CurrentField.GetLength(0); + int cols = CurrentField.GetLength(1); + for (int y = 0; y < rows; y++) + { + for (int x = 0; x < cols; x++) + { + if (CurrentField[y, x] == 0) + return false; + } + } + return true; + } + private int CountInDirection(int startX, int startY, int dx, int dy, int player) { var count = 0; diff --git a/API/Models/Game/GameInformationDto.cs b/API/Models/Game/GameInformationDto.cs new file mode 100644 index 0000000..d3dfa51 --- /dev/null +++ b/API/Models/Game/GameInformationDto.cs @@ -0,0 +1,10 @@ +namespace API.Models.Game +{ + public class GameInformationDto + { + public string Id { get; set; } + public List Players { get; set; } + public GameState? State { get; set; } + public int[,] CurrentField { get; set; } + } +} diff --git a/API/Program.cs b/API/Program.cs index fe14ad8..071c794 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -7,7 +7,11 @@ var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); -builder.Services.AddSignalR(); +builder.Services.AddSignalR(options => +{ + options.ClientTimeoutInterval = TimeSpan.FromSeconds(5); + options.KeepAliveInterval = TimeSpan.FromSeconds(2); +}); builder.Services.AddOpenApi(); builder.Services.AddSwaggerGen(); diff --git a/API/Repository/GameRepo/GameRepository.cs b/API/Repository/GameRepo/GameRepository.cs index 536257d..5d97322 100644 --- a/API/Repository/GameRepo/GameRepository.cs +++ b/API/Repository/GameRepo/GameRepository.cs @@ -22,6 +22,11 @@ public class GameRepository : IGameRepository return _games.FirstOrDefault(g => g.GameCode == gameCode); } + public Game? GetOneByConnectionId(string connectionId) + { + return _games.FirstOrDefault(g => g.Players.Any(p => p.ConnectionId == connectionId)); + } + public Game Create(Coordinates gameFieldSize) { Game newGame = new(gameFieldSize, GenerateGameCode()); diff --git a/API/Repository/GameRepo/IGameRepository.cs b/API/Repository/GameRepo/IGameRepository.cs index db508cc..b0cb8e6 100644 --- a/API/Repository/GameRepo/IGameRepository.cs +++ b/API/Repository/GameRepo/IGameRepository.cs @@ -8,6 +8,7 @@ public interface IGameRepository public List GetAll(); public Game? GetOne(string id); public Game? GetOne(SixDigitInt gameCode); + public Game? GetOneByConnectionId(string connectionId); public Game Create(Coordinates gameFieldSize); public void Destroy(string id); } \ No newline at end of file diff --git a/API/Services/GameManager/GameManager.cs b/API/Services/GameManager/GameManager.cs index b1346eb..c596f8a 100644 --- a/API/Services/GameManager/GameManager.cs +++ b/API/Services/GameManager/GameManager.cs @@ -19,16 +19,93 @@ public class GameManager(IGameRepository gameRepository, IHubContext Place(string gameCode, int coordinates, string playerConnectionId) + public async Task RequestGameInformation(string gameId, string playerConnectionId) { - return true; + var game = gameRepository.GetOne(gameId); + if (game == null) + return; + + var gameInfoDto = new GameInformationDto + { + Players = game?.Players, + CurrentField = game?.Field.CurrentField, + State = game?.State + }; + + await hubContext.Clients.Client(playerConnectionId).SendAsync("GameInformation", gameInfoDto); + } + + public async Task Drop(string gameCode, int column, string playerConnectionId) + { + var game = gameRepository.GetOne(gameCode); + + var player = game?.GetPlayerByConnectionId(playerConnectionId); + if (player == null) + return; + + var result = game.Field.Drop(column, player.PlayerTag); + + if (result != DropResult.Placed) + { + await hubContext.Clients.Client(playerConnectionId).SendAsync("Error", "Ungültiger Zug."); + return; + } + + await hubContext.Clients.Group(game.Id).SendAsync("FieldUpdated", game.Field.CurrentField); + + var winResult = game.Field.CheckForWin(); + if (winResult != 0) + { + var winPlayer = game.GetPlayerByTag(winResult); + game.EndGame(); + await hubContext.Clients.Group(game.Id).SendAsync("GameEnded", new + { + Method = "Win", + Player = winPlayer + }); + } + + if (game.Field.IsFull()) + { + game.EndGame(); + + await hubContext.Clients.Group(game.Id).SendAsync("GameEnded", new + { + Method = "Draw" + }); + } + } + + public Task DisconnectedPlayer(string playerConnectionId) + { + var game = gameRepository.GetOneByConnectionId(playerConnectionId); + if (game == null) + return Task.CompletedTask; + + var player = game.GetPlayerByConnectionId(playerConnectionId); + if (player == null) + return Task.CompletedTask; + + game.RemovePlayer(playerConnectionId); + game.EndGame(); + return hubContext.Clients.Group(game.Id).SendAsync("GameEnded", new + { + Method = "PlayerDisconnected", + Player = player + }); } } \ No newline at end of file diff --git a/API/Services/GameManager/IGameManager.cs b/API/Services/GameManager/IGameManager.cs index 47bffe3..7da32a7 100644 --- a/API/Services/GameManager/IGameManager.cs +++ b/API/Services/GameManager/IGameManager.cs @@ -5,6 +5,9 @@ namespace API.Services.GameManager; public interface IGameManager { public (string, int) CreateGame(Coordinates gFs, Player player); - public Task JoinGame(Player player, int gameCode) - public Task Place(string gameCode, int coordinates, string playerConnectionId) + public Task JoinGame(Player player, int gameCode); + public Task RequestGameInformation(string gameId, string playerConnectionId); + public Task Drop(string gameCode, int coordinates, string playerConnectionId); + + public Task DisconnectedPlayer(string playerConnectionId); } \ No newline at end of file