using System.ComponentModel; using Microsoft.AspNetCore.SignalR; namespace PongGame.Hubs; public class PongRoom { public string ID { get; } private readonly ILogger Logger; private const string JOIN_LOG_TEMPLATE = "[{ID}] {Player} joined pong room as player {Number}!"; private const string LEAVE_LOG_TEMPLATE = "[{ID}] Player {Number} {Player} left pong room!"; private const string DIRECTION_LOG_TEMPLATE = "[{ID}] Player {Number} {player} moves paddle in direction: {direction}!"; public PongRoom(string id, ILogger logger) { ID = id; Logger = logger; gameWorker.DoWork += GameLoop; } public PongPlayer? Player1 { get; private set; } public PongPlayer? Player2 { get; private set; } public bool IsEmpty => Player1 is null && Player2 is null; private PongGameState State = PongGameState.Initial; public void Join(PongPlayer player) { // TODO: synchronize this if (Player1 is null) { Player1 = player; Logger.LogInformation(JOIN_LOG_TEMPLATE, ID, player, 1); } else if (Player2 is null) { Player2 = player; Logger.LogInformation(JOIN_LOG_TEMPLATE, ID, player, 1); } else throw new HubException($"Lobby [{ID}] is already full!"); _ = Task.Run(PlayersChanged); player.ConnectedRoom = this; } public void Leave(PongPlayer player) { if (Player1 == player) { Player1 = null; Logger.LogInformation(LEAVE_LOG_TEMPLATE, ID, 1, player); } else if (Player2 == player) { Player2 = null; Logger.LogInformation(LEAVE_LOG_TEMPLATE, ID, 2, player); } player.ConnectedRoom = null; _ = Task.Run(PlayersChanged); } private readonly BackgroundWorker gameWorker = new() { WorkerSupportsCancellation = true }; private void GameLoop(object? sender, DoWorkEventArgs e) { while (!gameWorker.CancellationPending && Player1 is PongPlayer p1 && Player2 is PongPlayer p2 && State.Status is GameStatus.InProgress) { PongGameState.Update(ref State); _ = Task.Run(() => Task.WhenAll( p1.Client.ReceiveGameState(State), p2.Client.ReceiveGameState(State))); Thread.Sleep(1000 / 60); } } private Task PlayersChanged() { if (Player1 is PongPlayer player1 && Player2 is PongPlayer player2) { Logger.LogInformation("[{ID}] Pong game started: {player1} vs. {player2}", ID, player1, player2); ResumeGame(); } else if (IsEmpty) { CloseRoom(); } else PauseGame(); return Task.CompletedTask; } private void ResumeGame() { State.Status = GameStatus.InProgress; if (!gameWorker.IsBusy) gameWorker.RunWorkerAsync(); } private void PauseGame() => State.Status = GameStatus.Paused; private void CloseRoom() => State.Status = GameStatus.Finished; public override string ToString() => $"[{ID}]"; public Task MovePaddle(PongPlayer player, PongPaddleDirection direction) { if (Player1 == player) { State.Paddle1.Direction = direction; Logger.LogDebug(DIRECTION_LOG_TEMPLATE, ID, 1, player, direction); } else if (Player2 == player) { State.Paddle2.Direction = direction; Logger.LogDebug(DIRECTION_LOG_TEMPLATE, ID, 2, player, direction); } else throw new InvalidOperationException("Player is not in this room, but moved! Assumably players room wasn't deleted."); return Task.CompletedTask; } }