Updated dependencies. Use new array initializer in C#. Added comments to alot of methods and classes

This commit is contained in:
martin 2023-12-06 23:43:21 +01:00
parent 2520a9ed94
commit 9991bac6fa
32 changed files with 458 additions and 160 deletions

View File

@ -13,9 +13,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageReference Include="NSubstitute" Version="5.1.0"/> <PackageReference Include="NSubstitute" Version="5.1.0"/>
<PackageReference Include="NUnit" Version="3.14.0"/> <PackageReference Include="NUnit" Version="4.0.1" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/> <PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
<PackageReference Include="NUnit.Analyzers" Version="3.9.0"> <PackageReference Include="NUnit.Analyzers" Version="3.10.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@ -11,12 +11,10 @@ using pacMan.Utils;
namespace BackendTests.Controllers; namespace BackendTests.Controllers;
[TestFixture]
[TestOf(nameof(GameController))]
public class GameControllerTests public class GameControllerTests
{ {
private IActionService _actionService = null!;
private GameController _controller = null!;
private GameService _gameService = null!;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
@ -27,6 +25,10 @@ public class GameControllerTests
_controller = new GameController(Substitute.For<ILogger<GameController>>(), _gameService, _actionService); _controller = new GameController(Substitute.For<ILogger<GameController>>(), _gameService, _actionService);
} }
private IActionService _actionService = null!;
private GameController _controller = null!;
private GameService _gameService = null!;
[Test] [Test]
public void Run_ReturnsSame() public void Run_ReturnsSame()
{ {
@ -52,8 +54,6 @@ public class GameControllerTests
Assert.Fail("Result is not an ArraySegment<byte>"); Assert.Fail("Result is not an ArraySegment<byte>");
} }
#region DoAction(ActionMessage message)
[Test] [Test]
public void DoAction_NegativeAction() public void DoAction_NegativeAction()
{ {
@ -71,6 +71,4 @@ public class GameControllerTests
_controller.DoAction(message); _controller.DoAction(message);
Assert.That(message.Data, Is.EqualTo(data)); Assert.That(message.Data, Is.EqualTo(data));
} }
#endregion
} }

View File

@ -1,5 +1,9 @@
using pacMan.Controllers;
namespace BackendTests.Controllers; namespace BackendTests.Controllers;
[TestFixture]
[TestOf(nameof(PlayerController))]
public class PlayerControllerTests public class PlayerControllerTests
{ {
// TODO // TODO

View File

@ -2,6 +2,8 @@ using pacMan.GameStuff.Items;
namespace BackendTests.Game.Items; namespace BackendTests.Game.Items;
[TestFixture]
[TestOf(nameof(DiceCup))]
public class DiceCupTests public class DiceCupTests
{ {
[Test] [Test]

View File

@ -2,6 +2,8 @@ using pacMan.GameStuff.Items;
namespace BackendTests.Game.Items; namespace BackendTests.Game.Items;
[TestFixture]
[TestOf(nameof(Dice))]
public class DiceTests public class DiceTests
{ {
[Test] [Test]

View File

@ -10,21 +10,10 @@ using pacMan.Services;
namespace BackendTests.Services; namespace BackendTests.Services;
[TestFixture]
[TestOf(nameof(ActionService))]
public class ActionServiceTests public class ActionServiceTests
{ {
private readonly Player _blackPlayer = Players.Create("black");
private readonly Player _redPlayer = Players.Create("red");
private readonly Player _whitePlayer = Players.Create("white");
private ActionMessage _blackMessage = null!;
private pacMan.Services.Game _game = null!;
private GameService _gameService = null!;
private ActionMessage _redMessage = null!;
private IActionService _service = null!;
private Queue<DirectionalPosition> _spawns = null!;
private ActionMessage _whiteMessage = null!;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
@ -37,6 +26,18 @@ public class ActionServiceTests
_service = new ActionService(Substitute.For<ILogger<ActionService>>(), _gameService); _service = new ActionService(Substitute.For<ILogger<ActionService>>(), _gameService);
} }
private readonly Player _blackPlayer = Players.Create("black");
private readonly Player _redPlayer = Players.Create("red");
private readonly Player _whitePlayer = Players.Create("white");
private ActionMessage _blackMessage = null!;
private pacMan.Services.Game _game = null!;
private GameService _gameService = null!;
private ActionMessage _redMessage = null!;
private IActionService _service = null!;
private Queue<DirectionalPosition> _spawns = null!;
private ActionMessage _whiteMessage = null!;
private JsonElement SerializeData(string username) => private JsonElement SerializeData(string username) =>
JsonDocument.Parse(JsonSerializer.Serialize( JsonDocument.Parse(JsonSerializer.Serialize(
new JoinGameData { Username = username, GameId = _game.Id } new JoinGameData { Username = username, GameId = _game.Id }
@ -51,8 +52,6 @@ public class ActionServiceTests
new() { At = new Position { X = 9, Y = 9 }, Direction = Direction.Right } new() { At = new Position { X = 9, Y = 9 }, Direction = Direction.Right }
}); });
#region RollDice()
[Test] [Test]
public void RollDice_ReturnsListOfIntegers() public void RollDice_ReturnsListOfIntegers()
{ {
@ -65,10 +64,6 @@ public class ActionServiceTests
}); });
} }
#endregion
#region PlayerInfo(ActionMessage message)
[Test] [Test]
public void PlayerInfo_DataIsNull() public void PlayerInfo_DataIsNull()
{ {
@ -98,10 +93,6 @@ public class ActionServiceTests
Assert.That(new List<Player> { _whitePlayer }, Is.EqualTo(players)); Assert.That(new List<Player> { _whitePlayer }, Is.EqualTo(players));
} }
#endregion
#region Ready()
[Test] [Test]
public void Ready_PlayerIsNull() public void Ready_PlayerIsNull()
{ {
@ -162,10 +153,6 @@ public class ActionServiceTests
Is.EqualTo(_blackPlayer.Username).Or.EqualTo(_whitePlayer.Username)); Is.EqualTo(_blackPlayer.Username).Or.EqualTo(_whitePlayer.Username));
} }
#endregion
#region FindNextPlayer()
[Test] [Test]
public void FindNextPlayer_NoPlayers() public void FindNextPlayer_NoPlayers()
{ {
@ -200,6 +187,4 @@ public class ActionServiceTests
var second = _service.FindNextPlayer(); var second = _service.FindNextPlayer();
Assert.That(second, Is.EqualTo(_whitePlayer.Username)); Assert.That(second, Is.EqualTo(_whitePlayer.Username));
} }
#endregion
} }

View File

@ -3,19 +3,14 @@ using Microsoft.Extensions.Logging;
using NSubstitute; using NSubstitute;
using pacMan.Exceptions; using pacMan.Exceptions;
using pacMan.GameStuff; using pacMan.GameStuff;
using pacMan.GameStuff.Items;
using pacMan.Services; using pacMan.Services;
namespace BackendTests.Services; namespace BackendTests.Services;
[TestFixture]
[TestOf(nameof(GameService))]
public class GameServiceTests public class GameServiceTests
{ {
private readonly DirectionalPosition _spawn3By3Up = new()
{ At = new Position { X = 3, Y = 3 }, Direction = Direction.Up };
private GameService _service = null!;
private Queue<DirectionalPosition> _spawns = null!;
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
@ -29,7 +24,11 @@ public class GameServiceTests
}); });
} }
#region CreateAndJoin(IPlayer player, Queue<DirectionalPosition> spawns) private readonly DirectionalPosition _spawn3By3Up = new()
{ At = new Position { X = 3, Y = 3 }, Direction = Direction.Up };
private GameService _service = null!;
private Queue<DirectionalPosition> _spawns = null!;
[Test] [Test]
public void CreateAndJoin_WhenEmpty() public void CreateAndJoin_WhenEmpty()
@ -53,15 +52,11 @@ public class GameServiceTests
Assert.Throws<ArgumentException>(() => _service.CreateAndJoin(player, _spawns)); Assert.Throws<ArgumentException>(() => _service.CreateAndJoin(player, _spawns));
} }
#endregion
#region JoinbyId(Guid id)
[Test] [Test]
public void JoinById_WhenIdNotExists() public void JoinById_WhenIdNotExists()
{ {
var player = Players.Create("white"); var player = Players.Create("white");
_service.Games.Add(new pacMan.Services.Game(_spawns) { Players = new List<Player> { player } }); _service.Games.Add(new pacMan.Services.Game(_spawns) { Players = [player] });
Assert.Throws<GameNotFoundException>(() => _service.JoinById(Guid.NewGuid(), player)); Assert.Throws<GameNotFoundException>(() => _service.JoinById(Guid.NewGuid(), player));
} }
@ -70,7 +65,7 @@ public class GameServiceTests
public void JoinById_WhenIdExists() public void JoinById_WhenIdExists()
{ {
var player = Players.Create("white"); var player = Players.Create("white");
var game = new pacMan.Services.Game(_spawns) { Players = new List<Player> { player } }; var game = new pacMan.Services.Game(_spawns) { Players = [player] };
_service.Games.Add(game); _service.Games.Add(game);
@ -84,6 +79,4 @@ public class GameServiceTests
Assert.That(_service.Games, Has.Count.EqualTo(1)); Assert.That(_service.Games, Has.Count.EqualTo(1));
}); });
} }
#endregion
} }

View File

@ -6,8 +6,24 @@ using pacMan.Utils;
namespace BackendTests.Services; namespace BackendTests.Services;
[TestFixture]
[TestOf(nameof(pacMan.Services.Game))]
public class GameTests public class GameTests
{ {
[SetUp]
public void Setup()
{
_spawns = new Queue<DirectionalPosition>(
new[] { _spawn3By3Up, _spawn7By7Left, _spawn7By7Down, _spawn7By7Right });
_game = new pacMan.Services.Game(_spawns);
_redPlayer = Players.Create("red");
_bluePlayer = Players.Create("blue");
_yellowPlayer = Players.Create("yellow");
_greenPlayer = Players.Create("green");
_purplePlayer = Players.Create("purple");
}
private readonly DirectionalPosition _spawn3By3Up = new() private readonly DirectionalPosition _spawn3By3Up = new()
{ At = new Position { X = 3, Y = 3 }, Direction = Direction.Up }; { At = new Position { X = 3, Y = 3 }, Direction = Direction.Up };
@ -29,20 +45,6 @@ public class GameTests
private Queue<DirectionalPosition> _spawns = null!; private Queue<DirectionalPosition> _spawns = null!;
private Player _yellowPlayer = null!; private Player _yellowPlayer = null!;
[SetUp]
public void Setup()
{
_spawns = new Queue<DirectionalPosition>(
new[] { _spawn3By3Up, _spawn7By7Left, _spawn7By7Down, _spawn7By7Right });
_game = new pacMan.Services.Game(_spawns);
_redPlayer = Players.Create("red");
_bluePlayer = Players.Create("blue");
_yellowPlayer = Players.Create("yellow");
_greenPlayer = Players.Create("green");
_purplePlayer = Players.Create("purple");
}
private void AddFullParty() private void AddFullParty()
{ {
_game.AddPlayer(_bluePlayer); _game.AddPlayer(_bluePlayer);
@ -51,18 +53,12 @@ public class GameTests
_game.AddPlayer(_greenPlayer); _game.AddPlayer(_greenPlayer);
} }
#region NextPlayer()
[Test] [Test]
public void NextPlayer_WhenEmpty() public void NextPlayer_WhenEmpty()
{ {
Assert.Throws<InvalidOperationException>(() => _game.NextPlayer()); Assert.Throws<InvalidOperationException>(() => _game.NextPlayer());
} }
#endregion
#region IsGameStarted
[Test] [Test]
public void IsGameStarted_WhenEmpty() public void IsGameStarted_WhenEmpty()
{ {
@ -101,10 +97,6 @@ public class GameTests
Assert.That(_game.IsGameStarted, Is.True); Assert.That(_game.IsGameStarted, Is.True);
} }
#endregion
#region AddPlayer(Player player)
[Test] [Test]
public void AddPlayer_WhenEmpty() public void AddPlayer_WhenEmpty()
{ {
@ -157,10 +149,6 @@ public class GameTests
Assert.Throws<GameNotPlayableException>(() => _game.AddPlayer(_greenPlayer)); Assert.Throws<GameNotPlayableException>(() => _game.AddPlayer(_greenPlayer));
} }
#endregion
#region Sendtoall(ArraySegment<byte> segment)
[Test] [Test]
public void SendToAll_WhenConnectionsIsNull() public void SendToAll_WhenConnectionsIsNull()
{ {
@ -171,7 +159,6 @@ public class GameTests
public void SendToAll_WhenConnectionsIsNotNull() public void SendToAll_WhenConnectionsIsNotNull()
{ {
var counter = 0; var counter = 0;
async Task Send(ArraySegment<byte> segment) => await Task.Run(() => counter++);
_game.Connections += Send; _game.Connections += Send;
_game.Connections += Send; _game.Connections += Send;
@ -182,12 +169,11 @@ public class GameTests
while (counter < 2) { } while (counter < 2) { }
Assert.That(counter, Is.EqualTo(2)); Assert.That(counter, Is.EqualTo(2));
return;
async Task Send(ArraySegment<byte> segment) => await Task.Run(() => counter++);
} }
#endregion
#region SetReady(Player player)
[Test] [Test]
public void SetReady_ReturnsAllPlayers() public void SetReady_ReturnsAllPlayers()
{ {
@ -222,10 +208,6 @@ public class GameTests
Assert.Throws<PlayerNotFoundException>(() => _game.SetReady(_redPlayer.Username)); Assert.Throws<PlayerNotFoundException>(() => _game.SetReady(_redPlayer.Username));
} }
#endregion
#region SetAllIngame()
[Test] [Test]
public void SetAllInGame_SetsStateToInGame() public void SetAllInGame_SetsStateToInGame()
{ {
@ -257,10 +239,6 @@ public class GameTests
Assert.That(_game.Players, Is.Empty); Assert.That(_game.Players, Is.Empty);
} }
#endregion
#region IsGameStarted()
[Test] [Test]
public void IsGameStarted_AllWaiting() public void IsGameStarted_AllWaiting()
{ {
@ -275,6 +253,4 @@ public class GameTests
_game.Players.ForEach(player => player.State = State.InGame); _game.Players.ForEach(player => player.State = State.InGame);
Assert.That(_game.IsGameStarted, Is.True); Assert.That(_game.IsGameStarted, Is.True);
} }
#endregion
} }

View File

@ -6,17 +6,17 @@ using pacMan.Utils;
namespace BackendTests.Services; namespace BackendTests.Services;
[TestFixture]
[TestOf(nameof(WebSocketService))]
public class WebSocketServiceTests public class WebSocketServiceTests
{ {
private IWebSocketService _service = null!;
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
_service = new WebSocketService(Substitute.For<ILogger<WebSocketService>>()); _service = new WebSocketService(Substitute.For<ILogger<WebSocketService>>());
} }
#region Send(Websocket, ArraySegment<byte>) private IWebSocketService _service = null!;
[Test] [Test]
public void Send_OpenWebsocket() public void Send_OpenWebsocket()
@ -47,10 +47,6 @@ public class WebSocketServiceTests
webSocket.Received().SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None); webSocket.Received().SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None);
} }
#endregion
#region Receive(Websocket, byte[])
[Test] [Test]
public void Receive_ExactBuffer() public void Receive_ExactBuffer()
{ {
@ -89,10 +85,6 @@ public class WebSocketServiceTests
webSocket.ReceivedWithAnyArgs().ReceiveAsync(default, CancellationToken.None); webSocket.ReceivedWithAnyArgs().ReceiveAsync(default, CancellationToken.None);
} }
#endregion
#region Close(Websocket, WebSocketCloseStatus, string?)
[Test] [Test]
public void Close_OpenWebsocket() public void Close_OpenWebsocket()
{ {
@ -123,6 +115,4 @@ public class WebSocketServiceTests
webSocket.ReceivedWithAnyArgs().CloseAsync(default, default, CancellationToken.None); webSocket.ReceivedWithAnyArgs().CloseAsync(default, default, CancellationToken.None);
} }
#endregion
} }

View File

@ -3,9 +3,15 @@ using pacMan.Utils;
namespace BackendTests.Utils; namespace BackendTests.Utils;
[TestFixture]
[TestOf(nameof(Extensions))]
public class ExtensionsTests public class ExtensionsTests
{ {
#region ToArraySegment(this object obj) [SetUp]
public void Setup()
{
_bytes = "Hello World!"u8.ToArray();
}
[Test] [Test]
public void ToArraySegmentValidObject() public void ToArraySegmentValidObject()
@ -24,18 +30,8 @@ public class ExtensionsTests
Assert.That(segment, Has.Count.EqualTo(4)); Assert.That(segment, Has.Count.EqualTo(4));
} }
#endregion
#region GetString(this byte[] bytes, int length)
private byte[] _bytes = null!; private byte[] _bytes = null!;
[SetUp]
public void Setup()
{
_bytes = "Hello World!"u8.ToArray();
}
[Test] [Test]
public void GetString_ValidByteArray() public void GetString_ValidByteArray()
{ {
@ -65,6 +61,4 @@ public class ExtensionsTests
{ {
Assert.That(_bytes.GetString(_bytes.Length / 2), Is.EqualTo("Hello ")); Assert.That(_bytes.GetString(_bytes.Length / 2), Is.EqualTo("Hello "));
} }
#endregion
} }

View File

@ -4,8 +4,8 @@ namespace DAL.Database.Service;
public class UserService public class UserService
{ {
private readonly List<User> _users = new() private readonly List<User> _users =
{ [
new User new User
{ {
Username = "Firefox", Username = "Firefox",
@ -18,10 +18,10 @@ public class UserService
Password = "Chrome", Password = "Chrome",
Colour = "blue" Colour = "blue"
} }
}; ];
public async Task<User?> Login(string username, string password) public Task<User?> Login(string username, string password)
{ {
return await Task.Run(() => _users.FirstOrDefault(x => x.Username == username && x.Password == password)); return Task.Run(() => _users.FirstOrDefault(x => x.Username == username && x.Password == password));
} }
} }

View File

@ -1,4 +1,7 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/Environment/UnitTesting/CreateUnitTestDialog/ShowAdvancedOptions/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/Environment/UnitTesting/CreateUnitTestDialog/TestProjectMapping/=60072632_002DA16F_002D4007_002D8A97_002DAC74B7E6703B/@EntryIndexedValue">35336347-32EB-4764-A28E-3F8FF6CA54C4</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/CreateUnitTestDialog/TestTemplateMapping/=NUnit3x/@EntryIndexedValue">db4927dd-2e12-48a7-9a84-2b7e3e31b9c8</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=4f001339_002D2d48_002D46c8_002D91bc_002D45608c0ab446/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &amp;lt;BackendTests&amp;gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt; <s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=4f001339_002D2d48_002D46c8_002D91bc_002D45608c0ab446/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &amp;lt;BackendTests&amp;gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
&lt;Project Location="/home/martin/Git/Csharp/pac-man-board-game/BackendTests" Presentation="&amp;lt;BackendTests&amp;gt;" /&gt; &lt;Project Location="/home/martin/Git/Csharp/pac-man-board-game/BackendTests" Presentation="&amp;lt;BackendTests&amp;gt;" /&gt;
&lt;/SessionState&gt;</s:String></wpf:ResourceDictionary> &lt;/SessionState&gt;</s:String></wpf:ResourceDictionary>

View File

@ -14,6 +14,14 @@ import { getData } from "../utils/api"
const wsService = new WebSocketService(import.meta.env.VITE_API_WS) const wsService = new WebSocketService(import.meta.env.VITE_API_WS)
/**
* Represents the main game component.
* @component
* @param player - The current player.
* @param map - The current game map.
*
* @returns The rendered game component.
*/
export const GameComponent: FC<{ player: Player; map: GameMap }> = ({ player, map }) => { export const GameComponent: FC<{ player: Player; map: GameMap }> = ({ player, map }) => {
const players = useAtomValue(playersAtom) const players = useAtomValue(playersAtom)
const dice = useAtomValue(diceAtom) const dice = useAtomValue(diceAtom)

View File

@ -28,7 +28,7 @@ export const GameTile: FC<TileWithCharacterProps> = ({
showPath = false, showPath = false,
}) => ( }) => (
<Tile <Tile
className={`${possiblePath?.end ? "border-4 border-white" : ""}`} className={`${possiblePath?.end && "border-4 border-white"}`}
type={type} type={type}
onClick={possiblePath ? () => handleMoveCharacter?.(possiblePath) : undefined} onClick={possiblePath ? () => handleMoveCharacter?.(possiblePath) : undefined}
onMouseEnter={possiblePath ? () => handleStartShowPath?.(possiblePath) : undefined} onMouseEnter={possiblePath ? () => handleStartShowPath?.(possiblePath) : undefined}

View File

@ -4,6 +4,11 @@ import { getDefaultStore } from "jotai"
import { currentPlayerNameAtom, playersAtom } from "../utils/state" import { currentPlayerNameAtom, playersAtom } from "../utils/state"
import rules from "./rules" import rules from "./rules"
/**
* Represents the different states of a game.
*
* @enum {number}
*/
export enum State { export enum State {
waitingForPlayers, waitingForPlayers,
ready, ready,

View File

@ -1,3 +1,11 @@
/**
* getData is an asynchronous function that makes an API request to retrieve data.
* If the mode is test, it returns a promise that resolves to an empty array.
*
* @param path - The path of the API endpoint.
* @param headers - The headers to be included in the request.
* @returns - A promise that resolves to the response from the API.
*/
export const getData: Api = async (path, { headers } = {}) => { export const getData: Api = async (path, { headers } = {}) => {
if (import.meta.env.MODE === "test") return Promise.resolve(new Response(JSON.stringify([]))) if (import.meta.env.MODE === "test") return Promise.resolve(new Response(JSON.stringify([])))
return await fetch(import.meta.env.VITE_API_HTTP + path, { return await fetch(import.meta.env.VITE_API_HTTP + path, {
@ -6,6 +14,14 @@ export const getData: Api = async (path, { headers } = {}) => {
}) })
} }
/**
* Makes a POST request to the API endpoint.
*
* @param path - The path of the endpoint.
* @param body - The payload of the request.
* @param headers - Additional headers for the request.
* @returns - A Promise that resolves to the Response object representing the server's response.
*/
export const postData: Api = async (path, { body, headers } = {}) => { export const postData: Api = async (path, { body, headers } = {}) => {
return await fetch(import.meta.env.VITE_API_HTTP + path, { return await fetch(import.meta.env.VITE_API_HTTP + path, {
method: "POST", method: "POST",

View File

@ -9,14 +9,23 @@ using pacMan.Utils;
namespace pacMan.Controllers; namespace pacMan.Controllers;
/// <summary>
/// Controls the game logic and handles requests related to games.
/// </summary>
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
public class GameController(ILogger<GameController> logger, IGameService webSocketService, IActionService actionService) public class GameController(ILogger<GameController> logger, IGameService webSocketService, IActionService actionService)
: GenericController(logger, webSocketService) : GenericController(logger, webSocketService)
{ {
[HttpGet("[action]")] [HttpGet("[action]")]
public override async Task Connect() => await base.Connect(); public override Task Connect() => base.Connect();
/// <summary>
/// Retrieves all games from the WebSocketService.
/// </summary>
/// <returns>
/// An IEnumerable of Game objects representing all the games.
/// </returns>
[HttpGet("[action]")] [HttpGet("[action]")]
public IEnumerable<Game> All() public IEnumerable<Game> All()
{ {
@ -24,6 +33,12 @@ public class GameController(ILogger<GameController> logger, IGameService webSock
return webSocketService.Games; return webSocketService.Games;
} }
/// <summary>
/// Adds a player to a game.
/// </summary>
/// <param name="gameId">The unique identifier of the game.</param>
/// <param name="player">The player to be joined.</param>
/// <returns>An IActionResult representing the result of the operation.</returns>
[HttpPost("[action]/{gameId:guid}")] [HttpPost("[action]/{gameId:guid}")]
public IActionResult Join(Guid gameId, [FromBody] Player player) // TODO what if player is in a game already? public IActionResult Join(Guid gameId, [FromBody] Player player) // TODO what if player is in a game already?
{ {
@ -43,6 +58,15 @@ public class GameController(ILogger<GameController> logger, IGameService webSock
} }
} }
/// <summary>
/// Checks if a game with the specified ID exists.
/// </summary>
/// <param name="gameId">The ID of the game to check.</param>
/// <returns>
/// Returns an <see cref="IActionResult" /> representing the result of the operation.
/// If a game with the specified ID exists, returns an <see cref="OkResult" />.
/// If a game with the specified ID doesn't exist, returns a <see cref="NotFoundResult" />.
/// </returns>
[HttpGet("[action]/{gameId:guid}")] [HttpGet("[action]/{gameId:guid}")]
public IActionResult Exists(Guid gameId) public IActionResult Exists(Guid gameId)
{ {
@ -50,6 +74,16 @@ public class GameController(ILogger<GameController> logger, IGameService webSock
return webSocketService.Games.Any(game => game.Id == gameId) ? Ok() : NotFound(); return webSocketService.Games.Any(game => game.Id == gameId) ? Ok() : NotFound();
} }
/// <summary>
/// Creates a new game and adds the specified player to it.
/// </summary>
/// <param name="data">The data required to create the game.</param>
/// <returns>
/// Returns an <see cref="IActionResult" /> representing the result of the operation.
/// If the game is successfully created, returns a <see cref="CreatedResult" /> with the game details and a location
/// URL.
/// If there is an error during creation, returns a <see cref="BadRequestObjectResult" /> with the error message.
/// </returns>
[HttpPost("[action]")] [HttpPost("[action]")]
public IActionResult Create([FromBody] CreateGameData data) public IActionResult Create([FromBody] CreateGameData data)
{ {
@ -71,6 +105,14 @@ public class GameController(ILogger<GameController> logger, IGameService webSock
return base.Echo(); return base.Echo();
} }
/// <summary>
/// Runs the given WebSocketReceiveResult and byte array data to perform an action.
/// </summary>
/// <param name="result">The WebSocketReceiveResult object containing information about the received data.</param>
/// <param name="data">The byte array data received from the WebSocket.</param>
/// <returns>
/// Returns an ArraySegment object representing the action response in byte array format.
/// </returns>
protected override ArraySegment<byte> Run(WebSocketReceiveResult result, byte[] data) protected override ArraySegment<byte> Run(WebSocketReceiveResult result, byte[] data)
{ {
var stringResult = data.GetString(result.Count); var stringResult = data.GetString(result.Count);
@ -91,12 +133,16 @@ public class GameController(ILogger<GameController> logger, IGameService webSock
return action.ToArraySegment(); return action.ToArraySegment();
} }
protected override async void Send(ArraySegment<byte> segment) /// <summary>
/// Sends the specified data segment.
/// </summary>
/// <param name="segment">The data segment to send.</param>
protected override void Send(ArraySegment<byte> segment)
{ {
if (actionService.Game is not null) if (actionService.Game is not null)
actionService.SendToAll(segment); actionService.SendToAll(segment);
else if (WebSocket is not null) else if (WebSocket is not null)
await webSocketService.Send(WebSocket, segment); webSocketService.Send(WebSocket, segment);
} }
protected override ArraySegment<byte>? Disconnect() => protected override ArraySegment<byte>? Disconnect() =>
@ -105,6 +151,10 @@ public class GameController(ILogger<GameController> logger, IGameService webSock
protected override void SendDisconnectMessage(ArraySegment<byte> segment) => actionService.SendToAll(segment); protected override void SendDisconnectMessage(ArraySegment<byte> segment) => actionService.SendToAll(segment);
/// <summary>
/// Performs the specified action based on the given message.
/// </summary>
/// <param name="message">The action message containing the action to be performed.</param>
public void DoAction(ActionMessage message) => public void DoAction(ActionMessage message) =>
message.Data = message.Action switch message.Data = message.Action switch
{ {

View File

@ -4,13 +4,33 @@ using pacMan.Services;
namespace pacMan.Controllers; namespace pacMan.Controllers;
/// <summary>
/// Represents a generic controller for handling WebSocket connections.
/// </summary>
public abstract class GenericController(ILogger<GenericController> logger, IWebSocketService webSocketService) public abstract class GenericController(ILogger<GenericController> logger, IWebSocketService webSocketService)
: ControllerBase : ControllerBase
{ {
/// <summary>
/// Buffer size used for processing data.
/// </summary>
private const int BufferSize = 1024 * 4; private const int BufferSize = 1024 * 4;
protected readonly ILogger<GenericController> Logger = logger; protected readonly ILogger<GenericController> Logger = logger;
protected WebSocket? WebSocket; protected WebSocket? WebSocket;
/// <summary>
/// Establishes a WebSocket connection with the client.
/// </summary>
/// <remarks>
/// This method checks if the HTTP request is a WebSocket request. If it is, it accepts the WebSocket connection, logs
/// the connection establishment, and sets the WebSocket property to
/// the accepted WebSocket instance.
/// After the connection is established, the method calls the Echo method to start echoing messages.
/// If the request is not a WebSocket request, it sets the HTTP response status code to 400 (BadRequest).
/// </remarks>
/// <returns>
/// The task representing the asynchronous operation.
/// </returns>
public virtual async Task Connect() public virtual async Task Connect()
{ {
if (HttpContext.WebSockets.IsWebSocketRequest) if (HttpContext.WebSockets.IsWebSocketRequest)
@ -26,6 +46,11 @@ public abstract class GenericController(ILogger<GenericController> logger, IWebS
} }
} }
/// <summary>
/// An asynchronous method that reads data from the WebSocket connection,
/// processes it, and sends back the processed data.
/// </summary>
/// <returns>A Task representing the asynchronous operation.</returns>
protected virtual async Task Echo() protected virtual async Task Echo()
{ {
if (WebSocket is null) return; if (WebSocket is null) return;
@ -56,10 +81,15 @@ public abstract class GenericController(ILogger<GenericController> logger, IWebS
} }
} }
protected virtual async void Send(ArraySegment<byte> segment) /// <summary>
/// Sends the specified byte segment using the WebSocket connection.
/// If the WebSocket connection is null, the method does nothing.
/// </summary>
/// <param name="segment">The byte segment to send.</param>
protected virtual void Send(ArraySegment<byte> segment)
{ {
if (WebSocket is null) return; if (WebSocket is null) return;
await webSocketService.Send(WebSocket, segment); webSocketService.Send(WebSocket, segment);
} }
protected abstract ArraySegment<byte> Run(WebSocketReceiveResult result, byte[] data); protected abstract ArraySegment<byte> Run(WebSocketReceiveResult result, byte[] data);

View File

@ -9,6 +9,11 @@ namespace pacMan.Controllers;
[Route("api/[controller]/[action]")] [Route("api/[controller]/[action]")]
public class PlayerController(UserService userService) : ControllerBase public class PlayerController(UserService userService) : ControllerBase
{ {
/// <summary>
/// Logs in a user.
/// </summary>
/// <param name="user">The user object containing the username and password.</param>
/// <returns>Returns an IActionResult indicating the login result.</returns>
[HttpPost] [HttpPost]
public async Task<IActionResult> Login([FromBody] User user) public async Task<IActionResult> Login([FromBody] User user)
{ {
@ -18,5 +23,5 @@ public class PlayerController(UserService userService) : ControllerBase
} }
[HttpPost] [HttpPost]
public async Task<IActionResult> Register([FromBody] User user) => throw new NotSupportedException(); public Task<IActionResult> Register([FromBody] User user) => throw new NotSupportedException();
} }

View File

@ -3,6 +3,9 @@ using System.Text.Json.Serialization;
namespace pacMan.GameStuff; namespace pacMan.GameStuff;
/// <summary>
/// Represents various actions that can be performed in a game.
/// </summary>
public enum GameAction public enum GameAction
{ {
Error, Error,
@ -14,12 +17,22 @@ public enum GameAction
Disconnect Disconnect
} }
/// <summary>
/// Represents an action message with optional data of type <typeparamref name="T" />.
/// Every Action may have a different type of data, or no data at all.
/// </summary>
/// <typeparam name="T">The type of the data.</typeparam>
public class ActionMessage<T> public class ActionMessage<T>
{ {
[JsonPropertyName("action")] public GameAction Action { get; init; } [JsonPropertyName("action")] public GameAction Action { get; init; }
[JsonPropertyName("data")] public T? Data { get; set; } [JsonPropertyName("data")] public T? Data { get; set; }
/// <summary>
/// Parses a JSON string into an ActionMessage object. With dynamic data.
/// </summary>
/// <param name="json">The JSON string to deserialize.</param>
/// <returns>An ActionMessage object populated with the deserialized data.</returns>
public static ActionMessage FromJson(string json) => JsonSerializer.Deserialize<ActionMessage>(json)!; public static ActionMessage FromJson(string json) => JsonSerializer.Deserialize<ActionMessage>(json)!;
} }

View File

@ -31,9 +31,12 @@ public class Character : IEquatable<Character>
return obj.GetType() == GetType() && Equals((Character)obj); return obj.GetType() == GetType() && Equals((Character)obj);
} }
public override int GetHashCode() => HashCode.Combine(Colour, Position, IsEatable, SpawnPosition, (int?)Type); public override int GetHashCode() => HashCode.Combine(Colour, Type);
} }
/// <summary>
/// Represents the types of characters in a game.
/// </summary>
public enum CharacterType public enum CharacterType
{ {
PacMan, PacMan,

View File

@ -6,7 +6,14 @@ public class Dice
{ {
private readonly Random _random = new(); private readonly Random _random = new();
[JsonInclude] public int Value { get; private set; } /// <summary>
/// Represents the value of the previous roll.
/// </summary>
[JsonInclude]
public int Value { get; private set; }
/// <summary>
/// Rolls a dice by generating a random number between 1 and 6 and assigns it to the 'Value' property of the dice.
/// </summary>
public void Roll() => Value = _random.Next(1, 7); public void Roll() => Value = _random.Next(1, 7);
} }

View File

@ -2,15 +2,24 @@ using System.Text.Json.Serialization;
namespace pacMan.GameStuff.Items; namespace pacMan.GameStuff.Items;
/// <summary>
/// Represents a cup containing multiple dice.
/// </summary>
public class DiceCup public class DiceCup
{ {
private readonly List<Dice> _dices = new() private readonly List<Dice> _dices = [new Dice(), new Dice()];
{
new Dice(),
new Dice()
};
[JsonInclude] public List<int> Values => _dices.Select(dice => dice.Value).ToList(); /// <summary>
/// Gets a list of integer values representing the values of the dices.
/// </summary>
/// <value>
/// A list of integer values representing the values of the dices.
/// </value>
[JsonInclude]
public List<int> Values => _dices.Select(dice => dice.Value).ToList();
/// <summary>
/// Rolls all the dice in the list.
/// </summary>
public void Roll() => _dices.ForEach(dice => dice.Roll()); public void Roll() => _dices.ForEach(dice => dice.Roll());
} }

View File

@ -3,6 +3,9 @@ using DAL.Database.Models;
namespace pacMan.GameStuff.Items; namespace pacMan.GameStuff.Items;
/// <summary>
/// Represents the various states of a 'Player'.
/// </summary>
public enum State public enum State
{ {
WaitingForPlayers, WaitingForPlayers,
@ -11,6 +14,9 @@ public enum State
Disconnected Disconnected
} }
/// <summary>
/// Represents a player in the game.
/// </summary>
public class Player : IEquatable<Player>, ICloneable public class Player : IEquatable<Player>, ICloneable
{ {
[JsonPropertyName("username")] public required string Username { get; init; } [JsonPropertyName("username")] public required string Username { get; init; }

View File

@ -2,6 +2,9 @@ using System.Text.Json.Serialization;
namespace pacMan.GameStuff; namespace pacMan.GameStuff;
/// <summary>
/// Represents a move path consisting of a sequence of positions and a target end position with a specified direction.
/// </summary>
public class MovePath : IEquatable<MovePath> public class MovePath : IEquatable<MovePath>
{ {
[JsonInclude] [JsonInclude]
@ -19,6 +22,11 @@ public class MovePath : IEquatable<MovePath>
return Equals(Path, other.Path) && End.Equals(other.End) && Direction == other.Direction; return Equals(Path, other.Path) && End.Equals(other.End) && Direction == other.Direction;
} }
/// <summary>
/// Converts a DirectionalPosition object to a MovePath object.
/// </summary>
/// <param name="path">The DirectionalPosition object to convert.</param>
/// <returns>A MovePath object with the same End and Direction as the DirectionalPosition object.</returns>
public static implicit operator MovePath(DirectionalPosition path) => public static implicit operator MovePath(DirectionalPosition path) =>
new() new()
{ {
@ -33,9 +41,12 @@ public class MovePath : IEquatable<MovePath>
return obj.GetType() == GetType() && Equals((MovePath)obj); return obj.GetType() == GetType() && Equals((MovePath)obj);
} }
public override int GetHashCode() => HashCode.Combine(Path, End, (int)Direction); public override int GetHashCode() => HashCode.Combine(End, (int)Direction);
} }
/// <summary>
/// Represents a position with x and y coordinates.
/// </summary>
public class Position : IEquatable<Position> public class Position : IEquatable<Position>
{ {
[JsonPropertyName("x")] public int X { get; init; } [JsonPropertyName("x")] public int X { get; init; }
@ -59,6 +70,9 @@ public class Position : IEquatable<Position>
public override int GetHashCode() => HashCode.Combine(X, Y); public override int GetHashCode() => HashCode.Combine(X, Y);
} }
/// <summary>
/// Enum representing the possible directions: Left, Up, Right, and Down.
/// </summary>
public enum Direction public enum Direction
{ {
Left, Left,
@ -67,6 +81,9 @@ public enum Direction
Down Down
} }
/// <summary>
/// Represents a directional position with a coordinate and a direction.
/// </summary>
public class DirectionalPosition : IEquatable<DirectionalPosition> public class DirectionalPosition : IEquatable<DirectionalPosition>
{ {
[JsonPropertyName("at")] public required Position At { get; init; } [JsonPropertyName("at")] public required Position At { get; init; }
@ -80,6 +97,11 @@ public class DirectionalPosition : IEquatable<DirectionalPosition>
return At.Equals(other.At) && Direction == other.Direction; return At.Equals(other.At) && Direction == other.Direction;
} }
/// <summary>
/// Converts a MovePath object to a DirectionalPosition object.
/// </summary>
/// <param name="path">The MovePath object to convert.</param>
/// <returns>A DirectionalPosition object representing the converted MovePath object.</returns>
public static explicit operator DirectionalPosition(MovePath path) => public static explicit operator DirectionalPosition(MovePath path) =>
new() new()
{ {

View File

@ -1,5 +1,8 @@
namespace pacMan.GameStuff; namespace pacMan.GameStuff;
/// <summary>
/// The Rules class holds constant values related to the game rules.
/// </summary>
public static class Rules public static class Rules
{ {
public const int MinPlayers = 2; public const int MinPlayers = 2;

View File

@ -21,6 +21,9 @@ public interface IActionService
List<Player>? Disconnect(); List<Player>? Disconnect();
} }
/// <summary>
/// Provides various actions that can be performed in a game
/// </summary>
public class ActionService(ILogger logger, IGameService gameService) : IActionService public class ActionService(ILogger logger, IGameService gameService) : IActionService
{ {
public WebSocket WebSocket { private get; set; } = null!; public WebSocket WebSocket { private get; set; } = null!;
@ -29,15 +32,24 @@ public class ActionService(ILogger logger, IGameService gameService) : IActionSe
public Player? Player { get; set; } public Player? Player { get; set; }
/// <summary>
/// Rolls the dice and returns the result. If the game is null, an empty list is returned.
/// </summary>
/// <returns>A list of integers representing the values rolled on the dice.</returns>
public List<int> RollDice() public List<int> RollDice()
{ {
Game?.DiceCup.Roll(); Game?.DiceCup.Roll();
var rolls = Game?.DiceCup.Values ?? new List<int>(); var rolls = Game?.DiceCup.Values ?? [];
logger.LogInformation("Rolled [{}]", string.Join(", ", rolls)); logger.LogInformation("Rolled [{}]", string.Join(", ", rolls));
return rolls; return rolls;
} }
/// <summary>
/// Handles the movement of the character based on the provided JSON element.
/// </summary>
/// <param name="jsonElement">The JSON element containing the data to move the character.</param>
/// <returns>The MovePlayerData object representing the updated character movement information.</returns>
public MovePlayerData HandleMoveCharacter(JsonElement? jsonElement) public MovePlayerData HandleMoveCharacter(JsonElement? jsonElement)
{ {
var data = jsonElement?.Deserialize<MovePlayerData>() ?? throw new NullReferenceException("Data is null"); var data = jsonElement?.Deserialize<MovePlayerData>() ?? throw new NullReferenceException("Data is null");
@ -50,6 +62,14 @@ public class ActionService(ILogger logger, IGameService gameService) : IActionSe
return data; return data;
} }
/// <summary>
/// Finds a game based on the given JSON element.
/// </summary>
/// <param name="jsonElement">The JSON data containing the username and gameId.</param>
/// <returns>The list of players in the found game.</returns>
/// <exception cref="NullReferenceException">Thrown when the JSON data is null.</exception>
/// <exception cref="GameNotFoundException">Thrown when the game with the given gameId does not exist.</exception>
/// <exception cref="PlayerNotFoundException">Thrown when the player with the given username is not found in the game.</exception>
public List<Player> FindGame(JsonElement? jsonElement) public List<Player> FindGame(JsonElement? jsonElement)
{ {
var (username, gameId) = var (username, gameId) =
@ -69,6 +89,12 @@ public class ActionService(ILogger logger, IGameService gameService) : IActionSe
return Game.Players; return Game.Players;
} }
/// <summary>
/// Prepares the game and returns relevant data.
/// </summary>
/// <exception cref="PlayerNotFoundException">Thrown when the player is not found.</exception>
/// <exception cref="GameNotFoundException">Thrown when the game is not found.</exception>
/// <returns>A <see cref="ReadyData" /> object containing information about game readiness.</returns>
public ReadyData Ready() public ReadyData Ready()
{ {
if (Player is null) if (Player is null)
@ -84,8 +110,22 @@ public class ActionService(ILogger logger, IGameService gameService) : IActionSe
return new ReadyData { AllReady = allReady, Players = players }; return new ReadyData { AllReady = allReady, Players = players };
} }
/// <summary>
/// Finds the next player in the game.
/// </summary>
/// <returns>
/// The username of the next player in the game, if available.
/// </returns>
/// <exception cref="GameNotFoundException">
/// Thrown if the game is not found.
/// </exception>
public string FindNextPlayer() => Game?.NextPlayer().Username ?? throw new GameNotFoundException(); public string FindNextPlayer() => Game?.NextPlayer().Username ?? throw new GameNotFoundException();
/// <summary>
/// Removes the player from the game.
/// </summary>
/// <exception cref="NullReferenceException">Throws if the game or player is null.</exception>
/// <returns>A list of remaining players in the game.</returns>
public List<Player> LeaveGame() public List<Player> LeaveGame()
{ {
if (Game is null) throw new NullReferenceException("Game is null"); if (Game is null) throw new NullReferenceException("Game is null");
@ -94,6 +134,13 @@ public class ActionService(ILogger logger, IGameService gameService) : IActionSe
return Game.Players; return Game.Players;
} }
/// <summary>
/// Disconnects the player from the game.
/// </summary>
/// <returns>
/// Returns the list of players in the game after disconnecting the player.
/// Returns null if the player is already disconnected or is not connected to a game.
/// </returns>
public List<Player>? Disconnect() public List<Player>? Disconnect()
{ {
if (Player is null) return null; if (Player is null) return null;
@ -102,7 +149,16 @@ public class ActionService(ILogger logger, IGameService gameService) : IActionSe
return Game?.Players; return Game?.Players;
} }
/// <summary>
/// Sends a given byte segment to all players in the game.
/// </summary>
/// <param name="segment">The byte segment to send.</param>
public void SendToAll(ArraySegment<byte> segment) => Game?.SendToAll(segment); public void SendToAll(ArraySegment<byte> segment) => Game?.SendToAll(segment);
private async Task SendSegment(ArraySegment<byte> segment) => await gameService.Send(WebSocket, segment); /// <summary>
/// Sends an array segment of bytes through the WebSocket connection.
/// </summary>
/// <param name="segment">The array segment of bytes to send.</param>
/// <returns>A task that represents the asynchronous send operation.</returns>
private Task SendSegment(ArraySegment<byte> segment) => gameService.Send(WebSocket, segment);
} }

View File

@ -5,14 +5,21 @@ using pacMan.GameStuff.Items;
namespace pacMan.Services; namespace pacMan.Services;
/// <summary>
/// Represents a game instance.
/// </summary>
public class Game(Queue<DirectionalPosition> spawns) public class Game(Queue<DirectionalPosition> spawns)
{ {
private readonly Random _random = new(); private readonly Random _random = new();
private int _currentPlayerIndex; private int _currentPlayerIndex;
private List<Player> _players = new(); private List<Player> _players = [];
[JsonInclude] public Guid Id { get; } = Guid.NewGuid(); [JsonInclude] public Guid Id { get; } = Guid.NewGuid();
/// <summary>
/// Gets or sets the list of players.
/// When setting, the mutable values of the players are updated instead of replacing the list.
/// </summary>
[JsonIgnore] [JsonIgnore]
public List<Player> Players public List<Player> Players
{ {
@ -32,17 +39,36 @@ public class Game(Queue<DirectionalPosition> spawns)
} }
} }
[JsonIgnore] public List<Character> Ghosts { get; set; } = new(); // TODO include [JsonIgnore] public List<Character> Ghosts { get; set; } = []; // TODO include
[JsonIgnore] private Queue<DirectionalPosition> Spawns { get; } = spawns; /// <summary>
/// The spawn locations on the map.
/// </summary>
/// <value>
/// A Queue of DirectionalPositions representing the spawn locations on the map.
/// </value>
[JsonIgnore]
private Queue<DirectionalPosition> Spawns { get; } = spawns;
[JsonIgnore] public DiceCup DiceCup { get; } = new(); // TODO include [JsonIgnore] public DiceCup DiceCup { get; } = new(); // TODO include
[JsonInclude] public int Count => Players.Count; [JsonInclude] public int Count => Players.Count;
// TODO edge-case when game has started but all players have disconnected, Disconnected property? // TODO edge-case when game has started but all players have disconnected, Disconnected property?
[JsonInclude] public bool IsGameStarted => Count > 0 && Players.Any(player => player.State is State.InGame); /// <summary>
/// Whether or not the game has started.
/// </summary>
/// <remarks>
/// The game is considered started if the count is greater than zero and at least one player is in the "InGame" state.
/// </remarks>
[JsonInclude]
public bool IsGameStarted => Count > 0 && Players.Any(player => player.State is State.InGame);
/// <summary>
/// Gets the next player in the game.
/// </summary>
/// <returns>The next player.</returns>
/// <exception cref="PlayerNotFoundException">Thrown when there are no players in the game.</exception>
public Player NextPlayer() public Player NextPlayer()
{ {
try try
@ -59,8 +85,22 @@ public class Game(Queue<DirectionalPosition> spawns)
public void Shuffle() => Players.Sort((_, _) => _random.Next(-1, 2)); public void Shuffle() => Players.Sort((_, _) => _random.Next(-1, 2));
/// <summary>
/// An event that is invoked when a message is to be sent to all connections.
/// Each player in the game should be listening to this event.
/// </summary>
/// <remarks>
/// The event handler is of type <see cref="Func{T, TResult}" /> which accepts an <see cref="ArraySegment{T}" /> of
/// bytes and returns a <see cref="Task" />.
/// This event is typically used to perform some action when something happens.
/// </remarks>
public event Func<ArraySegment<byte>, Task>? Connections; public event Func<ArraySegment<byte>, Task>? Connections;
/// <summary>
/// Adds a player to the game.
/// </summary>
/// <param name="player">The player to be added.</param>
/// <exception cref="GameNotPlayableException">Thrown when the game is already full or has already started.</exception>
public void AddPlayer(Player player) public void AddPlayer(Player player)
{ {
if (Players.Count >= Rules.MaxPlayers) if (Players.Count >= Rules.MaxPlayers)
@ -84,6 +124,10 @@ public class Game(Queue<DirectionalPosition> spawns)
return removedPlayer; return removedPlayer;
} }
/// <summary>
/// Sets the spawn position and current position of the specified player's PacMan character.
/// </summary>
/// <param name="player">The player whose PacMan character's spawn and current positions will be set.</param>
private void SetSpawn(Player player) private void SetSpawn(Player player)
{ {
if (player.PacMan.SpawnPosition is not null) return; if (player.PacMan.SpawnPosition is not null) return;
@ -92,8 +136,17 @@ public class Game(Queue<DirectionalPosition> spawns)
player.PacMan.Position = spawn; player.PacMan.Position = spawn;
} }
/// <summary>
/// Sends the specified byte segment to all connected clients.
/// </summary>
/// <param name="segment">The byte segment to send.</param>
public void SendToAll(ArraySegment<byte> segment) => Connections?.Invoke(segment); public void SendToAll(ArraySegment<byte> segment) => Connections?.Invoke(segment);
/// <summary>
/// Sets the state of the player with the specified username to Ready.
/// </summary>
/// <param name="username">The username of the player.</param>
/// <returns>An enumerable collection of Player objects.</returns>
public IEnumerable<Player> SetReady(string username) public IEnumerable<Player> SetReady(string username)
{ {
var player = Players.FirstOrDefault(p => p.Username == username); var player = Players.FirstOrDefault(p => p.Username == username);
@ -103,6 +156,12 @@ public class Game(Queue<DirectionalPosition> spawns)
return Players; return Players;
} }
/// <summary>
/// Sets all players to the "InGame" state if they are currently in the "Ready" state.
/// </summary>
/// <returns>
/// Returns true if all players were successfully set to the "InGame" state, false otherwise.
/// </returns>
public bool SetAllInGame() public bool SetAllInGame()
{ {
if (Players.Any(player => player.State is not State.Ready)) return false; if (Players.Any(player => player.State is not State.Ready)) return false;
@ -111,6 +170,11 @@ public class Game(Queue<DirectionalPosition> spawns)
return true; return true;
} }
/// <summary>
/// Finds a player by their username.
/// </summary>
/// <param name="username">The username of the player to find.</param>
/// <returns>The found Player object if a player with the given username is found; otherwise, null.</returns>
public Player? FindPlayerByUsername(string username) => public Player? FindPlayerByUsername(string username) =>
Players.FirstOrDefault(player => player.Username == username); Players.FirstOrDefault(player => player.Username == username);
} }

View File

@ -64,13 +64,20 @@ public class GameService(ILogger logger) : WebSocketService(logger), IGameServic
return game; return game;
} }
public Game? FindGameById(Guid id) /// <summary>
{ /// Finds a game by its ID.
return Games.FirstOrDefault(game => game.Id == id); /// </summary>
} /// <param name="id">The ID of the game.</param>
/// <returns>The game with the specified ID, or null if no game was found.</returns>
public Game? FindGameById(Guid id) => Games.FirstOrDefault(game => game.Id == id);
public Game? FindGameByUsername(string username) /// <summary>
{ /// Finds a game by the given username.
return Games.FirstOrDefault(game => game.Players.Exists(player => player.Username == username)); /// </summary>
} /// <param name="username">The username to search for.</param>
/// <returns>
/// The found game, if any. Returns null if no game is found.
/// </returns>
public Game? FindGameByUsername(string username) =>
Games.FirstOrDefault(game => game.Players.Exists(player => player.Username == username));
} }

View File

@ -10,8 +10,24 @@ public interface IWebSocketService
Task Close(WebSocket webSocket, WebSocketCloseStatus closeStatus, string? closeStatusDescription); Task Close(WebSocket webSocket, WebSocketCloseStatus closeStatus, string? closeStatusDescription);
} }
/// <summary>
/// WebSocketService class provides methods to send, receive and close a WebSocket connection.
/// </summary>
public class WebSocketService(ILogger logger) : IWebSocketService public class WebSocketService(ILogger logger) : IWebSocketService
{ {
/// <summary>
/// Sends the specified byte array as a text message through the WebSocket connection.
/// </summary>
/// <param name="webSocket">The WebSocket connection.</param>
/// <param name="segment">The byte array to send.</param>
/// <returns>
/// A task representing the asynchronous operation of sending the message.
/// </returns>
/// <remarks>
/// This method sends the specified byte array as a text message through the WebSocket connection.
/// It uses the WebSocket.SendAsync method to send the message asynchronously.
/// After sending the message, it logs a debug message using the logger provided.
/// </remarks>
public async Task Send(WebSocket webSocket, ArraySegment<byte> segment) public async Task Send(WebSocket webSocket, ArraySegment<byte> segment)
{ {
await webSocket.SendAsync( await webSocket.SendAsync(
@ -23,6 +39,15 @@ public class WebSocketService(ILogger logger) : IWebSocketService
logger.LogDebug("Message sent through WebSocket"); logger.LogDebug("Message sent through WebSocket");
} }
/// <summary>
/// Receives data from a websocket and logs a debug message.
/// </summary>
/// <param name="webSocket">The websocket to receive data from.</param>
/// <param name="buffer">The buffer to store the received data.</param>
/// <returns>
/// A task representing the asynchronous operation. The result contains the <see cref="WebSocketReceiveResult" />
/// which contains information about the received data.
/// </returns>
public async Task<WebSocketReceiveResult> Receive(WebSocket webSocket, byte[] buffer) public async Task<WebSocketReceiveResult> Receive(WebSocket webSocket, byte[] buffer)
{ {
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
@ -32,6 +57,13 @@ public class WebSocketService(ILogger logger) : IWebSocketService
return result; return result;
} }
/// <summary>
/// Closes the WebSocket connection with the specified close status and description.
/// </summary>
/// <param name="webSocket">The WebSocket connection to close.</param>
/// <param name="closeStatus">The status code indicating the reason for the close.</param>
/// <param name="closeStatusDescription">The optional description explaining the reason for the close.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task Close(WebSocket webSocket, WebSocketCloseStatus closeStatus, string? closeStatusDescription) public async Task Close(WebSocket webSocket, WebSocketCloseStatus closeStatus, string? closeStatusDescription)
{ {
await webSocket.CloseAsync( await webSocket.CloseAsync(

View File

@ -6,6 +6,12 @@ namespace pacMan.Utils;
public static partial class Extensions public static partial class Extensions
{ {
/// <summary>
/// Converts the specified byte array into a string using UTF-8 encoding and then removes any invalid characters.
/// </summary>
/// <param name="bytes">The byte array to convert.</param>
/// <param name="length">The number of bytes to decode.</param>
/// <returns>The decoded string without any invalid characters.</returns>
public static string GetString(this byte[] bytes, int length) public static string GetString(this byte[] bytes, int length)
{ {
var s = Encoding.UTF8.GetString(bytes, 0, length); var s = Encoding.UTF8.GetString(bytes, 0, length);
@ -13,6 +19,11 @@ public static partial class Extensions
return InvalidCharacters().Replace(s, string.Empty); return InvalidCharacters().Replace(s, string.Empty);
} }
/// <summary>
/// Converts an object to an <see cref="ArraySegment{T}" /> of bytes.
/// </summary>
/// <param name="obj">The object to convert.</param>
/// <returns>An <see cref="ArraySegment{T}" /> of bytes representing the serialized object in UTF-8 encoding.</returns>
public static ArraySegment<byte> ToArraySegment(this object obj) public static ArraySegment<byte> ToArraySegment(this object obj)
{ {
var json = JsonSerializer.Serialize(obj); var json = JsonSerializer.Serialize(obj);
@ -20,6 +31,10 @@ public static partial class Extensions
return new ArraySegment<byte>(bytes, 0, json.Length); return new ArraySegment<byte>(bytes, 0, json.Length);
} }
/// <summary>
/// Retrieves a regular expression pattern that matches invalid characters.
/// </summary>
/// <returns>A regular expression pattern for matching invalid characters.</returns>
[GeneratedRegex("\\p{C}+")] [GeneratedRegex("\\p{C}+")]
private static partial Regex InvalidCharacters(); private static partial Regex InvalidCharacters();
} }

View File

@ -18,7 +18,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="8.0.0"/> <PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="8.0.0"/>
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="5.2.2"> <PackageReference Include="Microsoft.TypeScript.MSBuild" Version="5.3.2">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>