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:
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
{
|
{
|
||||||
1 => PlaceResult.OccupiedRed,
|
int currentValue = CurrentField[y, column];
|
||||||
2 => PlaceResult.OccupiedYellow,
|
|
||||||
_ => PlaceResult.InvalidFieldValue
|
|
||||||
};
|
|
||||||
|
|
||||||
|
if (currentValue == 0)
|
||||||
|
{
|
||||||
CreateSave();
|
CreateSave();
|
||||||
CurrentField[coordinates.Y, coordinates.X] = player;
|
CurrentField[y, column] = player;
|
||||||
|
return DropResult.Placed;
|
||||||
|
}
|
||||||
|
|
||||||
return PlaceResult.Placed;
|
if (currentValue != 1 && currentValue != 2)
|
||||||
|
return DropResult.InvalidFieldValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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,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);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user