From e8b45fa2352e35dec1172c44f6428b8d07a10458 Mon Sep 17 00:00:00 2001 From: Jonas <77726472+kobolol@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:26:53 +0100 Subject: [PATCH] Add column drop, game info DTO, and disconnect Introduce column-based drop mechanics and game info transfer; add disconnect handling and related API/interface updates. GameField API renamed Place->Drop and PlaceResult->DropResult, added ColumnFull, IsFull, and updated drop logic to insert at lowest empty row. Game model now exposes Players, adds GetPlayerByTag and makes StartGame public. New GameInformationDto conveys game state and field. Hub methods updated: GameCreated->GameJoined, RequestGameInformation, Place now forwards to Drop, and OnDisconnectedAsync notifies GameManager. GameManager implements RequestGameInformation, Drop (validates moves, broadcasts FieldUpdated and GameEnded on win/draw), improved JoinGame logic to auto-start when two players join, and handles player disconnects. Repository and interface extended with GetOneByConnectionId. Also tightened SignalR timeouts in Program. --- API/Controllers/GameHubSocket.cs | 16 +++- API/Models/Game/Game.cs | 9 ++- API/Models/Game/GameField.cs | 55 +++++++++----- API/Models/Game/GameInformationDto.cs | 10 +++ API/Program.cs | 6 +- API/Repository/GameRepo/GameRepository.cs | 5 ++ API/Repository/GameRepo/IGameRepository.cs | 1 + API/Services/GameManager/GameManager.cs | 87 ++++++++++++++++++++-- API/Services/GameManager/IGameManager.cs | 7 +- 9 files changed, 165 insertions(+), 31 deletions(-) create mode 100644 API/Models/Game/GameInformationDto.cs 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