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.
This commit is contained in:
Jonas
2026-03-03 20:26:53 +01:00
parent 955d1e18c6
commit e8b45fa235
9 changed files with 165 additions and 31 deletions
+14 -2
View File
@@ -38,15 +38,27 @@ public class GameHubSocket(IGameManager gameManager) : Hub
await Groups.AddToGroupAsync(Context.ConnectionId, gameCode.ToString()); await Groups.AddToGroupAsync(Context.ConnectionId, gameCode.ToString());
await Clients.Caller.SendAsync("GameCreated", new await Clients.Caller.SendAsync("GameJoined", new
{ {
GameId = result, GameId = result,
GameCode = gameCode, 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);
} }
} }
+7 -2
View File
@@ -13,7 +13,7 @@ public class Game(Coordinates gFs, SixDigitInt gameCode)
{ {
public string Id { get; init; } = Guid.NewGuid().ToString(); public string Id { get; init; } = Guid.NewGuid().ToString();
public SixDigitInt GameCode { get; } = gameCode; public SixDigitInt GameCode { get; } = gameCode;
private List<Player> Players { get; set; } = new(); public List<Player> Players { get; set; } = new();
public GameState State { get; private set; } = GameState.Lobby; public GameState State { get; private set; } = GameState.Lobby;
public GameField Field { get; } = new(gFs); public GameField Field { get; } = new(gFs);
@@ -43,7 +43,12 @@ public class Game(Coordinates gFs, SixDigitInt gameCode)
return Players.FirstOrDefault(x => x.ConnectionId == playerConnectionId); 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; State = GameState.Running;
} }
+36 -19
View File
@@ -6,12 +6,11 @@ public readonly struct Coordinates(int x, int y)
public readonly int Y = y; public readonly int Y = y;
} }
public enum PlaceResult public enum DropResult
{ {
OutOfGameField, OutOfGameField,
NotAllowedPlayer, NotAllowedPlayer,
OccupiedRed, ColumnFull,
OccupiedYellow,
InvalidFieldValue, InvalidFieldValue,
Placed Placed
} }
@@ -27,33 +26,36 @@ public enum FieldState
public class GameField(Coordinates gFs) 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[,] CurrentField { get; } = new int[gFs.Y, gFs.X];
public int[,] BackupField { 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) || if (column < 0 || column >= CurrentField.GetLength(1))
coordinates.Y < 0 || coordinates.Y >= CurrentField.GetLength(0)) return DropResult.OutOfGameField;
return PlaceResult.OutOfGameField;
if (player != 1 && player != 2) 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) for (int y = rows - 1; y >= 0; y--)
return currentValue switch {
int currentValue = CurrentField[y, column];
if (currentValue == 0)
{ {
1 => PlaceResult.OccupiedRed, CreateSave();
2 => PlaceResult.OccupiedYellow, CurrentField[y, column] = player;
_ => PlaceResult.InvalidFieldValue return DropResult.Placed;
}; }
CreateSave(); if (currentValue != 1 && currentValue != 2)
CurrentField[coordinates.Y, coordinates.X] = player; return DropResult.InvalidFieldValue;
}
return PlaceResult.Placed; return DropResult.ColumnFull;
} }
public FieldState CheckField(Coordinates coordinates) 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) private int CountInDirection(int startX, int startY, int dx, int dy, int player)
{ {
var count = 0; var count = 0;
+10
View File
@@ -0,0 +1,10 @@
namespace API.Models.Game
{
public class GameInformationDto
{
public string Id { get; set; }
public List<Player> Players { get; set; }
public GameState? State { get; set; }
public int[,] CurrentField { get; set; }
}
}
+5 -1
View File
@@ -7,7 +7,11 @@ var builder = WebApplication.CreateBuilder(args);
// Add services to the container. // Add services to the container.
builder.Services.AddControllers(); 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.AddOpenApi();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();
@@ -22,6 +22,11 @@ public class GameRepository : IGameRepository
return _games.FirstOrDefault(g => g.GameCode == gameCode); 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) public Game Create(Coordinates gameFieldSize)
{ {
Game newGame = new(gameFieldSize, GenerateGameCode()); Game newGame = new(gameFieldSize, GenerateGameCode());
@@ -8,6 +8,7 @@ public interface IGameRepository
public List<Game> GetAll(); public List<Game> GetAll();
public Game? GetOne(string id); public Game? GetOne(string id);
public Game? GetOne(SixDigitInt gameCode); public Game? GetOne(SixDigitInt gameCode);
public Game? GetOneByConnectionId(string connectionId);
public Game Create(Coordinates gameFieldSize); public Game Create(Coordinates gameFieldSize);
public void Destroy(string id); public void Destroy(string id);
} }
+82 -5
View File
@@ -19,16 +19,93 @@ public class GameManager(IGameRepository gameRepository, IHubContext<GameHubSock
{ {
var game = gameRepository.GetOne(new SixDigitInt(gameCode)); var game = gameRepository.GetOne(new SixDigitInt(gameCode));
var success = game != null && game.AddPlayer(player); var success = game != null && game.State == GameState.Lobby && game.AddPlayer(player);
if (game!.State == GameState.Running) if (!success) return null;
await hubContext.Clients.Group(game.Id).SendAsync("GameStarted");
if (game.Players.Count == 2)
{
game.StartGame();
await hubContext.Clients.Group(game.Id).SendAsync("GameStarted", game.Players);
}
return game.Id; return game.Id;
} }
public async Task<bool> 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
});
} }
} }
+5 -2
View File
@@ -5,6 +5,9 @@ namespace API.Services.GameManager;
public interface IGameManager public interface IGameManager
{ {
public (string, int) CreateGame(Coordinates gFs, Player player); public (string, int) CreateGame(Coordinates gFs, Player player);
public Task<string?> JoinGame(Player player, int gameCode) public Task<string?> JoinGame(Player player, int gameCode);
public Task<bool> Place(string gameCode, int coordinates, string playerConnectionId) public Task RequestGameInformation(string gameId, string playerConnectionId);
public Task Drop(string gameCode, int coordinates, string playerConnectionId);
public Task DisconnectedPlayer(string playerConnectionId);
} }