Implemented basic Pong lobby system

This commit is contained in:
2022-11-03 12:29:35 +01:00
parent 1254e32749
commit 462e27c88a
24 changed files with 5921 additions and 3 deletions

View File

@ -0,0 +1,7 @@
namespace PongGame;
public enum GameState {
WaitingForPlayers,
InProgress,
Finished,
}

View File

@ -0,0 +1,6 @@
namespace PongGame.Hubs;
public interface IPongClient {
Task GameStateChanged(GameState state);
Task UsernameChanged(string value);
}

61
PongGame/Hubs/PongHub.cs Normal file
View File

@ -0,0 +1,61 @@
using Microsoft.AspNetCore.SignalR;
namespace PongGame.Hubs;
public class PongHub : Hub<IPongClient> {
private const string PLAYER_KEY = "PLAYER";
private readonly PongLobby Lobby;
private readonly ILogger<PongHub> Logger;
public PongHub(PongLobby lobby, ILogger<PongHub> logger) : base() {
Lobby = lobby;
Logger = logger;
}
private PongPlayer Player {
get => (Context.Items.TryGetValue(PLAYER_KEY, out var player) ? player as PongPlayer : null)
?? throw new InvalidProgramException("Player was not assigned at connection start!");
set => Context.Items[PLAYER_KEY] = value;
}
public override Task OnConnectedAsync() {
Player = Lobby.CreatePlayer();
Player.Client = Clients.Client(Context.ConnectionId);
Player.Username = "Anon";
return Task.CompletedTask;
}
private void AssertNotInRoom() {
if (Player.ConnectedRoom is PongRoom currentRoom)
throw new HubException($"User is already connected to room [{currentRoom}]");
}
public Task<string> CreateRoom() {
AssertNotInRoom();
var room = Lobby.CreateRoom(Player);
return Task.FromResult(room.ID);
}
public Task<string> JoinRoom(string roomId) {
AssertNotInRoom();
var room = Lobby.JoinRoom(Player, roomId);
return Task.FromResult(room.ID);
}
public Task LeaveRoom() {
Lobby.LeaveRoom(Player);
return Task.CompletedTask;
}
public Task RequestUsernameChange(string username) {
// TOOD: check this
Logger.LogInformation($"Player {Player} requested username change to [{username}]");
Player.Username = username;
return Task.CompletedTask;
}
public override Task OnDisconnectedAsync(Exception? exception) {
Lobby.RemovePlayer(Player);
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,65 @@
using System.Collections.Concurrent;
using Microsoft.AspNetCore.SignalR;
namespace PongGame.Hubs;
public class PongLobby {
private readonly HashSet<PongPlayer> connectedPlayers = new();
private readonly Dictionary<string, PongRoom> PongRooms = new();
public PongLobby(ILogger<PongLobby> logger)
=> Logger = logger;
public const int ROOM_ID_LENGTH = 4;
public PongPlayer CreatePlayer() {
var player = new PongPlayer();
lock (connectedPlayers)
connectedPlayers.Add(player);
return player;
}
public void RemovePlayer(PongPlayer player) {
if (player.ConnectedRoom is PongRoom room)
room.Leave(player);
lock (connectedPlayers)
_ = connectedPlayers.Remove(player);
}
public PongRoom CreateRoom(PongPlayer player) {
PongRoom room;
lock (PongRooms) {
room = new(GenerateRoomId(), Logger);
PongRooms.Add(room.ID, room);
}
room.Join(player);
return room;
}
public PongRoom JoinRoom(PongPlayer player, string roomId) {
PongRoom? room;
lock (PongRooms) {
room = PongRooms.GetValueOrDefault(roomId);
}
if (room is null) throw new HubException($"Room [{roomId}] not found!");
room.Join(player);
return room;
}
public void LeaveRoom(PongPlayer player) {
if (player.ConnectedRoom is PongRoom room)
room.Leave(player);
}
private readonly Random random = new();
private readonly ILogger<PongLobby> Logger;
private const string ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private string GenerateRoomId() {
string id;
do {
id = string.Concat(Enumerable.Range(0, ROOM_ID_LENGTH).Select(_ => ALPHABET[random.Next(ALPHABET.Length)]));
} while (PongRooms.ContainsKey(id));
return id;
}
}

View File

@ -0,0 +1,22 @@
using System.Diagnostics;
namespace PongGame.Hubs;
[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")]
public class PongPlayer {
private string username = default!;
public PongRoom? ConnectedRoom { get; internal set; }
public string Username {
get => username;
internal set {
if (username != value) {
username = value;
Task.Run(() => Client.UsernameChanged(value));
}
}
}
public IPongClient Client { get; internal set; } = default!;
public override string ToString() => $"[{Username}]";
}

65
PongGame/Hubs/PongRoom.cs Normal file
View File

@ -0,0 +1,65 @@
using Microsoft.AspNetCore.SignalR;
namespace PongGame.Hubs;
public class PongRoom {
public string ID { get; }
private readonly ILogger Logger;
public PongRoom(string id, ILogger logger) {
ID = id;
Logger = logger;
}
public PongPlayer? Player1 { get; private set; }
public PongPlayer? Player2 { get; private set; }
public GameState State { get; private set; } = GameState.WaitingForPlayers;
public void Join(PongPlayer player) {
// TODO: synchronize this
if (Player1 is null) {
Player1 = player;
Logger.LogInformation($"[{ID}] {player} joined pong room as player 1!");
} else if (Player2 is null) {
Player2 = player;
Logger.LogInformation($"[{ID}] {player} joined pong room as player 2!");
} 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($"[{ID}] Player 1 {player} left pong room!");
} else if (Player2 == player) {
Player2 = null;
Logger.LogInformation($"[{ID}] Player 2 {player} left pong room!");
}
player.ConnectedRoom = null;
_ = Task.Run(PlayersChanged);
}
private Task PlayersChanged() {
if (Player1 is PongPlayer player1 && Player2 is PongPlayer player2) {
ResumeGame(player1, player2);
} else if (Player1 is null && Player2 is null) {
CloseRoom();
} else
PauseGame();
return Task.CompletedTask;
}
private void ResumeGame(PongPlayer player1, PongPlayer player2) {
Logger.LogInformation($"[{ID}] Pong game started: {player1} vs. {player2}");
State = GameState.InProgress;
player1.Client.GameStateChanged(State);
player2.Client.GameStateChanged(State);
}
private void PauseGame() => State = GameState.WaitingForPlayers;
private void CloseRoom() => State = GameState.Finished;
public override string ToString() => $"[{ID}]";
}