Compare commits
No commits in common. "82923a311e211be911a841979d18faead52fc791" and "f907be7f895c70e66edfdcfbc3efa0c9bd651648" have entirely different histories.
82923a311e
...
f907be7f89
@ -1,5 +1,5 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<state>
|
<state>
|
||||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||||
</state>
|
</state>
|
||||||
</component>
|
</component>
|
@ -1,32 +1,24 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
|
|
||||||
<LangVersion>13</LangVersion>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||||||
<PackageReference Include="NSubstitute" Version="5.1.0"/>
|
<PackageReference Include="NSubstitute" Version="5.0.0" />
|
||||||
<PackageReference Include="NUnit" Version="4.0.1"/>
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
|
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||||
<PackageReference Include="NUnit.Analyzers" Version="3.10.0">
|
<PackageReference Include="NUnit.Analyzers" Version="3.3.0" />
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PackageReference Include="coverlet.collector" Version="3.1.2" />
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\pac-man-board-game\pac-man-board-game.csproj"/>
|
<ProjectReference Include="..\pac-man-board-game\pac-man-board-game.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -11,10 +11,12 @@ 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()
|
||||||
{
|
{
|
||||||
@ -25,10 +27,6 @@ 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()
|
||||||
{
|
{
|
||||||
@ -53,22 +51,4 @@ public class GameControllerTests
|
|||||||
else
|
else
|
||||||
Assert.Fail("Result is not an ArraySegment<byte>");
|
Assert.Fail("Result is not an ArraySegment<byte>");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void DoAction_NegativeAction()
|
|
||||||
{
|
|
||||||
const string data = "Nothing happens";
|
|
||||||
var message = new ActionMessage { Action = (GameAction)(-1), Data = data };
|
|
||||||
_controller.DoAction(message);
|
|
||||||
Assert.That(message.Data, Is.EqualTo(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void DoAction_OutOfBoundsAction()
|
|
||||||
{
|
|
||||||
const string data = "Nothing happens";
|
|
||||||
var message = new ActionMessage { Action = (GameAction)100, Data = data };
|
|
||||||
_controller.DoAction(message);
|
|
||||||
Assert.That(message.Data, Is.EqualTo(data));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
using pacMan.Controllers;
|
|
||||||
|
|
||||||
namespace BackendTests.Controllers;
|
namespace BackendTests.Controllers;
|
||||||
|
|
||||||
[TestFixture]
|
|
||||||
[TestOf(nameof(PlayerController))]
|
|
||||||
public class PlayerControllerTests
|
public class PlayerControllerTests
|
||||||
{
|
{
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -2,8 +2,6 @@ using pacMan.GameStuff.Items;
|
|||||||
|
|
||||||
namespace BackendTests.Game.Items;
|
namespace BackendTests.Game.Items;
|
||||||
|
|
||||||
[TestFixture]
|
|
||||||
[TestOf(nameof(DiceCup))]
|
|
||||||
public class DiceCupTests
|
public class DiceCupTests
|
||||||
{
|
{
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -2,8 +2,6 @@ using pacMan.GameStuff.Items;
|
|||||||
|
|
||||||
namespace BackendTests.Game.Items;
|
namespace BackendTests.Game.Items;
|
||||||
|
|
||||||
[TestFixture]
|
|
||||||
[TestOf(nameof(Dice))]
|
|
||||||
public class DiceTests
|
public class DiceTests
|
||||||
{
|
{
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -2,7 +2,6 @@ using System.Text.Json;
|
|||||||
using BackendTests.TestUtils;
|
using BackendTests.TestUtils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using pacMan.DTOs;
|
|
||||||
using pacMan.Exceptions;
|
using pacMan.Exceptions;
|
||||||
using pacMan.GameStuff;
|
using pacMan.GameStuff;
|
||||||
using pacMan.GameStuff.Items;
|
using pacMan.GameStuff.Items;
|
||||||
@ -10,21 +9,8 @@ using pacMan.Services;
|
|||||||
|
|
||||||
namespace BackendTests.Services;
|
namespace BackendTests.Services;
|
||||||
|
|
||||||
[TestFixture, TestOf(nameof(ActionService))]
|
|
||||||
public class ActionServiceTests
|
public class ActionServiceTests
|
||||||
{
|
{
|
||||||
[SetUp]
|
|
||||||
public void Setup()
|
|
||||||
{
|
|
||||||
_spawns = CreateQueue();
|
|
||||||
_game = new pacMan.Services.Game(_spawns);
|
|
||||||
_whiteMessage = new ActionMessage { Action = GameAction.JoinGame, Data = SerializeData(_whitePlayer.Username) };
|
|
||||||
_blackMessage = new ActionMessage { Action = GameAction.JoinGame, Data = SerializeData(_blackPlayer.Username) };
|
|
||||||
_redMessage = new ActionMessage { Action = GameAction.JoinGame, Data = SerializeData(_redPlayer.Username) };
|
|
||||||
_gameService = Substitute.For<GameService>(Substitute.For<ILogger<GameService>>());
|
|
||||||
_service = new ActionService(Substitute.For<ILogger<ActionService>>(), _gameService);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Player _blackPlayer = Players.Create("black");
|
private readonly Player _blackPlayer = Players.Create("black");
|
||||||
private readonly Player _redPlayer = Players.Create("red");
|
private readonly Player _redPlayer = Players.Create("red");
|
||||||
private readonly Player _whitePlayer = Players.Create("white");
|
private readonly Player _whitePlayer = Players.Create("white");
|
||||||
@ -37,6 +23,19 @@ public class ActionServiceTests
|
|||||||
private Queue<DirectionalPosition> _spawns = null!;
|
private Queue<DirectionalPosition> _spawns = null!;
|
||||||
private ActionMessage _whiteMessage = null!;
|
private ActionMessage _whiteMessage = null!;
|
||||||
|
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_spawns = CreateQueue();
|
||||||
|
_game = new pacMan.Services.Game(_spawns);
|
||||||
|
_whiteMessage = new ActionMessage { Action = GameAction.JoinGame, Data = SerializeData(_whitePlayer.Username) };
|
||||||
|
_blackMessage = new ActionMessage { Action = GameAction.JoinGame, Data = SerializeData(_blackPlayer.Username) };
|
||||||
|
_redMessage = new ActionMessage { Action = GameAction.JoinGame, Data = SerializeData(_redPlayer.Username) };
|
||||||
|
_gameService = Substitute.For<GameService>(Substitute.For<ILogger<GameService>>());
|
||||||
|
_service = new ActionService(Substitute.For<ILogger<ActionService>>(), _gameService);
|
||||||
|
}
|
||||||
|
|
||||||
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,6 +50,8 @@ 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()
|
||||||
{
|
{
|
||||||
@ -63,6 +64,10 @@ public class ActionServiceTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region PlayerInfo(ActionMessage message)
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void PlayerInfo_DataIsNull()
|
public void PlayerInfo_DataIsNull()
|
||||||
{
|
{
|
||||||
@ -92,6 +97,32 @@ public class ActionServiceTests
|
|||||||
Assert.That(new List<Player> { _whitePlayer }, Is.EqualTo(players));
|
Assert.That(new List<Player> { _whitePlayer }, Is.EqualTo(players));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region DoAction(ActionMessage message)
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DoAction_NegativeAction()
|
||||||
|
{
|
||||||
|
const string data = "Nothing happens";
|
||||||
|
var message = new ActionMessage { Action = (GameAction)(-1), Data = data };
|
||||||
|
_service.DoAction(message);
|
||||||
|
Assert.That(message.Data, Is.EqualTo(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DoAction_OutOfBoundsAction()
|
||||||
|
{
|
||||||
|
const string data = "Nothing happens";
|
||||||
|
var message = new ActionMessage { Action = (GameAction)100, Data = data };
|
||||||
|
_service.DoAction(message);
|
||||||
|
Assert.That(message.Data, Is.EqualTo(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Ready()
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Ready_PlayerIsNull()
|
public void Ready_PlayerIsNull()
|
||||||
{
|
{
|
||||||
@ -152,11 +183,15 @@ 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()
|
||||||
{
|
{
|
||||||
_service.Game = new pacMan.Services.Game(new Queue<DirectionalPosition>());
|
_service.Game = new pacMan.Services.Game(new Queue<DirectionalPosition>());
|
||||||
Assert.Throws<PlayerNotFoundException>(() => _service.FindNextPlayer());
|
Assert.Throws<InvalidOperationException>(() => _service.FindNextPlayer());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -186,4 +221,6 @@ 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
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,19 @@ 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()
|
||||||
{
|
{
|
||||||
@ -24,11 +29,7 @@ public class GameServiceTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly DirectionalPosition _spawn3By3Up = new()
|
#region CreateAndJoin(IPlayer player, Queue<DirectionalPosition> spawns)
|
||||||
{ 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()
|
||||||
@ -52,11 +53,15 @@ 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 = [player] });
|
_service.Games.Add(new pacMan.Services.Game(_spawns) { Players = new List<Player> { player } });
|
||||||
|
|
||||||
Assert.Throws<GameNotFoundException>(() => _service.JoinById(Guid.NewGuid(), player));
|
Assert.Throws<GameNotFoundException>(() => _service.JoinById(Guid.NewGuid(), player));
|
||||||
}
|
}
|
||||||
@ -65,7 +70,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 = [player] };
|
var game = new pacMan.Services.Game(_spawns) { Players = new List<Player> { player } };
|
||||||
_service.Games.Add(game);
|
_service.Games.Add(game);
|
||||||
|
|
||||||
|
|
||||||
@ -79,4 +84,6 @@ public class GameServiceTests
|
|||||||
Assert.That(_service.Games, Has.Count.EqualTo(1));
|
Assert.That(_service.Games, Has.Count.EqualTo(1));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -6,23 +6,8 @@ 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 };
|
||||||
|
|
||||||
@ -44,6 +29,20 @@ 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);
|
||||||
@ -52,12 +51,18 @@ public class GameTests
|
|||||||
_game.AddPlayer(_greenPlayer);
|
_game.AddPlayer(_greenPlayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region NextPlayer()
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void NextPlayer_WhenEmpty()
|
public void NextPlayer_WhenEmpty()
|
||||||
{
|
{
|
||||||
Assert.Throws<PlayerNotFoundException>(() => _game.NextPlayer());
|
Assert.Throws<InvalidOperationException>(() => _game.NextPlayer());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region IsGameStarted
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void IsGameStarted_WhenEmpty()
|
public void IsGameStarted_WhenEmpty()
|
||||||
{
|
{
|
||||||
@ -96,6 +101,10 @@ 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()
|
||||||
{
|
{
|
||||||
@ -148,6 +157,10 @@ 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()
|
||||||
{
|
{
|
||||||
@ -158,6 +171,7 @@ 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;
|
||||||
@ -168,11 +182,12 @@ 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()
|
||||||
{
|
{
|
||||||
@ -207,6 +222,10 @@ 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()
|
||||||
{
|
{
|
||||||
@ -238,6 +257,10 @@ 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()
|
||||||
{
|
{
|
||||||
@ -252,4 +275,6 @@ 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
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
|
using pacMan.Interfaces;
|
||||||
using pacMan.Services;
|
using pacMan.Services;
|
||||||
using pacMan.Utils;
|
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>>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private IWebSocketService _service = null!;
|
#region Send(Websocket, ArraySegment<byte>)
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Send_OpenWebsocket()
|
public void Send_OpenWebsocket()
|
||||||
@ -47,6 +48,10 @@ 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()
|
||||||
{
|
{
|
||||||
@ -85,6 +90,10 @@ 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()
|
||||||
{
|
{
|
||||||
@ -115,4 +124,6 @@ public class WebSocketServiceTests
|
|||||||
|
|
||||||
webSocket.ReceivedWithAnyArgs().CloseAsync(default, default, CancellationToken.None);
|
webSocket.ReceivedWithAnyArgs().CloseAsync(default, default, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -3,15 +3,9 @@ using pacMan.Utils;
|
|||||||
|
|
||||||
namespace BackendTests.Utils;
|
namespace BackendTests.Utils;
|
||||||
|
|
||||||
[TestFixture]
|
|
||||||
[TestOf(nameof(Extensions))]
|
|
||||||
public class ExtensionsTests
|
public class ExtensionsTests
|
||||||
{
|
{
|
||||||
[SetUp]
|
#region ToArraySegment(this object obj)
|
||||||
public void Setup()
|
|
||||||
{
|
|
||||||
_bytes = "Hello World!"u8.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void ToArraySegmentValidObject()
|
public void ToArraySegmentValidObject()
|
||||||
@ -30,8 +24,18 @@ 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()
|
||||||
{
|
{
|
||||||
@ -61,4 +65,6 @@ public class ExtensionsTests
|
|||||||
{
|
{
|
||||||
Assert.That(_bytes.GetString(_bytes.Length / 2), Is.EqualTo("Hello "));
|
Assert.That(_bytes.GetString(_bytes.Length / 2), Is.EqualTo("Hello "));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<RootNamespace>DAL</RootNamespace>
|
<RootNamespace>DAL</RootNamespace>
|
||||||
<LangVersion>13</LangVersion>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -4,8 +4,8 @@ namespace DAL.Database.Service;
|
|||||||
|
|
||||||
public class UserService
|
public class UserService
|
||||||
{
|
{
|
||||||
private readonly List<User> _users =
|
private readonly List<User> _users = new()
|
||||||
[
|
{
|
||||||
new User
|
new User
|
||||||
{
|
{
|
||||||
Username = "Firefox",
|
Username = "Firefox",
|
||||||
@ -18,10 +18,10 @@ public class UserService
|
|||||||
Password = "Chrome",
|
Password = "Chrome",
|
||||||
Colour = "blue"
|
Colour = "blue"
|
||||||
}
|
}
|
||||||
];
|
};
|
||||||
|
|
||||||
public Task<User?> Login(string username, string password)
|
public async Task<User?> Login(string username, string password)
|
||||||
{
|
{
|
||||||
return Task.Run(() => _users.FirstOrDefault(x => x.Username == username && x.Password == password));
|
return await Task.Run(() => _users.FirstOrDefault(x => x.Username == username && x.Password == password));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,11 +11,6 @@ cd ..
|
|||||||
dotnet run
|
dotnet run
|
||||||
```
|
```
|
||||||
|
|
||||||
### Log in
|
|
||||||
For testing there are currently two users available:
|
|
||||||
- Username: `Firefox`, Password: `Firefox`
|
|
||||||
- Username: `Chrome`, Password: `Chrome`
|
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
- Frontend
|
- Frontend
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
<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/Dpa/ExcludeFilters/=_002A_003B_002A_003BpacMan_002EControllers_002EGameController_003BConnect/@EntryIndexedValue">True</s:Boolean>
|
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=4f001339_002D2d48_002D46c8_002D91bc_002D45608c0ab446/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &lt;BackendTests&gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||||
<s:Boolean x:Key="/Default/Environment/UnitTesting/CreateUnitTestDialog/ShowAdvancedOptions/@EntryValue">True</s:Boolean>
|
<Project Location="/home/martin/Git/Csharp/pac-man-board-game/BackendTests" Presentation="&lt;BackendTests&gt;" />
|
||||||
<s:String x:Key="/Default/Environment/UnitTesting/CreateUnitTestDialog/TestProjectMapping/=60072632_002DA16F_002D4007_002D8A97_002DAC74B7E6703B/@EntryIndexedValue">35336347-32EB-4764-A28E-3F8FF6CA54C4</s:String>
|
</SessionState></s:String></wpf:ResourceDictionary>
|
||||||
<s:String x:Key="/Default/Environment/UnitTesting/CreateUnitTestDialog/TestTemplateMapping/=NUnit3x/@EntryIndexedValue">db4927dd-2e12-48a7-9a84-2b7e3e31b9c8</s:String>
|
|
||||||
</wpf:ResourceDictionary>
|
|
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": false,
|
|
||||||
"arrowParens": "avoid",
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"bracketSameLine": true,
|
|
||||||
"printWidth": 120
|
|
||||||
}
|
|
@ -2,37 +2,34 @@
|
|||||||
"name": "pac_man_board_game",
|
"name": "pac_man_board_game",
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.3",
|
"@emotion/react": "^11.11.1",
|
||||||
"@headlessui/react": "^1.7.18",
|
"@headlessui/react": "^1.7.17",
|
||||||
"@heroicons/react": "^2.1.1",
|
"@heroicons/react": "^2.0.18",
|
||||||
"jotai": "^2.6.1",
|
"jotai": "^2.3.1",
|
||||||
"jotai-devtools": "^0.7.1",
|
"jotai-devtools": "^0.6.2",
|
||||||
"oidc-client": "^1.11.5",
|
"oidc-client": "^1.11.5",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.21.1",
|
"react-router-dom": "^6.15.0",
|
||||||
"web-vitals": "^3.5.1"
|
"web-vitals": "^3.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.10.8",
|
"@types/react": "^18.2.33",
|
||||||
"@types/react": "^18.2.47",
|
"@types/react-dom": "^18.2.14",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@vitejs/plugin-react": "^4.1.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitest/coverage-c8": "^0.33.0",
|
||||||
"@vitest/coverage-v8": "^1.1.3",
|
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"happy-dom": "^12.10.3",
|
"happy-dom": "^12.9.1",
|
||||||
"postcss": "^8.4.33",
|
"postcss": "^8.4.31",
|
||||||
"prettier": "^3.1.1",
|
"tailwindcss": "^3.3.3",
|
||||||
"tailwindcss": "^3.4.1",
|
"typescript": "^5.1.6",
|
||||||
"typescript": "^5.3.3",
|
"vite": "^4.4.9",
|
||||||
"vite": "^5.0.11",
|
"vite-plugin-node-polyfills": "^0.15.0",
|
||||||
"vite-plugin-node-polyfills": "^0.19.0",
|
"vite-plugin-svgr": "^3.2.0",
|
||||||
"vite-plugin-svgr": "^4.2.0",
|
"vite-tsconfig-paths": "^4.2.0",
|
||||||
"vite-tsconfig-paths": "^4.2.3",
|
"vitest": "^0.34.6"
|
||||||
"vitest": "^1.1.3"
|
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"css-what": "^5.0.1",
|
"css-what": "^5.0.1",
|
||||||
@ -44,8 +41,7 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"test": "cross-env CI=true vitest",
|
"test": "cross-env CI=true vitest",
|
||||||
"coverage": "vitest run --coverage",
|
"coverage": "vitest run --coverage"
|
||||||
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,md}\""
|
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
5704
pac-man-board-game/ClientApp/pnpm-lock.yaml
generated
5704
pac-man-board-game/ClientApp/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,21 +1,21 @@
|
|||||||
import React, { FC } from "react"
|
import React, {FC} from "react";
|
||||||
import { Navigate, Route, Routes } from "react-router-dom"
|
import {Navigate, Route, Routes} from "react-router-dom";
|
||||||
import Layout from "./components/layout"
|
import Layout from "./components/layout";
|
||||||
import AppRoutes from "./AppRoutes"
|
import AppRoutes from "./AppRoutes";
|
||||||
import "./index.css"
|
import "./index.css";
|
||||||
import { useAtomValue } from "jotai"
|
import {useAtomValue} from "jotai";
|
||||||
import { thisPlayerAtom } from "./utils/state"
|
import {thisPlayerAtom} from "./utils/state";
|
||||||
|
|
||||||
export const App: FC = () => (
|
export const App: FC = () => (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Routes>
|
<Routes>
|
||||||
{AppRoutes.map((route, index) => {
|
{AppRoutes.map((route, index) => {
|
||||||
const { element, secured = false, ...rest } = route
|
const {element, secured = false, ...rest} = route;
|
||||||
return <Route key={index} {...rest} element={<Secured secured={secured}>{element}</Secured>} />
|
return <Route key={index} {...rest} element={<Secured secured={secured}>{element}</Secured>}/>;
|
||||||
})}
|
})}
|
||||||
</Routes>
|
</Routes>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component is used to redirect the user to the login page if they are not logged in and the page is secured.
|
* This component is used to redirect the user to the login page if they are not logged in and the page is secured.
|
||||||
@ -23,15 +23,13 @@ export const App: FC = () => (
|
|||||||
* @param secured Whether or not the page is secured.
|
* @param secured Whether or not the page is secured.
|
||||||
* @constructor The Secured component.
|
* @constructor The Secured component.
|
||||||
*/
|
*/
|
||||||
const Secured: FC<
|
const Secured: FC<{
|
||||||
{
|
secured: boolean
|
||||||
secured: boolean
|
} & ChildProps> = ({children, secured}) => {
|
||||||
} & ChildProps
|
const player = useAtomValue(thisPlayerAtom);
|
||||||
> = ({ children, secured }) => {
|
|
||||||
const player = useAtomValue(thisPlayerAtom)
|
|
||||||
|
|
||||||
if (secured && player === undefined) {
|
if (secured && player === undefined) {
|
||||||
return <Navigate to={"/login"} replace />
|
return <Navigate to={"/login"} replace/>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>
|
return <>{children}</>
|
||||||
|
@ -1,37 +1,37 @@
|
|||||||
import React from "react"
|
import React from "react";
|
||||||
import { Counter } from "./pages/counter"
|
import {Counter} from "./pages/counter";
|
||||||
import GamePage from "./pages/game"
|
import GamePage from "./pages/game";
|
||||||
import LobbyPage from "./pages/lobby"
|
import LobbyPage from "./pages/lobby";
|
||||||
import LoginPage from "./pages/login"
|
import LoginPage from "./pages/login";
|
||||||
import HomePage from "./pages/home"
|
import HomePage from "./pages/home";
|
||||||
|
|
||||||
const AppRoutes = [
|
const AppRoutes = [
|
||||||
{
|
{
|
||||||
index: true,
|
index: true,
|
||||||
element: <HomePage />,
|
element: <HomePage/>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/counter",
|
path: "/counter",
|
||||||
element: <Counter />,
|
element: <Counter/>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/game/:id",
|
path: "/game/:id",
|
||||||
element: <GamePage />,
|
element: <GamePage/>,
|
||||||
secured: true,
|
secured: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/lobby",
|
path: "/lobby",
|
||||||
element: <LobbyPage />,
|
element: <LobbyPage/>,
|
||||||
secured: true,
|
secured: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/login",
|
path: "/login",
|
||||||
element: <LoginPage />,
|
element: <LoginPage/>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "*",
|
path: "*",
|
||||||
element: <p>Page not found</p>,
|
element: <p>Page not found</p>
|
||||||
},
|
}
|
||||||
]
|
];
|
||||||
|
|
||||||
export default AppRoutes
|
export default AppRoutes;
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import React, { FC } from "react"
|
import React, {FC} from "react";
|
||||||
|
|
||||||
export const Button: FC<ButtonProps> = ({
|
export const Button: FC<ButtonProps> = (
|
||||||
className,
|
{
|
||||||
onClick,
|
className,
|
||||||
style,
|
onClick,
|
||||||
title,
|
style,
|
||||||
id,
|
title,
|
||||||
disabled = false,
|
id,
|
||||||
children,
|
disabled = false,
|
||||||
type = "button",
|
children,
|
||||||
}) => {
|
type = "button",
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
id={id}
|
id={id}
|
||||||
@ -22,4 +23,4 @@ export const Button: FC<ButtonProps> = ({
|
|||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -1,34 +1,36 @@
|
|||||||
import React, { FC } from "react"
|
import React, {FC} from "react";
|
||||||
import useToggle from "../hooks/useToggle"
|
import useToggle from "../hooks/useToggle";
|
||||||
import { BugAntIcon } from "@heroicons/react/20/solid"
|
import {BugAntIcon} from "@heroicons/react/20/solid";
|
||||||
import { selectedMapAtom } from "../utils/state"
|
import {selectedMapAtom} from "../utils/state";
|
||||||
import { useAtom } from "jotai"
|
import {useAtom} from "jotai";
|
||||||
|
|
||||||
const DebugMenu: FC = () => {
|
const DebugMenu: FC = () => {
|
||||||
const [open, toggleOpen] = useToggle()
|
|
||||||
|
const [open, toggleOpen] = useToggle();
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{open && <DebugOptions />}
|
{open && <DebugOptions/>}
|
||||||
<button
|
<button className={"fixed bottom-2 right-2 bg-gray-800 text-white p-2 z-50 rounded-full"}
|
||||||
className={"fixed bottom-2 right-2 bg-gray-800 text-white p-2 z-50 rounded-full"}
|
title={"Debug menu"}
|
||||||
title={"Debug menu"}
|
onClick={() => toggleOpen()}>
|
||||||
onClick={() => toggleOpen()}>
|
<BugAntIcon className={"w-8 m-1"}/>
|
||||||
<BugAntIcon className={"w-8 m-1"} />
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DebugMenu
|
export default DebugMenu;
|
||||||
|
|
||||||
const DebugOptions: FC = () => {
|
const DebugOptions: FC = () => {
|
||||||
const [map, setMap] = useAtom(selectedMapAtom)
|
|
||||||
|
const [map, setMap] = useAtom(selectedMapAtom);
|
||||||
|
|
||||||
function clearSessionStorage(): void {
|
function clearSessionStorage(): void {
|
||||||
sessionStorage.clear()
|
sessionStorage.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function restartGame(): void {
|
function restartGame(): void {
|
||||||
|
@ -1,49 +1,56 @@
|
|||||||
import React, { FC } from "react"
|
import React, {FC} from "react";
|
||||||
import { useAtom, useAtomValue } from "jotai"
|
import {useAtom, useAtomValue} from "jotai";
|
||||||
import { selectedDiceAtom, thisPlayerAtom } from "../utils/state"
|
import {selectedDiceAtom, thisPlayerAtom,} from "../utils/state";
|
||||||
import { Button } from "./button"
|
import {Button} from "./button";
|
||||||
|
|
||||||
export const AllDice: FC<{ values?: number[] } & ComponentProps> = ({ className, values }) => {
|
export const AllDice: FC<{ values?: number[] } & ComponentProps> = (
|
||||||
const [selectedDice, setSelectedDice] = useAtom(selectedDiceAtom)
|
{
|
||||||
|
className,
|
||||||
|
values,
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const [selectedDice, setSelectedDice] = useAtom(selectedDiceAtom);
|
||||||
|
|
||||||
function handleClick(dice: SelectedDice): void {
|
function handleClick(dice: SelectedDice): void {
|
||||||
setSelectedDice(dice)
|
setSelectedDice(dice);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"flex gap-5 justify-center"}>
|
<div className={"flex gap-5 justify-center"}>
|
||||||
{values?.map((value, index) => (
|
{values?.map((value, index) =>
|
||||||
<Dice
|
<Dice key={index}
|
||||||
key={index}
|
className={`${selectedDice?.index === index ? "border-2 border-black" : ""} ${className}`}
|
||||||
className={`${selectedDice?.index === index ? "border-2 border-black" : ""} ${className}`}
|
value={value}
|
||||||
value={value}
|
onClick={(value) => handleClick({index, value})}/>)}
|
||||||
onClick={value => handleClick({ index, value })}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface DiceProps extends ComponentProps {
|
interface DiceProps extends ComponentProps {
|
||||||
value?: number
|
value?: number,
|
||||||
onClick?: (value: number) => void
|
onClick?: (value: number) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Dice: FC<DiceProps> = ({ className, value, onClick }) => {
|
export const Dice: FC<DiceProps> = (
|
||||||
const thisPlayer = useAtomValue(thisPlayerAtom)
|
{
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const thisPlayer = useAtomValue(thisPlayerAtom);
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
if (onClick && value) {
|
if (onClick && value) {
|
||||||
onClick(value)
|
onClick(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button className={`text-2xl bg-gray-400 px-4 m-1 ${className}`}
|
||||||
className={`text-2xl bg-gray-400 px-4 m-1 ${className}`}
|
disabled={!thisPlayer?.isTurn()}
|
||||||
disabled={!thisPlayer?.isTurn()}
|
onClick={handleClick}>
|
||||||
onClick={handleClick}>
|
|
||||||
{value?.toString()}
|
{value?.toString()}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,17 +1,24 @@
|
|||||||
import React, { forwardRef, ReactEventHandler } from "react"
|
import React, {forwardRef, ReactEventHandler} from "react";
|
||||||
|
|
||||||
export interface DropdownProps extends ComponentProps {
|
export interface DropdownProps extends ComponentProps {
|
||||||
options?: string[]
|
options?: string[],
|
||||||
onSelect?: ReactEventHandler<HTMLSelectElement>
|
onSelect?: ReactEventHandler<HTMLSelectElement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Dropdown: FRComponent<DropdownProps, HTMLSelectElement> = forwardRef(({ className, options, onSelect }, ref) => (
|
const Dropdown: FRComponent<DropdownProps, HTMLSelectElement> = forwardRef((
|
||||||
<select
|
{
|
||||||
ref={ref}
|
className,
|
||||||
className={"border-2 border-gray-300 rounded-md py-1 px-2 bg-white " + className}
|
options,
|
||||||
onSelect={onSelect}>
|
onSelect
|
||||||
{options?.map((option, index) => <option key={index}>{option}</option>)}
|
}, ref) => (
|
||||||
|
<select ref={ref} className={"border-2 border-gray-300 rounded-md py-1 px-2 bg-white " + className}
|
||||||
|
onSelect={onSelect}>
|
||||||
|
{
|
||||||
|
options?.map((option, index) => (
|
||||||
|
<option key={index}>{option}</option>
|
||||||
|
))
|
||||||
|
}
|
||||||
</select>
|
</select>
|
||||||
))
|
));
|
||||||
|
|
||||||
export default Dropdown
|
export default Dropdown;
|
||||||
|
@ -1,138 +1,147 @@
|
|||||||
import React, { FC, Fragment, useEffect, useState } from "react"
|
import React, {FC, Fragment, useEffect, useState} from "react";
|
||||||
import { Character } from "../game/character"
|
import {Character} from "../game/character";
|
||||||
import findPossiblePositions from "../game/possibleMovesAlgorithm"
|
import findPossiblePositions from "../game/possibleMovesAlgorithm";
|
||||||
import { GameTile } from "./gameTile"
|
import {GameTile} from "./gameTile";
|
||||||
import { TileType } from "../game/tileType"
|
import {TileType} from "../game/tileType";
|
||||||
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai"
|
import {atom, useAtom, useAtomValue, useSetAtom} from "jotai";
|
||||||
import { allCharactersAtom, currentPlayerAtom, playersAtom, selectedDiceAtom } from "../utils/state"
|
import {allCharactersAtom, currentPlayerAtom, playersAtom, selectedDiceAtom} from "../utils/state";
|
||||||
import { Dialog, Transition } from "@headlessui/react"
|
import {Dialog, Transition} from "@headlessui/react";
|
||||||
|
|
||||||
interface BoardProps extends ComponentProps {
|
interface BoardProps extends ComponentProps {
|
||||||
onMove?: Action<Position[]>
|
onMove?: Action<Position[]>,
|
||||||
map: GameMap
|
map: GameMap
|
||||||
}
|
}
|
||||||
|
|
||||||
const modalOpenAtom = atom(false)
|
const modalOpenAtom = atom(false);
|
||||||
|
|
||||||
const Board: FC<BoardProps> = ({ className, onMove, map }) => {
|
const Board: FC<BoardProps> = (
|
||||||
const currentPlayer = useAtomValue(currentPlayerAtom)
|
{
|
||||||
const characters = useAtomValue(allCharactersAtom)
|
className,
|
||||||
const selectedDice = useAtomValue(selectedDiceAtom)
|
onMove,
|
||||||
const [selectedCharacter, setSelectedCharacter] = useState<Character>()
|
map
|
||||||
const [possiblePositions, setPossiblePositions] = useState<Path[]>([])
|
}) => {
|
||||||
const [hoveredPosition, setHoveredPosition] = useState<Path>()
|
|
||||||
const setModalOpen = useSetAtom(modalOpenAtom)
|
const currentPlayer = useAtomValue(currentPlayerAtom);
|
||||||
|
const characters = useAtomValue(allCharactersAtom);
|
||||||
|
const selectedDice = useAtomValue(selectedDiceAtom);
|
||||||
|
const [selectedCharacter, setSelectedCharacter] = useState<Character>();
|
||||||
|
const [possiblePositions, setPossiblePositions] = useState<Path[]>([]);
|
||||||
|
const [hoveredPosition, setHoveredPosition] = useState<Path>();
|
||||||
|
const setModalOpen = useSetAtom(modalOpenAtom);
|
||||||
|
|
||||||
function handleSelectCharacter(character: Character): void {
|
function handleSelectCharacter(character: Character): void {
|
||||||
if (character.isPacMan() && currentPlayer?.pacMan.colour !== character.colour) {
|
if (character.isPacMan() && currentPlayer?.pacMan.colour !== character.colour) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
setSelectedCharacter(character)
|
setSelectedCharacter(character);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleShowPath(path: Path): void {
|
function handleShowPath(path: Path): void {
|
||||||
setHoveredPosition(path)
|
setHoveredPosition(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMoveCharacter(destination: Path): void {
|
function handleMoveCharacter(destination: Path): void {
|
||||||
if (selectedCharacter) {
|
if (selectedCharacter) {
|
||||||
setHoveredPosition(undefined)
|
setHoveredPosition(undefined);
|
||||||
|
|
||||||
if (selectedCharacter.isGhost()) {
|
if (selectedCharacter.isGhost()) {
|
||||||
tryMovePacManToSpawn(destination)
|
tryMovePacManToSpawn(destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedCharacter.follow(destination)
|
selectedCharacter.follow(destination);
|
||||||
|
|
||||||
const positions = pickUpPellets(destination)
|
const positions = pickUpPellets(destination);
|
||||||
onMove?.(positions)
|
onMove?.(positions);
|
||||||
setSelectedCharacter(undefined)
|
setSelectedCharacter(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryMovePacManToSpawn(destination: Path): void {
|
function tryMovePacManToSpawn(destination: Path): void {
|
||||||
const takenChar = characters.find(c => c.isPacMan() && c.isAt(destination.end))
|
const takenChar = characters.find(c => c.isPacMan() && c.isAt(destination.end));
|
||||||
if (takenChar) {
|
if (takenChar) {
|
||||||
takenChar.moveToSpawn()
|
takenChar.moveToSpawn();
|
||||||
stealFromPlayer()
|
stealFromPlayer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function stealFromPlayer(): void {
|
function stealFromPlayer(): void {
|
||||||
setModalOpen(true)
|
setModalOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickUpPellets(destination: Path): Position[] {
|
function pickUpPellets(destination: Path): Position[] {
|
||||||
const positions: Position[] = []
|
const positions: Position[] = [];
|
||||||
if (selectedCharacter?.isPacMan()) {
|
if (selectedCharacter?.isPacMan()) {
|
||||||
for (const tile of [...(destination.path ?? []), destination.end]) {
|
|
||||||
const currentTile = map[tile.y][tile.x]
|
for (const tile of [...destination.path ?? [], destination.end]) {
|
||||||
|
const currentTile = map[tile.y][tile.x];
|
||||||
|
|
||||||
function updateTileAndPlayerBox(isPowerPellet = false): void {
|
function updateTileAndPlayerBox(isPowerPellet = false): void {
|
||||||
if (isPowerPellet) {
|
if (isPowerPellet) {
|
||||||
currentPlayer?.addPowerPellet()
|
currentPlayer?.addPowerPellet();
|
||||||
} else {
|
} else {
|
||||||
currentPlayer?.addPellet()
|
currentPlayer?.addPellet();
|
||||||
}
|
}
|
||||||
map[tile.y][tile.x] = TileType.empty
|
map[tile.y][tile.x] = TileType.empty;
|
||||||
positions.push(tile)
|
positions.push(tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentTile === TileType.pellet) {
|
if (currentTile === TileType.pellet) {
|
||||||
updateTileAndPlayerBox()
|
updateTileAndPlayerBox();
|
||||||
} else if (currentTile === TileType.powerPellet) {
|
} else if (currentTile === TileType.powerPellet) {
|
||||||
updateTileAndPlayerBox(true)
|
updateTileAndPlayerBox(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return positions
|
return positions;
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedCharacter && selectedDice) {
|
if (selectedCharacter && selectedDice) {
|
||||||
const possiblePaths = findPossiblePositions(map, selectedCharacter, selectedDice.value, characters)
|
const possiblePaths = findPossiblePositions(map, selectedCharacter, selectedDice.value, characters);
|
||||||
setPossiblePositions(possiblePaths)
|
setPossiblePositions(possiblePaths);
|
||||||
} else {
|
} else {
|
||||||
setPossiblePositions([])
|
setPossiblePositions([]);
|
||||||
}
|
}
|
||||||
}, [selectedCharacter, selectedDice])
|
}, [selectedCharacter, selectedDice]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`w-fit ${className}`}>
|
<div className={`w-fit ${className}`}>
|
||||||
<SelectPlayerModal />
|
<SelectPlayerModal/>
|
||||||
{map.map((row, rowIndex) => (
|
{
|
||||||
<div key={rowIndex} className={"flex"}>
|
map.map((row, rowIndex) =>
|
||||||
{row.map((tile, colIndex) => (
|
<div key={rowIndex} className={"flex"}>
|
||||||
<GameTile
|
{
|
||||||
key={colIndex + rowIndex * colIndex}
|
row.map((tile, colIndex) =>
|
||||||
type={tile}
|
<GameTile
|
||||||
possiblePath={possiblePositions.find(p => p.end.x === colIndex && p.end.y === rowIndex)}
|
key={colIndex + rowIndex * colIndex}
|
||||||
character={characters.find(c => c.isAt({ x: colIndex, y: rowIndex }))}
|
type={tile}
|
||||||
isSelected={selectedCharacter?.isAt({ x: colIndex, y: rowIndex })}
|
possiblePath={possiblePositions.find(p => p.end.x === colIndex && p.end.y === rowIndex)}
|
||||||
showPath={hoveredPosition?.path?.find(pos => pos.x === colIndex && pos.y === rowIndex) !== undefined}
|
character={characters.find(c => c.isAt({x: colIndex, y: rowIndex}))}
|
||||||
handleMoveCharacter={handleMoveCharacter}
|
isSelected={selectedCharacter?.isAt({x: colIndex, y: rowIndex})}
|
||||||
handleSelectCharacter={handleSelectCharacter}
|
showPath={hoveredPosition?.path?.find(pos => pos.x === colIndex && pos.y === rowIndex) !== undefined}
|
||||||
handleStartShowPath={handleShowPath}
|
handleMoveCharacter={handleMoveCharacter}
|
||||||
handleStopShowPath={() => setHoveredPosition(undefined)}
|
handleSelectCharacter={handleSelectCharacter}
|
||||||
/>
|
handleStartShowPath={handleShowPath}
|
||||||
))}
|
handleStopShowPath={() => setHoveredPosition(undefined)}/>
|
||||||
</div>
|
)
|
||||||
))}
|
}
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Board
|
export default Board;
|
||||||
|
|
||||||
const SelectPlayerModal: FC = () => {
|
const SelectPlayerModal: FC = () => {
|
||||||
const [isOpen, setIsOpen] = useAtom(modalOpenAtom)
|
const [isOpen, setIsOpen] = useAtom(modalOpenAtom);
|
||||||
const currentPlayer = useAtomValue(currentPlayerAtom)
|
const currentPlayer = useAtomValue(currentPlayerAtom);
|
||||||
const allPlayers = useAtomValue(playersAtom).filter(p => p !== currentPlayer)
|
const allPlayers = useAtomValue(playersAtom).filter(p => p !== currentPlayer);
|
||||||
|
|
||||||
if (currentPlayer === undefined) return null
|
if (currentPlayer === undefined) return null;
|
||||||
|
|
||||||
function close(): void {
|
function close(): void {
|
||||||
setIsOpen(false)
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -146,8 +155,9 @@ const SelectPlayerModal: FC = () => {
|
|||||||
enterTo="opacity-100"
|
enterTo="opacity-100"
|
||||||
leave="ease-in duration-200"
|
leave="ease-in duration-200"
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0">
|
leaveTo="opacity-0"
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
>
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-25"/>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 overflow-y-auto">
|
<div className="fixed inset-0 overflow-y-auto">
|
||||||
@ -159,8 +169,10 @@ const SelectPlayerModal: FC = () => {
|
|||||||
enterTo="opacity-100 scale-100"
|
enterTo="opacity-100 scale-100"
|
||||||
leave="ease-in duration-200"
|
leave="ease-in duration-200"
|
||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95">
|
leaveTo="opacity-0 scale-95"
|
||||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
>
|
||||||
|
<Dialog.Panel
|
||||||
|
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
||||||
Steal from player
|
Steal from player
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
@ -170,29 +182,29 @@ const SelectPlayerModal: FC = () => {
|
|||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{allPlayers.map(player => (
|
{
|
||||||
<div key={player.username} className={"border-b pb-1"}>
|
allPlayers.map(player =>
|
||||||
<span className={"mx-2"}>
|
<div key={player.username} className={"border-b pb-1"}>
|
||||||
{player.username} has {player.box.pellets} pellets
|
<span className={"mx-2"}>{player.username} has {player.box.pellets} pellets</span>
|
||||||
</span>
|
<button className={"text-blue-500 enabled:cursor-pointer disabled:text-gray-500"}
|
||||||
<button
|
style={{background: "none"}}
|
||||||
className={"text-blue-500 enabled:cursor-pointer disabled:text-gray-500"}
|
disabled={player.box.pellets === 0}
|
||||||
style={{ background: "none" }}
|
onClick={() => {
|
||||||
disabled={player.box.pellets === 0}
|
currentPlayer?.stealFrom(player);
|
||||||
onClick={() => {
|
close();
|
||||||
currentPlayer?.stealFrom(player)
|
}}>
|
||||||
close()
|
Steal
|
||||||
}}>
|
</button>
|
||||||
Steal
|
</div>
|
||||||
</button>
|
)
|
||||||
</div>
|
}
|
||||||
))}
|
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
onClick={close}>
|
onClick={close}
|
||||||
|
>
|
||||||
Don't steal from anyone
|
Don't steal from anyone
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,36 +1,33 @@
|
|||||||
import React, { FC, MouseEventHandler } from "react"
|
import React, {FC, MouseEventHandler} from "react";
|
||||||
import { State } from "../game/player"
|
import {State} from "../game/player";
|
||||||
import { currentPlayerAtom, playersAtom, rollDiceButtonAtom, thisPlayerAtom } from "../utils/state"
|
import {currentPlayerAtom, playersAtom, rollDiceButtonAtom, thisPlayerAtom} from "../utils/state";
|
||||||
import { useAtomValue } from "jotai"
|
import {useAtomValue} from "jotai";
|
||||||
import { Button } from "./button"
|
import {Button} from "./button";
|
||||||
import rules from "../game/rules"
|
import rules from "../game/rules";
|
||||||
|
|
||||||
interface GameButtonProps extends ComponentProps {
|
interface GameButtonProps extends ComponentProps {
|
||||||
onReadyClick?: MouseEventHandler
|
onReadyClick?: MouseEventHandler,
|
||||||
onRollDiceClick?: MouseEventHandler
|
onRollDiceClick?: MouseEventHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
const GameButton: FC<GameButtonProps> = ({ onReadyClick, onRollDiceClick }) => {
|
const GameButton: FC<GameButtonProps> = (
|
||||||
const currentPlayer = useAtomValue(currentPlayerAtom)
|
{
|
||||||
const thisPlayer = useAtomValue(thisPlayerAtom)
|
onReadyClick,
|
||||||
const players = useAtomValue(playersAtom)
|
onRollDiceClick,
|
||||||
const activeRollDiceButton = useAtomValue(rollDiceButtonAtom)
|
}) => {
|
||||||
|
|
||||||
if (
|
const currentPlayer = useAtomValue(currentPlayerAtom);
|
||||||
players.length >= rules.minPlayers &&
|
const thisPlayer = useAtomValue(thisPlayerAtom);
|
||||||
(currentPlayer === undefined || currentPlayer.state === State.waitingForPlayers)
|
const players = useAtomValue(playersAtom);
|
||||||
) {
|
const activeRollDiceButton = useAtomValue(rollDiceButtonAtom);
|
||||||
return <Button onClick={onReadyClick}>Ready</Button>
|
|
||||||
}
|
|
||||||
if (!thisPlayer?.isTurn()) {
|
|
||||||
// TODO also show when waiting for other players
|
|
||||||
return <Button disabled>Please wait</Button>
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Button onClick={onRollDiceClick} disabled={!activeRollDiceButton}>
|
|
||||||
Roll dice
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default GameButton
|
if (players.length >= rules.minPlayers && (currentPlayer === undefined || currentPlayer.state === State.waitingForPlayers)) {
|
||||||
|
return <Button onClick={onReadyClick}>Ready</Button>;
|
||||||
|
}
|
||||||
|
if (!thisPlayer?.isTurn()) { // TODO also show when waiting for other players
|
||||||
|
return <Button disabled>Please wait</Button>;
|
||||||
|
}
|
||||||
|
return <Button onClick={onRollDiceClick} disabled={!activeRollDiceButton}>Roll dice</Button>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GameButton;
|
||||||
|
@ -1,46 +1,39 @@
|
|||||||
import React, { FC, useEffect } from "react"
|
import React, {FC, useEffect} from "react";
|
||||||
import { AllDice } from "./dice"
|
import {AllDice} from "./dice";
|
||||||
import { doAction, GameAction } from "../utils/actions"
|
import {doAction, GameAction} from "../utils/actions";
|
||||||
import GameBoard from "./gameBoard"
|
import GameBoard from "./gameBoard";
|
||||||
import WebSocketService from "../websockets/WebSocketService"
|
import WebSocketService from "../websockets/WebSocketService";
|
||||||
import Player from "../game/player"
|
import Player from "../game/player";
|
||||||
import PlayerStats from "../components/playerStats"
|
import PlayerStats from "../components/playerStats";
|
||||||
import { useAtom, useAtomValue, useSetAtom } from "jotai"
|
import {useAtom, useAtomValue, useSetAtom} from "jotai";
|
||||||
import { diceAtom, ghostsAtom, playersAtom, rollDiceButtonAtom, selectedDiceAtom } from "../utils/state"
|
import {diceAtom, ghostsAtom, playersAtom, rollDiceButtonAtom, selectedDiceAtom} from "../utils/state";
|
||||||
import GameButton from "./gameButton"
|
import GameButton from "./gameButton";
|
||||||
import { Button } from "./button"
|
import {Button} from "./button";
|
||||||
import { useNavigate, useParams } from "react-router-dom"
|
import {useNavigate, useParams} from "react-router-dom";
|
||||||
import { getData } from "../utils/api"
|
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);
|
||||||
|
|
||||||
/**
|
export const GameComponent: FC<{ player: Player, map: GameMap }> = ({player, map}) => {
|
||||||
* 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 }) => {
|
|
||||||
const players = useAtomValue(playersAtom)
|
|
||||||
const dice = useAtomValue(diceAtom)
|
|
||||||
const [selectedDice, setSelectedDice] = useAtom(selectedDiceAtom)
|
|
||||||
const setActiveRollDiceButton = useSetAtom(rollDiceButtonAtom)
|
|
||||||
const ghosts = useAtomValue(ghostsAtom)
|
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const players = useAtomValue(playersAtom);
|
||||||
const { id } = useParams()
|
const dice = useAtomValue(diceAtom);
|
||||||
|
const [selectedDice, setSelectedDice] = useAtom(selectedDiceAtom);
|
||||||
|
const setActiveRollDiceButton = useSetAtom(rollDiceButtonAtom);
|
||||||
|
const ghosts = useAtomValue(ghostsAtom);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const {id} = useParams();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rolls the dice for the current player's turn.
|
* Rolls the dice for the current player's turn.
|
||||||
*/
|
*/
|
||||||
function rollDice(): void {
|
function rollDice(): void {
|
||||||
if (!player.isTurn()) return
|
if (!player.isTurn()) return;
|
||||||
|
|
||||||
setSelectedDice(undefined)
|
setSelectedDice(undefined);
|
||||||
wsService.send({ action: GameAction.rollDice })
|
wsService.send({action: GameAction.rollDice});
|
||||||
setActiveRollDiceButton(false)
|
setActiveRollDiceButton(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,22 +42,22 @@ export const GameComponent: FC<{ player: Player; map: GameMap }> = ({ player, ma
|
|||||||
*/
|
*/
|
||||||
function onCharacterMove(eatenPellets: Position[]): void {
|
function onCharacterMove(eatenPellets: Position[]): void {
|
||||||
if (dice && selectedDice) {
|
if (dice && selectedDice) {
|
||||||
dice.splice(selectedDice.index, 1)
|
dice.splice(selectedDice.index, 1);
|
||||||
}
|
}
|
||||||
setSelectedDice(undefined)
|
setSelectedDice(undefined);
|
||||||
const data: ActionMessage = {
|
const data: ActionMessage = {
|
||||||
action: GameAction.moveCharacter,
|
action: GameAction.moveCharacter,
|
||||||
data: {
|
data: {
|
||||||
dice: dice?.length ?? 0 > 0 ? dice : null,
|
dice: dice?.length ?? 0 > 0 ? dice : null,
|
||||||
players: players,
|
players: players,
|
||||||
ghosts: ghosts,
|
ghosts: ghosts,
|
||||||
eatenPellets: eatenPellets,
|
eatenPellets: eatenPellets
|
||||||
},
|
}
|
||||||
}
|
};
|
||||||
wsService.send(data)
|
wsService.send(data);
|
||||||
|
|
||||||
if (dice?.length === 0) {
|
if (dice?.length === 0) {
|
||||||
endTurn()
|
endTurn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,15 +70,15 @@ export const GameComponent: FC<{ player: Player; map: GameMap }> = ({ player, ma
|
|||||||
data: {
|
data: {
|
||||||
username: player.username,
|
username: player.username,
|
||||||
gameId: id,
|
gameId: id,
|
||||||
} as JoinGameData,
|
} as JoinGameData
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a ready action to the WebSocket service.
|
* Sends a ready action to the WebSocket service.
|
||||||
*/
|
*/
|
||||||
function sendReady(): void {
|
function sendReady(): void {
|
||||||
wsService.send({ action: GameAction.ready })
|
wsService.send({action: GameAction.ready});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -93,40 +86,44 @@ export const GameComponent: FC<{ player: Player; map: GameMap }> = ({ player, ma
|
|||||||
* to advance to the next player in the game.
|
* to advance to the next player in the game.
|
||||||
*/
|
*/
|
||||||
function endTurn(): void {
|
function endTurn(): void {
|
||||||
wsService.send({ action: GameAction.nextPlayer })
|
wsService.send({action: GameAction.nextPlayer});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Leaves the current game and navigates to the lobby.
|
* Leaves the current game and navigates to the lobby.
|
||||||
*/
|
*/
|
||||||
function leaveGame(): void {
|
function leaveGame(): void {
|
||||||
wsService.send({ action: GameAction.disconnect })
|
wsService.send({action: GameAction.disconnect});
|
||||||
navigate("/lobby")
|
navigate("/lobby");
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getData(`/game/exists/${id}`).then(res => {
|
|
||||||
if (!res.ok) {
|
|
||||||
return navigate("/lobby")
|
|
||||||
}
|
|
||||||
wsService.onReceive = doAction
|
|
||||||
wsService.open()
|
|
||||||
|
|
||||||
wsService.waitForOpen().then(() => joinGame())
|
getData(`/game/exists/${id}`)
|
||||||
})
|
.then(res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
return navigate("/lobby");
|
||||||
|
}
|
||||||
|
wsService.onReceive = doAction;
|
||||||
|
wsService.open();
|
||||||
|
|
||||||
return () => wsService.close()
|
wsService.waitForOpen().then(() => joinGame());
|
||||||
}, [])
|
})
|
||||||
|
|
||||||
|
return () => wsService.close();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button onClick={leaveGame}>Leave game</Button>
|
<Button onClick={leaveGame}>Leave game</Button>
|
||||||
<div className={"flex justify-center"}>{players?.map(p => <PlayerStats key={p.username} player={p} />)}</div>
|
<div className={"flex justify-center"}>
|
||||||
<div className={"flex-center"}>
|
{players?.map(p => <PlayerStats key={p.username} player={p}/>)}
|
||||||
<GameButton onReadyClick={sendReady} onRollDiceClick={rollDice} />
|
|
||||||
</div>
|
</div>
|
||||||
<AllDice values={dice} />
|
<div className={"flex-center"}>
|
||||||
<GameBoard className={"mx-auto my-2"} onMove={onCharacterMove} map={map} />
|
<GameButton onReadyClick={sendReady} onRollDiceClick={rollDice}/>
|
||||||
|
</div>
|
||||||
|
<AllDice values={dice}/>
|
||||||
|
<GameBoard className={"mx-auto my-2"} onMove={onCharacterMove} map={map}/>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,151 +1,165 @@
|
|||||||
import React, { FC, useEffect, useState } from "react"
|
import React, {FC, useEffect, useState} from "react";
|
||||||
import { TileType } from "../game/tileType"
|
import {TileType} from "../game/tileType";
|
||||||
import { Character, Dummy } from "../game/character"
|
import {Character, Dummy} from "../game/character";
|
||||||
import { Direction } from "../game/direction"
|
import {Direction} from "../game/direction";
|
||||||
import { Colour } from "../game/colour"
|
import {Colour} from "../game/colour";
|
||||||
|
|
||||||
interface TileWithCharacterProps extends ComponentProps {
|
interface TileWithCharacterProps extends ComponentProps {
|
||||||
possiblePath?: Path
|
possiblePath?: Path,
|
||||||
character?: Character
|
character?: Character,
|
||||||
type?: TileType
|
type?: TileType,
|
||||||
handleMoveCharacter?: Action<Path>
|
handleMoveCharacter?: Action<Path>,
|
||||||
handleSelectCharacter?: Action<Character>
|
handleSelectCharacter?: Action<Character>,
|
||||||
handleStartShowPath?: Action<Path>
|
handleStartShowPath?: Action<Path>,
|
||||||
handleStopShowPath?: VoidFunction
|
handleStopShowPath?: VoidFunction,
|
||||||
isSelected?: boolean
|
isSelected?: boolean,
|
||||||
showPath?: boolean
|
showPath?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GameTile: FC<TileWithCharacterProps> = ({
|
export const GameTile: FC<TileWithCharacterProps> = (
|
||||||
possiblePath,
|
{
|
||||||
character,
|
possiblePath,
|
||||||
type,
|
character,
|
||||||
handleMoveCharacter,
|
type,
|
||||||
handleSelectCharacter,
|
handleMoveCharacter,
|
||||||
handleStartShowPath,
|
handleSelectCharacter,
|
||||||
handleStopShowPath,
|
handleStartShowPath,
|
||||||
isSelected = false,
|
handleStopShowPath,
|
||||||
showPath = false,
|
isSelected = false,
|
||||||
}) => (
|
showPath = false
|
||||||
<Tile
|
}) => (
|
||||||
className={`${possiblePath?.end && "border-4 border-white"}`}
|
<Tile 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}
|
||||||
onMouseLeave={handleStopShowPath}>
|
onMouseLeave={handleStopShowPath}>
|
||||||
<>
|
<>
|
||||||
{character && (
|
{character &&
|
||||||
<div className={"flex-center wh-full"}>
|
<div className={"flex-center wh-full"}>
|
||||||
<CharacterComponent
|
<CharacterComponent
|
||||||
character={character}
|
character={character}
|
||||||
onClick={handleSelectCharacter}
|
onClick={handleSelectCharacter}
|
||||||
className={isSelected ? "animate-bounce" : ""}
|
className={isSelected ? "animate-bounce" : ""}/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
}
|
||||||
)}
|
{showPath && <Circle/>}
|
||||||
{showPath && <Circle />}
|
<AddDummy path={possiblePath}/>
|
||||||
<AddDummy path={possiblePath} />
|
|
||||||
</>
|
</>
|
||||||
</Tile>
|
</Tile>
|
||||||
)
|
);
|
||||||
|
|
||||||
const Circle: FC<{ colour?: Colour } & ComponentProps> = ({ colour = Colour.white, className }) => (
|
const Circle: FC<{ colour?: Colour } & ComponentProps> = ({colour = Colour.white, className}) => (
|
||||||
<div className={`flex-center w-full h-full ${className}`}>
|
<div className={`flex-center w-full h-full ${className}`}>
|
||||||
<div className={`w-1/2 h-1/2 rounded-full`} style={{ backgroundColor: colour }} />
|
<div className={`w-1/2 h-1/2 rounded-full`}
|
||||||
|
style={{backgroundColor: colour}}/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
|
||||||
interface TileProps extends ChildProps {
|
interface TileProps extends ChildProps {
|
||||||
type?: TileType
|
type?: TileType,
|
||||||
onClick?: VoidFunction
|
onClick?: VoidFunction,
|
||||||
onMouseEnter?: VoidFunction
|
onMouseEnter?: VoidFunction,
|
||||||
onMouseLeave?: VoidFunction
|
onMouseLeave?: VoidFunction,
|
||||||
character?: Character
|
character?: Character,
|
||||||
onCharacterClick?: Action<Character>
|
onCharacterClick?: Action<Character>,
|
||||||
characterClass?: string
|
characterClass?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Tile: FC<TileProps> = ({ type = TileType.empty, onClick, onMouseEnter, onMouseLeave, className, children }) => {
|
const Tile: FC<TileProps> = (
|
||||||
const [tileSize, setTileSize] = useState(2)
|
{
|
||||||
|
type = TileType.empty,
|
||||||
|
onClick,
|
||||||
|
onMouseEnter,
|
||||||
|
onMouseLeave,
|
||||||
|
className,
|
||||||
|
children
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const [tileSize, setTileSize] = useState(2);
|
||||||
|
|
||||||
function setColor(): string {
|
function setColor(): string {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TileType.wall:
|
case TileType.wall:
|
||||||
return "bg-blue-500"
|
return "bg-blue-500";
|
||||||
case TileType.ghostSpawn:
|
case TileType.ghostSpawn:
|
||||||
return "bg-red-500"
|
return "bg-red-500";
|
||||||
case TileType.pacmanSpawn:
|
case TileType.pacmanSpawn:
|
||||||
return "bg-green-500" // TODO should be the colour of the player
|
return "bg-green-500"; // TODO should be the colour of the player
|
||||||
default:
|
default:
|
||||||
return "bg-black"
|
return "bg-black";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
function handleResize(): void {
|
function handleResize(): void {
|
||||||
const newSize = Math.floor(window.innerWidth / 16)
|
const newSize = Math.floor(window.innerWidth / 16);
|
||||||
setTileSize(newSize)
|
setTileSize(newSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleResize()
|
handleResize();
|
||||||
|
|
||||||
window.addEventListener("resize", handleResize)
|
window.addEventListener("resize", handleResize);
|
||||||
return () => window.removeEventListener("resize", handleResize)
|
return () => window.removeEventListener("resize", handleResize);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`${setColor()} hover:border relative max-w-[75px] max-h-[75px] ${className}`}
|
||||||
className={`${setColor()} hover:border relative max-w-[75px] max-h-[75px] ${className}`}
|
style={{width: `${tileSize}px`, height: `${tileSize}px`}}
|
||||||
style={{ width: `${tileSize}px`, height: `${tileSize}px` }}
|
onClick={onClick}
|
||||||
onClick={onClick}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseLeave={onMouseLeave}>
|
||||||
onMouseLeave={onMouseLeave}>
|
|
||||||
{children}
|
{children}
|
||||||
{type === TileType.pellet && <Circle colour={Colour.yellow} />}
|
{type === TileType.pellet && <Circle colour={Colour.yellow}/>}
|
||||||
{type === TileType.powerPellet && <Circle colour={Colour.red} />}
|
{type === TileType.powerPellet && <Circle colour={Colour.red}/>}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const AddDummy: FC<{ path?: Path } & ComponentProps> = ({ path }) => (
|
const AddDummy: FC<{ path?: Path } & ComponentProps> = ({path}) => (
|
||||||
<>
|
<>
|
||||||
{path && (
|
{path &&
|
||||||
<div className={"flex-center wh-full"}>
|
<div className={"flex-center wh-full"}>
|
||||||
<CharacterComponent character={new Dummy(path)} />
|
<CharacterComponent character={new Dummy(path)}/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
}
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
|
|
||||||
interface CharacterComponentProps extends ComponentProps {
|
interface CharacterComponentProps extends ComponentProps {
|
||||||
character?: Character
|
character?: Character,
|
||||||
onClick?: Action<Character>
|
onClick?: Action<Character>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const CharacterComponent: FC<CharacterComponentProps> = ({ character, onClick, className }) => {
|
const CharacterComponent: FC<CharacterComponentProps> = (
|
||||||
|
{
|
||||||
|
character,
|
||||||
|
onClick,
|
||||||
|
className
|
||||||
|
}) => {
|
||||||
|
|
||||||
function getSide() {
|
function getSide() {
|
||||||
switch (character?.position?.direction) {
|
switch (character?.position?.direction) {
|
||||||
case Direction.up:
|
case Direction.up:
|
||||||
return "right-1/4 top-0"
|
return "right-1/4 top-0";
|
||||||
case Direction.down:
|
case Direction.down:
|
||||||
return "right-1/4 bottom-0"
|
return "right-1/4 bottom-0";
|
||||||
case Direction.left:
|
case Direction.left:
|
||||||
return "left-0 top-1/4"
|
return "left-0 top-1/4";
|
||||||
case Direction.right:
|
case Direction.right:
|
||||||
return "right-0 top-1/4"
|
return "right-0 top-1/4";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (character === undefined) return null
|
if (character === undefined) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`rounded-full w-4/5 h-4/5 cursor-pointer hover:border border-black relative ${className}`}
|
||||||
className={`rounded-full w-4/5 h-4/5 cursor-pointer hover:border border-black relative ${className}`}
|
style={{backgroundColor: `${character.colour}`}}
|
||||||
style={{ backgroundColor: `${character.colour}` }}
|
onClick={() => onClick?.(character)}>
|
||||||
onClick={() => onClick?.(character)}>
|
|
||||||
<div>
|
<div>
|
||||||
<div className={`absolute ${getSide()} w-1/2 h-1/2 rounded-full bg-black`} />
|
<div className={`absolute ${getSide()} w-1/2 h-1/2 rounded-full bg-black`}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,18 +1,23 @@
|
|||||||
import React, { forwardRef } from "react"
|
import React, {forwardRef} from "react";
|
||||||
|
|
||||||
const Input: FRComponent<InputProps, HTMLInputElement> = forwardRef(
|
const Input: FRComponent<InputProps, HTMLInputElement> = forwardRef((
|
||||||
({ type = "text", className, id, placeholder, required = false, name, autoComplete = "off" }, ref) => (
|
{
|
||||||
<input
|
type = "text",
|
||||||
type={type}
|
className,
|
||||||
autoComplete={autoComplete}
|
id,
|
||||||
ref={ref}
|
placeholder,
|
||||||
id={id}
|
required = false,
|
||||||
name={name}
|
name,
|
||||||
className={"border-2 border-gray-300 rounded-md p-1 " + className}
|
autoComplete = "off",
|
||||||
placeholder={placeholder}
|
}, ref) => (
|
||||||
required={required}
|
<input type={type}
|
||||||
/>
|
autoComplete={autoComplete}
|
||||||
),
|
ref={ref}
|
||||||
)
|
id={id}
|
||||||
|
name={name}
|
||||||
|
className={"border-2 border-gray-300 rounded-md p-1 " + className}
|
||||||
|
placeholder={placeholder}
|
||||||
|
required={required}/>
|
||||||
|
));
|
||||||
|
|
||||||
export default Input
|
export default Input;
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import React, { FC } from "react"
|
import React, {FC} from "react";
|
||||||
import NavMenu from "./navMenu"
|
import NavMenu from "./navMenu";
|
||||||
|
|
||||||
const Layout: FC<ChildProps> = ({ children }) => (
|
const Layout: FC<ChildProps> = ({children}) => (
|
||||||
<div>
|
<div>
|
||||||
<NavMenu />
|
<NavMenu/>
|
||||||
<main>{children}</main>
|
<main>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
|
||||||
export default Layout
|
export default Layout;
|
||||||
|
@ -1,89 +1,84 @@
|
|||||||
import React, { FC, useEffect } from "react"
|
import React, {FC, useEffect} from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom"
|
import {Link, useNavigate} from "react-router-dom";
|
||||||
import { useAtom, useAtomValue } from "jotai"
|
import {useAtom, useAtomValue} from "jotai";
|
||||||
import { thisPlayerAtom } from "../utils/state"
|
import {thisPlayerAtom} from "../utils/state";
|
||||||
import { UserCircleIcon } from "@heroicons/react/24/outline"
|
import {UserCircleIcon} from "@heroicons/react/24/outline";
|
||||||
import useToggle from "../hooks/useToggle"
|
import useToggle from "../hooks/useToggle";
|
||||||
|
|
||||||
const NavMenu: FC = () => {
|
const NavMenu: FC = () => {
|
||||||
const player = useAtomValue(thisPlayerAtom)
|
const player = useAtomValue(thisPlayerAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={"z-10"}>
|
<header className={"z-10"}>
|
||||||
<nav className="mb-3 flex justify-between border-b-2">
|
<nav className="mb-3 flex justify-between border-b-2">
|
||||||
<Link to="/">
|
<Link to="/"><h2 className={"m-1"}>Pac-Man Board Game</h2></Link>
|
||||||
<h2 className={"m-1"}>Pac-Man Board Game</h2>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<ul className="inline-flex gap-2 items-center mr-5 relative">
|
<ul className="inline-flex gap-2 items-center mr-5 relative">
|
||||||
<NavItem to="/">Home</NavItem>
|
<NavItem to="/">Home</NavItem>
|
||||||
{player === undefined ? (
|
{
|
||||||
<NavItem className={"mx-2"} to={"/login"}>
|
player === undefined ?
|
||||||
Login
|
<NavItem className={"mx-2"} to={"/login"}>Login</NavItem>
|
||||||
</NavItem>
|
:
|
||||||
) : (
|
<>
|
||||||
<>
|
<NavItem to={"/lobby"}>Lobby</NavItem>
|
||||||
<NavItem to={"/lobby"}>Lobby</NavItem>
|
<ProfileDropdown className={"mx-2"}/>
|
||||||
<ProfileDropdown className={"mx-2"} />
|
</>
|
||||||
</>
|
}
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default NavMenu
|
export default NavMenu;
|
||||||
|
|
||||||
const NavItem: FC<LinkProps> = ({ to, children, className }) => (
|
const NavItem: FC<LinkProps> = ({to, children, className}) => (
|
||||||
<li>
|
<li>
|
||||||
<Link className={`hover:underline ${className}`} to={to}>
|
<Link className={`hover:underline ${className}`} to={to}>{children}</Link>
|
||||||
{children}
|
|
||||||
</Link>
|
|
||||||
</li>
|
</li>
|
||||||
)
|
);
|
||||||
|
|
||||||
const ProfileDropdown: FC<ComponentProps> = ({ className }) => {
|
const ProfileDropdown: FC<ComponentProps> = ({className}) => {
|
||||||
const [player, setPlayer] = useAtom(thisPlayerAtom)
|
const [player, setPlayer] = useAtom(thisPlayerAtom);
|
||||||
const [isOpened, toggleOpen] = useToggle()
|
const [isOpened, toggleOpen] = useToggle();
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate();
|
||||||
|
|
||||||
async function logout(): Promise<void> {
|
async function logout(): Promise<void> {
|
||||||
setPlayer(undefined)
|
setPlayer(undefined);
|
||||||
navigate("/login")
|
navigate("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
if (isOpened) {
|
if (isOpened) {
|
||||||
function closeIfOutsideButton(e: MouseEvent): void {
|
function closeIfOutsideButton(e: MouseEvent): void {
|
||||||
if (isOpened && e.target instanceof HTMLElement) {
|
if (isOpened && e.target instanceof HTMLElement) {
|
||||||
if (e.target.closest("#profile-dropdown") === null) {
|
if (e.target.closest("#profile-dropdown") === null) {
|
||||||
toggleOpen(false)
|
toggleOpen(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("click", closeIfOutsideButton)
|
document.addEventListener("click", closeIfOutsideButton);
|
||||||
return () => document.removeEventListener("click", closeIfOutsideButton)
|
return () => document.removeEventListener("click", closeIfOutsideButton);
|
||||||
}
|
}
|
||||||
}, [isOpened])
|
|
||||||
|
}, [isOpened]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<li
|
<li id={"profile-dropdown"}
|
||||||
id={"profile-dropdown"}
|
className={`inline-flex-center cursor-pointer hover:bg-gray-100 h-full px-2 ${className}`}
|
||||||
className={`inline-flex-center cursor-pointer hover:bg-gray-100 h-full px-2 ${className}`}
|
onClick={() => toggleOpen()}>
|
||||||
onClick={() => toggleOpen()}>
|
<UserCircleIcon className={"w-7"}/>
|
||||||
<UserCircleIcon className={"w-7"} />
|
|
||||||
<span>{player?.username}</span>
|
<span>{player?.username}</span>
|
||||||
</li>
|
</li>
|
||||||
{isOpened && (
|
{
|
||||||
<div className={"absolute right-2 border rounded-b -bottom-9 px-5"}>
|
isOpened &&
|
||||||
<button onClick={logout} className={"hover:underline py-1"}>
|
<div className={"absolute right-2 border rounded-b -bottom-9 px-5"}>
|
||||||
Logout
|
<button onClick={logout} className={"hover:underline py-1"}>Logout</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
}
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,29 @@
|
|||||||
import React, { FC } from "react"
|
import React, {FC} from "react";
|
||||||
import Player, { State } from "../game/player"
|
import Player, {State} from "../game/player";
|
||||||
import { useAtomValue } from "jotai"
|
import {useAtomValue} from "jotai";
|
||||||
import { currentPlayerNameAtom } from "../utils/state"
|
import {currentPlayerNameAtom} from "../utils/state";
|
||||||
|
|
||||||
const PlayerStats: FC<{ player: Player } & ComponentProps> = ({ player, className, id }) => {
|
const PlayerStats: FC<{ player: Player } & ComponentProps> = (
|
||||||
const currentPlayerName = useAtomValue(currentPlayerNameAtom)
|
{
|
||||||
|
player,
|
||||||
|
className,
|
||||||
|
id
|
||||||
|
}) => {
|
||||||
|
const currentPlayerName = useAtomValue(currentPlayerNameAtom);
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={player.colour}
|
||||||
key={player.colour}
|
className={`w-fit m-2 ${player.state === State.disconnected ? "text-gray-500" : ""} ${className}`} id={id}>
|
||||||
className={`w-fit m-2 ${player.state === State.disconnected ? "text-gray-500" : ""} ${className}`}
|
|
||||||
id={id}>
|
|
||||||
<p className={player.username === currentPlayerName ? "underline" : ""}>Player: {player.username}</p>
|
<p className={player.username === currentPlayerName ? "underline" : ""}>Player: {player.username}</p>
|
||||||
<p>Colour: {player.colour}</p>
|
<p>Colour: {player.colour}</p>
|
||||||
{player.state === State.inGame || player.state === State.disconnected ? (
|
{player.state === State.inGame || player.state === State.disconnected ?
|
||||||
<>
|
<>
|
||||||
<p>Pellets: {player.box.pellets}</p>
|
<p>Pellets: {player.box.pellets}</p>
|
||||||
<p>PowerPellets: {player.box.powerPellets}</p>
|
<p>PowerPellets: {player.box.powerPellets}</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
:
|
||||||
<p>{player.state === State.waitingForPlayers ? "Waiting" : "Ready"}</p>
|
<p>{player.state === State.waitingForPlayers ? "Waiting" : "Ready"}</p>}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default PlayerStats
|
export default PlayerStats;
|
||||||
|
@ -1,31 +1,32 @@
|
|||||||
export default class Box implements BoxProps {
|
export default class Box implements BoxProps {
|
||||||
public readonly colour
|
public readonly colour;
|
||||||
public pellets
|
public pellets;
|
||||||
public powerPellets
|
public powerPellets;
|
||||||
|
|
||||||
public constructor({ colour, pellets = 0, powerPellets = 0 }: BoxProps) {
|
public constructor({colour, pellets = 0, powerPellets = 0}: BoxProps) {
|
||||||
this.colour = colour
|
this.colour = colour;
|
||||||
this.pellets = pellets
|
this.pellets = pellets;
|
||||||
this.powerPellets = powerPellets
|
this.powerPellets = powerPellets;
|
||||||
}
|
}
|
||||||
|
|
||||||
public addPellet(): void {
|
public addPellet(): void {
|
||||||
this.pellets++
|
this.pellets++;
|
||||||
}
|
}
|
||||||
|
|
||||||
public removePellet(): boolean {
|
public removePellet(): boolean {
|
||||||
if (this.pellets <= 0) return false
|
if (this.pellets <= 0) return false;
|
||||||
this.pellets--
|
this.pellets--;
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public addPowerPellet(): void {
|
public addPowerPellet(): void {
|
||||||
this.powerPellets++
|
this.powerPellets++;
|
||||||
}
|
}
|
||||||
|
|
||||||
public removePowerPellet(): boolean {
|
public removePowerPellet(): boolean {
|
||||||
if (this.powerPellets <= 0) return false
|
if (this.powerPellets <= 0) return false;
|
||||||
this.powerPellets--
|
this.powerPellets--;
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Direction } from "./direction"
|
import {Direction} from "./direction";
|
||||||
import { Colour } from "./colour"
|
import {Colour} from "./colour";
|
||||||
|
|
||||||
export enum CharacterType {
|
export enum CharacterType {
|
||||||
pacMan,
|
pacMan,
|
||||||
@ -8,106 +8,89 @@ export enum CharacterType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Character implements CharacterProps {
|
export class Character implements CharacterProps {
|
||||||
public readonly colour
|
public readonly colour;
|
||||||
public position
|
public position;
|
||||||
public isEatable
|
public isEatable;
|
||||||
public readonly spawnPosition
|
public readonly spawnPosition;
|
||||||
public readonly type
|
public readonly type;
|
||||||
|
|
||||||
public constructor({
|
public constructor(
|
||||||
colour,
|
{
|
||||||
position = null,
|
colour,
|
||||||
type = CharacterType.dummy,
|
position = null,
|
||||||
isEatable = type === CharacterType.pacMan,
|
type = CharacterType.dummy,
|
||||||
spawnPosition = null,
|
isEatable = type === CharacterType.pacMan,
|
||||||
}: CharacterProps) {
|
spawnPosition = null
|
||||||
this.colour = colour
|
}: CharacterProps) {
|
||||||
this.isEatable = isEatable
|
this.colour = colour;
|
||||||
this.spawnPosition = spawnPosition
|
this.isEatable = isEatable;
|
||||||
|
this.spawnPosition = spawnPosition;
|
||||||
|
|
||||||
if (position) {
|
if (position) {
|
||||||
this.position = position
|
this.position = position;
|
||||||
} else {
|
} else {
|
||||||
this.position = spawnPosition
|
this.position = spawnPosition ? {
|
||||||
? {
|
end: spawnPosition!.at,
|
||||||
end: spawnPosition!.at,
|
direction: spawnPosition!.direction
|
||||||
direction: spawnPosition!.direction,
|
} : null;
|
||||||
}
|
|
||||||
: null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.type = type
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public follow(path: Path): void {
|
public follow(path: Path): void {
|
||||||
if (!this.position) {
|
if (!this.position) {
|
||||||
this.position = path
|
this.position = path;
|
||||||
} else {
|
} else {
|
||||||
this.position.end = path.end
|
this.position.end = path.end;
|
||||||
this.position.direction = path.direction
|
this.position.direction = path.direction;
|
||||||
this.position.path = undefined
|
this.position.path = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public isPacMan(): boolean {
|
public isPacMan(): boolean {
|
||||||
return this.type === CharacterType.pacMan
|
return this.type === CharacterType.pacMan;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isGhost(): boolean {
|
public isGhost(): boolean {
|
||||||
return this.type === CharacterType.ghost
|
return this.type === CharacterType.ghost;
|
||||||
}
|
}
|
||||||
|
|
||||||
public moveToSpawn(): void {
|
public moveToSpawn(): void {
|
||||||
if (!this.spawnPosition) return
|
if (!this.spawnPosition) return;
|
||||||
this.follow({
|
this.follow({end: this.spawnPosition.at, direction: this.spawnPosition.direction});
|
||||||
end: this.spawnPosition.at,
|
|
||||||
direction: this.spawnPosition.direction,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public isAt(position: Position): boolean {
|
public isAt(position: Position): boolean {
|
||||||
return this.position !== null && this.position.end.x === position.x && this.position.end.y === position.y
|
return this.position !== null && this.position.end.x === position.x && this.position.end.y === position.y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PacMan extends Character implements CharacterProps {
|
export class PacMan extends Character implements CharacterProps {
|
||||||
public constructor({
|
|
||||||
colour,
|
public constructor({colour, position, isEatable = true, spawnPosition, type = CharacterType.pacMan}: CharacterProps) {
|
||||||
position,
|
super({colour: colour, position: position, isEatable: isEatable, spawnPosition: spawnPosition, type: type});
|
||||||
isEatable = true,
|
|
||||||
spawnPosition,
|
|
||||||
type = CharacterType.pacMan,
|
|
||||||
}: CharacterProps) {
|
|
||||||
super({
|
|
||||||
colour: colour,
|
|
||||||
position: position,
|
|
||||||
isEatable: isEatable,
|
|
||||||
spawnPosition: spawnPosition,
|
|
||||||
type: type,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Ghost extends Character implements CharacterProps {
|
export class Ghost extends Character implements CharacterProps {
|
||||||
public constructor({ colour, position, isEatable, spawnPosition, type = CharacterType.ghost }: CharacterProps) {
|
|
||||||
super({
|
public constructor({colour, position, isEatable, spawnPosition, type = CharacterType.ghost}: CharacterProps) {
|
||||||
colour: colour,
|
super({colour: colour, position: position, isEatable: isEatable, spawnPosition: spawnPosition, type: type});
|
||||||
position: position,
|
|
||||||
isEatable: isEatable,
|
|
||||||
spawnPosition: spawnPosition,
|
|
||||||
type: type,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Dummy extends Character implements CharacterProps {
|
export class Dummy extends Character implements CharacterProps {
|
||||||
|
|
||||||
public constructor(position: Path) {
|
public constructor(position: Path) {
|
||||||
super({
|
super({
|
||||||
colour: Colour.grey,
|
colour: Colour.grey,
|
||||||
position: position,
|
position: position,
|
||||||
isEatable: false,
|
isEatable: false,
|
||||||
spawnPosition: { at: { x: 0, y: 0 }, direction: Direction.up },
|
spawnPosition: {at: {x: 0, y: 0}, direction: Direction.up},
|
||||||
type: CharacterType.dummy,
|
type: CharacterType.dummy,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,4 +8,4 @@ export enum Colour {
|
|||||||
grey = "grey",
|
grey = "grey",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getColours = (): Colour[] => Object.values(Colour)
|
export const getColours = (): Colour[] => Object.values(Colour);
|
||||||
|
@ -2,7 +2,8 @@ export enum Direction {
|
|||||||
left,
|
left,
|
||||||
up,
|
up,
|
||||||
right,
|
right,
|
||||||
down,
|
down
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getDirections = () => Object.values(Direction).filter(d => !isNaN(Number(d))) as Direction[]
|
export const getDirections = () => Object.values(Direction)
|
||||||
|
.filter(d => !isNaN(Number(d))) as Direction[];
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { CharacterType } from "./character"
|
import {CharacterType} from "./character";
|
||||||
import { Direction } from "./direction"
|
import {Direction} from "./direction";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 0 = empty
|
* 0 = empty
|
||||||
@ -25,32 +25,29 @@ export const customMap: GameMap = [
|
|||||||
[1, 2, 1, 0, 1, 2, 1, 0, 1, 2, 1, 0, 1, 2, 1],
|
[1, 2, 1, 0, 1, 2, 1, 0, 1, 2, 1, 0, 1, 2, 1],
|
||||||
[1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1],
|
[1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1],
|
||||||
[1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1],
|
[1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1],
|
||||||
]
|
];
|
||||||
|
|
||||||
export function getCharacterSpawns(map: GameMap): { type: CharacterType; position: DirectionalPosition }[] {
|
export function getCharacterSpawns(map: GameMap): { type: CharacterType, position: DirectionalPosition }[] {
|
||||||
const result: { type: CharacterType; position: DirectionalPosition }[] = []
|
|
||||||
|
const result: { type: CharacterType, position: DirectionalPosition }[] = [];
|
||||||
for (let row = 0; row < map.length; row++) {
|
for (let row = 0; row < map.length; row++) {
|
||||||
for (let col = 0; col < map.length; col++) {
|
for (let col = 0; col < map.length; col++) {
|
||||||
// TODO find direction
|
// TODO find direction
|
||||||
if (map[row][col] === 4) {
|
if (map[row][col] === 4) {
|
||||||
result.push({
|
result.push({type: CharacterType.ghost, position: {at: {x: col, y: row}, direction: Direction.up}});
|
||||||
type: CharacterType.ghost,
|
|
||||||
position: { at: { x: col, y: row }, direction: Direction.up },
|
|
||||||
})
|
|
||||||
} else if (map[row][col] === 5) {
|
} else if (map[row][col] === 5) {
|
||||||
result.push({
|
result.push({
|
||||||
type: CharacterType.pacMan,
|
type: CharacterType.pacMan, position: {at: {x: col, y: row}, direction: Direction.up}
|
||||||
position: { at: { x: col, y: row }, direction: Direction.up },
|
});
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPacManSpawns(map: GameMap): DirectionalPosition[] {
|
export function getPacManSpawns(map: GameMap): DirectionalPosition[] {
|
||||||
return getCharacterSpawns(map)
|
return getCharacterSpawns(map)
|
||||||
.filter(s => s.type === CharacterType.pacMan)
|
.filter(s => s.type === CharacterType.pacMan)
|
||||||
.map(s => s.position)
|
.map(s => s.position)
|
||||||
}
|
}
|
@ -1,62 +1,54 @@
|
|||||||
import { Character, CharacterType } from "./character"
|
import {Character, CharacterType} from "./character";
|
||||||
import Box from "./box"
|
import Box from "./box";
|
||||||
import { getDefaultStore } from "jotai"
|
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,
|
||||||
inGame,
|
inGame,
|
||||||
disconnected,
|
disconnected
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Player implements PlayerProps {
|
export default class Player implements PlayerProps {
|
||||||
private static store = getDefaultStore()
|
private static store = getDefaultStore();
|
||||||
public readonly username
|
public readonly username;
|
||||||
public readonly pacMan
|
public readonly pacMan;
|
||||||
public readonly colour
|
public readonly colour;
|
||||||
public readonly box
|
public readonly box;
|
||||||
public state
|
public state;
|
||||||
|
|
||||||
constructor(props: PlayerProps) {
|
constructor(props: PlayerProps) {
|
||||||
this.username = props.username
|
this.username = props.username;
|
||||||
this.colour = props.colour
|
this.colour = props.colour;
|
||||||
this.box = new Box(props.box ?? { colour: props.colour })
|
this.box = new Box(props.box ?? {colour: props.colour});
|
||||||
this.pacMan = new Character(
|
this.pacMan = new Character(props.pacMan ?? {
|
||||||
props.pacMan ?? {
|
colour: props.colour,
|
||||||
colour: props.colour,
|
type: CharacterType.pacMan
|
||||||
type: CharacterType.pacMan,
|
});
|
||||||
},
|
this.state = props.state ?? State.waitingForPlayers;
|
||||||
)
|
|
||||||
this.state = props.state ?? State.waitingForPlayers
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public isTurn(): boolean {
|
public isTurn(): boolean {
|
||||||
return Player.store.get(currentPlayerNameAtom) === this.username
|
return Player.store.get(currentPlayerNameAtom) === this.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
public addPellet(): void {
|
public addPellet(): void {
|
||||||
this.box.addPellet()
|
this.box.addPellet();
|
||||||
}
|
}
|
||||||
|
|
||||||
public addPowerPellet(): void {
|
public addPowerPellet(): void {
|
||||||
this.box.addPowerPellet()
|
this.box.addPowerPellet();
|
||||||
}
|
}
|
||||||
|
|
||||||
public stealFrom(other: Player): void {
|
public stealFrom(other: Player): void {
|
||||||
for (let i = 0; i < rules.maxStealPellets; i++) {
|
for (let i = 0; i < rules.maxStealPellets; i++) {
|
||||||
const removed = other.box.removePellet()
|
const removed = other.box.removePellet();
|
||||||
if (removed) this.box.addPellet()
|
if (removed)
|
||||||
|
this.box.addPellet();
|
||||||
}
|
}
|
||||||
Player.store.set(
|
Player.store.set(playersAtom, Player.store.get(playersAtom).map(player => player));
|
||||||
playersAtom,
|
|
||||||
Player.store.get(playersAtom).map(player => player),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { TileType } from "./tileType"
|
import {TileType} from "./tileType";
|
||||||
import { Character } from "./character"
|
import {Character} from "./character";
|
||||||
import { Direction, getDirections } from "./direction"
|
import {Direction, getDirections} from "./direction";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds all the possible positions for the character to move to
|
* Finds all the possible positions for the character to move to
|
||||||
@ -10,15 +10,10 @@ import { Direction, getDirections } from "./direction"
|
|||||||
* @param characters All the characters on the map
|
* @param characters All the characters on the map
|
||||||
* @returns An array of paths the character can move to
|
* @returns An array of paths the character can move to
|
||||||
*/
|
*/
|
||||||
export default function findPossiblePositions(
|
export default function findPossiblePositions(board: GameMap, character: Character, steps: number, characters: Character[]): Path[] {
|
||||||
board: GameMap,
|
if (!character.position || !character.spawnPosition) throw new Error("Character has no position or spawn position");
|
||||||
character: Character,
|
return findPossibleRecursive(board, character.position, steps, character, characters);
|
||||||
steps: number,
|
};
|
||||||
characters: Character[],
|
|
||||||
): Path[] {
|
|
||||||
if (!character.position || !character.spawnPosition) throw new Error("Character has no position or spawn position")
|
|
||||||
return findPossibleRecursive(board, character.position, steps, character, characters)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses recursion to move through the map and find all the possible positions
|
* Uses recursion to move through the map and find all the possible positions
|
||||||
@ -29,40 +24,38 @@ export default function findPossiblePositions(
|
|||||||
* @param characters All the characters on the map
|
* @param characters All the characters on the map
|
||||||
* @returns {Path[]} An array of paths the character can move to
|
* @returns {Path[]} An array of paths the character can move to
|
||||||
*/
|
*/
|
||||||
function findPossibleRecursive(
|
function findPossibleRecursive(map: GameMap, currentPath: Path, steps: number, character: Character, characters: Character[]): Path[] {
|
||||||
map: GameMap,
|
|
||||||
currentPath: Path,
|
const paths: Path[] = [];
|
||||||
steps: number,
|
|
||||||
character: Character,
|
|
||||||
characters: Character[],
|
|
||||||
): Path[] {
|
|
||||||
const paths: Path[] = []
|
|
||||||
if (isOutsideBoard(currentPath, map.length)) {
|
if (isOutsideBoard(currentPath, map.length)) {
|
||||||
if (character.isPacMan()) {
|
if (character.isPacMan()) {
|
||||||
return addTeleportationTiles(map, currentPath, steps, character, characters)
|
return addTeleportationTiles(map, currentPath, steps, character, characters);
|
||||||
}
|
}
|
||||||
} else if (!isWall(map, currentPath)) {
|
} else if (!isWall(map, currentPath)) {
|
||||||
|
|
||||||
if (!characterHitsAnotherCharacter(character, currentPath, characters)) {
|
if (!characterHitsAnotherCharacter(character, currentPath, characters)) {
|
||||||
if (steps <= 0) {
|
if (steps <= 0) {
|
||||||
if (!(isSpawn(map, currentPath) && !isOwnSpawn(currentPath, character))) {
|
if (!(isSpawn(map, currentPath) && !isOwnSpawn(currentPath, character))) {
|
||||||
paths.push(currentPath)
|
paths.push(currentPath);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
tryAddToPath(currentPath)
|
|
||||||
|
|
||||||
steps--
|
} else {
|
||||||
|
|
||||||
|
tryAddToPath(currentPath);
|
||||||
|
|
||||||
|
steps--;
|
||||||
for (const direction of getDirections()) {
|
for (const direction of getDirections()) {
|
||||||
paths.push(...tryMove(map, currentPath, direction, steps, character, characters))
|
paths.push(...tryMove(map, currentPath, direction, steps, character, characters));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const pacMan = ghostHitsPacMan(character, currentPath, characters)
|
const pacMan = ghostHitsPacMan(character, currentPath, characters);
|
||||||
if (pacMan instanceof Character && !isCharactersSpawn(currentPath, pacMan)) {
|
if (pacMan instanceof Character && !isCharactersSpawn(currentPath, pacMan)) {
|
||||||
paths.push(currentPath)
|
paths.push(currentPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return paths
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,7 +65,7 @@ function findPossibleRecursive(
|
|||||||
* @returns {boolean} True if the character is on its spawn, otherwise false
|
* @returns {boolean} True if the character is on its spawn, otherwise false
|
||||||
*/
|
*/
|
||||||
function isCharactersSpawn(currentPath: Path, character: Character): boolean {
|
function isCharactersSpawn(currentPath: Path, character: Character): boolean {
|
||||||
return character.spawnPosition?.at.x === currentPath.end.x && character.spawnPosition.at.y === currentPath.end.y
|
return character.spawnPosition?.at.x === currentPath.end.x && character.spawnPosition.at.y === currentPath.end.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -82,12 +75,8 @@ function isCharactersSpawn(currentPath: Path, character: Character): boolean {
|
|||||||
* @param characters All the characters on the board
|
* @param characters All the characters on the board
|
||||||
* @returns {boolean} True if the character is a ghost and hits Pac-Man, otherwise false
|
* @returns {boolean} True if the character is a ghost and hits Pac-Man, otherwise false
|
||||||
*/
|
*/
|
||||||
function ghostHitsPacMan(
|
function ghostHitsPacMan(character: Character, currentPath: Path, characters: Character[]): Character | undefined | false {
|
||||||
character: Character,
|
return character.isGhost() && characters.find(c => c.isPacMan() && c.isAt(currentPath.end));
|
||||||
currentPath: Path,
|
|
||||||
characters: Character[],
|
|
||||||
): Character | undefined | false {
|
|
||||||
return character.isGhost() && characters.find(c => c.isPacMan() && c.isAt(currentPath.end))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -98,7 +87,7 @@ function ghostHitsPacMan(
|
|||||||
* @returns {boolean} True if the character hits another character, otherwise false
|
* @returns {boolean} True if the character hits another character, otherwise false
|
||||||
*/
|
*/
|
||||||
function characterHitsAnotherCharacter(character: Character, currentPath: Path, characters: Character[]): boolean {
|
function characterHitsAnotherCharacter(character: Character, currentPath: Path, characters: Character[]): boolean {
|
||||||
return characters.find(c => c !== character && c.isAt(currentPath.end)) !== undefined
|
return characters.find(c => c !== character && c.isAt(currentPath.end)) !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -107,9 +96,9 @@ function characterHitsAnotherCharacter(character: Character, currentPath: Path,
|
|||||||
*/
|
*/
|
||||||
function tryAddToPath(currentPos: Path): void {
|
function tryAddToPath(currentPos: Path): void {
|
||||||
if (!currentPos.path) {
|
if (!currentPos.path) {
|
||||||
currentPos.path = []
|
currentPos.path = [];
|
||||||
} else if (!currentPos.path.includes(currentPos.end)) {
|
} else if (!currentPos.path.includes(currentPos.end)) {
|
||||||
currentPos.path = [...currentPos.path, currentPos.end]
|
currentPos.path = [...currentPos.path, currentPos.end];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,53 +113,39 @@ function tryAddToPath(currentPos: Path): void {
|
|||||||
* @param characters All the characters on the board
|
* @param characters All the characters on the board
|
||||||
* @returns An array of paths the character can move to
|
* @returns An array of paths the character can move to
|
||||||
*/
|
*/
|
||||||
function tryMove(
|
function tryMove(board: GameMap, path: Path, direction: Direction, steps: number, character: Character, characters: Character[]): Path[] {
|
||||||
board: GameMap,
|
|
||||||
path: Path,
|
|
||||||
direction: Direction,
|
|
||||||
steps: number,
|
|
||||||
character: Character,
|
|
||||||
characters: Character[],
|
|
||||||
): Path[] {
|
|
||||||
function getNewPosition(): Position {
|
function getNewPosition(): Position {
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
case Direction.left:
|
case Direction.left:
|
||||||
return {
|
return {
|
||||||
x: path.end.x - 1,
|
x: path.end.x - 1,
|
||||||
y: path.end.y,
|
y: path.end.y
|
||||||
}
|
};
|
||||||
case Direction.up:
|
case Direction.up:
|
||||||
return {
|
return {
|
||||||
x: path.end.x,
|
x: path.end.x,
|
||||||
y: path.end.y - 1,
|
y: path.end.y - 1
|
||||||
}
|
};
|
||||||
case Direction.right:
|
case Direction.right:
|
||||||
return {
|
return {
|
||||||
x: path.end.x + 1,
|
x: path.end.x + 1,
|
||||||
y: path.end.y,
|
y: path.end.y
|
||||||
}
|
};
|
||||||
case Direction.down:
|
case Direction.down:
|
||||||
return {
|
return {
|
||||||
x: path.end.x,
|
x: path.end.x,
|
||||||
y: path.end.y + 1,
|
y: path.end.y + 1
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.direction !== (direction + 2) % 4) {
|
if (path.direction !== (direction + 2) % 4) {
|
||||||
return findPossibleRecursive(
|
return findPossibleRecursive(board, {
|
||||||
board,
|
end: getNewPosition(), direction: direction, path: path.path
|
||||||
{
|
}, steps, character, characters);
|
||||||
end: getNewPosition(),
|
|
||||||
direction: direction,
|
|
||||||
path: path.path,
|
|
||||||
},
|
|
||||||
steps,
|
|
||||||
character,
|
|
||||||
characters,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return []
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -182,26 +157,22 @@ function tryMove(
|
|||||||
* @param characters All the characters on the map
|
* @param characters All the characters on the map
|
||||||
* @returns {Path[]} An array of paths the character can move to
|
* @returns {Path[]} An array of paths the character can move to
|
||||||
*/
|
*/
|
||||||
function addTeleportationTiles(
|
function addTeleportationTiles(board: GameMap, currentPath: Path, steps: number, character: Character, characters: Character[]): Path[] {
|
||||||
board: GameMap,
|
const possiblePositions = findTeleportationTiles(board);
|
||||||
currentPath: Path,
|
const paths: Path[] = [];
|
||||||
steps: number,
|
|
||||||
character: Character,
|
|
||||||
characters: Character[],
|
|
||||||
): Path[] {
|
|
||||||
const possiblePositions = findTeleportationTiles(board)
|
|
||||||
const paths: Path[] = []
|
|
||||||
for (const pos of possiblePositions) {
|
for (const pos of possiblePositions) {
|
||||||
|
|
||||||
function inInterval(coordinate: "x" | "y"): boolean {
|
function inInterval(coordinate: "x" | "y"): boolean {
|
||||||
return pos.end[coordinate] !== interval(0, board.length - 1, currentPath.end[coordinate])
|
return pos.end[coordinate] !== interval(0, board.length - 1, currentPath.end[coordinate])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inInterval("x") || inInterval("y")) {
|
if (inInterval("x") || inInterval("y")) {
|
||||||
pos.path = currentPath.path
|
|
||||||
paths.push(...findPossibleRecursive(board, pos, steps, character, characters))
|
pos.path = currentPath.path;
|
||||||
|
paths.push(...findPossibleRecursive(board, pos, steps, character, characters));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return paths
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -212,7 +183,7 @@ function addTeleportationTiles(
|
|||||||
* @returns {number} The value if it's between the lower and upper bounds, otherwise it returns the lower or upper bound
|
* @returns {number} The value if it's between the lower and upper bounds, otherwise it returns the lower or upper bound
|
||||||
*/
|
*/
|
||||||
function interval(lower: number, upper: number, value: number): number {
|
function interval(lower: number, upper: number, value: number): number {
|
||||||
return Math.max(Math.min(value, upper), lower)
|
return Math.max(Math.min(value, upper), lower);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -221,17 +192,18 @@ function interval(lower: number, upper: number, value: number): number {
|
|||||||
* @returns An array of paths containing the teleportation tiles
|
* @returns An array of paths containing the teleportation tiles
|
||||||
*/
|
*/
|
||||||
function findTeleportationTiles(map: GameMap): Path[] {
|
function findTeleportationTiles(map: GameMap): Path[] {
|
||||||
const possiblePositions: Path[] = []
|
const possiblePositions: Path[] = [];
|
||||||
const edge = [0, map.length - 1]
|
const edge = [0, map.length - 1];
|
||||||
|
|
||||||
for (const e of edge) {
|
for (const e of edge) {
|
||||||
for (let i = 0; i < map[e].length; i++) {
|
for (let i = 0; i < map[e].length; i++) {
|
||||||
pushPath(map, possiblePositions, i, e)
|
|
||||||
pushPath(map, possiblePositions, e, i)
|
pushPath(map, possiblePositions, i, e);
|
||||||
|
pushPath(map, possiblePositions, e, i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return possiblePositions
|
return possiblePositions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -243,10 +215,7 @@ function findTeleportationTiles(map: GameMap): Path[] {
|
|||||||
*/
|
*/
|
||||||
function pushPath(board: GameMap, possiblePositions: Path[], x: number, y: number): void {
|
function pushPath(board: GameMap, possiblePositions: Path[], x: number, y: number): void {
|
||||||
if (board[y] && board[y][x] !== TileType.wall) {
|
if (board[y] && board[y][x] !== TileType.wall) {
|
||||||
possiblePositions.push({
|
possiblePositions.push({end: {x: x, y: y}, direction: findDirection(x, y, board.length)});
|
||||||
end: { x: x, y: y },
|
|
||||||
direction: findDirection(x, y, board.length),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,17 +226,17 @@ function pushPath(board: GameMap, possiblePositions: Path[], x: number, y: numbe
|
|||||||
* @param boardSize The length of the board
|
* @param boardSize The length of the board
|
||||||
*/
|
*/
|
||||||
function findDirection(x: number, y: number, boardSize: number): Direction {
|
function findDirection(x: number, y: number, boardSize: number): Direction {
|
||||||
let direction: Direction
|
let direction: Direction;
|
||||||
if (x === 0) {
|
if (x === 0) {
|
||||||
direction = Direction.right
|
direction = Direction.right;
|
||||||
} else if (y === 0) {
|
} else if (y === 0) {
|
||||||
direction = Direction.down
|
direction = Direction.down;
|
||||||
} else if (x === boardSize - 1) {
|
} else if (x === boardSize - 1) {
|
||||||
direction = Direction.left
|
direction = Direction.left;
|
||||||
} else {
|
} else {
|
||||||
direction = Direction.up
|
direction = Direction.up;
|
||||||
}
|
}
|
||||||
return direction
|
return direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -276,8 +245,8 @@ function findDirection(x: number, y: number, boardSize: number): Direction {
|
|||||||
* @param boardSize The size of the board
|
* @param boardSize The size of the board
|
||||||
*/
|
*/
|
||||||
function isOutsideBoard(currentPos: Path, boardSize: number): boolean {
|
function isOutsideBoard(currentPos: Path, boardSize: number): boolean {
|
||||||
const pos = currentPos.end
|
const pos = currentPos.end;
|
||||||
return pos.x < 0 || pos.x >= boardSize || pos.y < 0 || pos.y >= boardSize
|
return pos.x < 0 || pos.x >= boardSize || pos.y < 0 || pos.y >= boardSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -286,8 +255,8 @@ function isOutsideBoard(currentPos: Path, boardSize: number): boolean {
|
|||||||
* @param currentPos The current position of the character
|
* @param currentPos The current position of the character
|
||||||
*/
|
*/
|
||||||
function isWall(board: GameMap, currentPos: Path): boolean {
|
function isWall(board: GameMap, currentPos: Path): boolean {
|
||||||
const pos = currentPos.end
|
const pos = currentPos.end;
|
||||||
return board[pos.y][pos.x] === TileType.wall // Shouldn't work, but it does
|
return board[pos.y][pos.x] === TileType.wall; // Shouldn't work, but it does
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -296,8 +265,8 @@ function isWall(board: GameMap, currentPos: Path): boolean {
|
|||||||
* @param currentPos The current position of the character
|
* @param currentPos The current position of the character
|
||||||
*/
|
*/
|
||||||
function isSpawn(board: GameMap, currentPos: Path) {
|
function isSpawn(board: GameMap, currentPos: Path) {
|
||||||
const pos = currentPos.end
|
const pos = currentPos.end;
|
||||||
return board[pos.y][pos.x] === TileType.pacmanSpawn || board[pos.y][pos.x] === TileType.ghostSpawn
|
return board[pos.y][pos.x] === TileType.pacmanSpawn || board[pos.y][pos.x] === TileType.ghostSpawn;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -306,7 +275,8 @@ function isSpawn(board: GameMap, currentPos: Path) {
|
|||||||
* @param character The current character
|
* @param character The current character
|
||||||
*/
|
*/
|
||||||
function isOwnSpawn(currentPos: Path, character: Character): boolean {
|
function isOwnSpawn(currentPos: Path, character: Character): boolean {
|
||||||
const pos = currentPos.end
|
const pos = currentPos.end;
|
||||||
const charPos = character.spawnPosition!.at
|
const charPos = character.spawnPosition!.at;
|
||||||
return charPos.x === pos.x && charPos.y === pos.y
|
return charPos.x === pos.x && charPos.y === pos.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,4 +4,4 @@ const rules = {
|
|||||||
maxStealPellets: 2,
|
maxStealPellets: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default rules
|
export default rules;
|
||||||
|
@ -5,4 +5,4 @@ export enum TileType {
|
|||||||
powerPellet,
|
powerPellet,
|
||||||
ghostSpawn,
|
ghostSpawn,
|
||||||
pacmanSpawn,
|
pacmanSpawn,
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react"
|
import {useState} from "react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A hook that returns a boolean value and a function to toggle it. The function can optionally be passed a boolean
|
* A hook that returns a boolean value and a function to toggle it. The function can optionally be passed a boolean
|
||||||
@ -6,7 +6,7 @@ import { useState } from "react"
|
|||||||
* @returns A tuple containing the boolean value and a function to toggle it.
|
* @returns A tuple containing the boolean value and a function to toggle it.
|
||||||
*/
|
*/
|
||||||
export default function useToggle(defaultValue = false): [boolean, (value?: boolean) => void] {
|
export default function useToggle(defaultValue = false): [boolean, (value?: boolean) => void] {
|
||||||
const [value, setValue] = useState(defaultValue)
|
const [value, setValue] = useState(defaultValue);
|
||||||
const toggleValue = (newValue?: boolean) => (newValue ? setValue(newValue) : setValue(!value))
|
const toggleValue = (newValue?: boolean) => newValue ? setValue(newValue) : setValue(!value);
|
||||||
return [value, toggleValue]
|
return [value, toggleValue];
|
||||||
}
|
}
|
@ -3,37 +3,35 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
.debug {
|
.debug {
|
||||||
@apply border border-red-500;
|
@apply border border-red-500;
|
||||||
@apply after:content-['debug'] after:absolute;
|
@apply after:content-['debug'] after:absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-center {
|
.flex-center {
|
||||||
@apply flex justify-center items-center;
|
@apply flex justify-center items-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-flex-center {
|
.inline-flex-center {
|
||||||
@apply inline-flex justify-center items-center;
|
@apply inline-flex justify-center items-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wh-full {
|
.wh-full {
|
||||||
@apply w-full h-full;
|
@apply w-full h-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
@apply text-4xl;
|
@apply text-4xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
@apply text-3xl;
|
@apply text-3xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
br {
|
br {
|
||||||
@apply my-2;
|
@apply my-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-default,
|
.button-default, button[type=submit], input[type=submit] {
|
||||||
button[type="submit"],
|
@apply bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded;
|
||||||
input[type="submit"] {
|
@apply disabled:bg-gray-500;
|
||||||
@apply bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded;
|
|
||||||
@apply disabled:bg-gray-500;
|
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,25 @@
|
|||||||
import React from "react"
|
import React from 'react';
|
||||||
import { createRoot } from "react-dom/client"
|
import {createRoot} from 'react-dom/client';
|
||||||
import { BrowserRouter } from "react-router-dom"
|
import {BrowserRouter} from 'react-router-dom';
|
||||||
import { App } from "./App"
|
import {App} from './App';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import reportWebVitals from "./reportWebVitals"
|
import reportWebVitals from './reportWebVitals';
|
||||||
import { DevTools } from "jotai-devtools"
|
import {DevTools} from "jotai-devtools";
|
||||||
import DebugMenu from "./components/debugMenu"
|
import DebugMenu from "./components/debugMenu";
|
||||||
|
|
||||||
const baseUrl = document.getElementsByTagName("base")[0].getAttribute("href")
|
const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href');
|
||||||
const rootElement = document.getElementById("root")
|
const rootElement = document.getElementById('root');
|
||||||
if (rootElement === null) throw new Error("Root element is null")
|
if (rootElement === null) throw new Error("Root element is null");
|
||||||
const root = createRoot(rootElement)
|
const root = createRoot(rootElement);
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<BrowserRouter basename={baseUrl ?? undefined}>
|
<BrowserRouter basename={baseUrl ?? undefined}>
|
||||||
<DevTools />
|
<DevTools/>
|
||||||
<DebugMenu />
|
<DebugMenu/>
|
||||||
<App />
|
<App/>
|
||||||
</BrowserRouter>,
|
</BrowserRouter>);
|
||||||
)
|
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
// If you want to start measuring performance in your app, pass a function
|
||||||
// to log results (for example: reportWebVitals(console.log))
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
reportWebVitals()
|
reportWebVitals();
|
||||||
|
@ -1,27 +1,29 @@
|
|||||||
import React, { FC } from "react"
|
import React, {FC} from "react";
|
||||||
import WebSocketService from "../websockets/WebSocketService"
|
import WebSocketService from "../websockets/WebSocketService";
|
||||||
|
|
||||||
const ws = new WebSocketService("wss://localhost:3000/api/ws")
|
const ws = new WebSocketService("wss://localhost:3000/api/ws");
|
||||||
|
|
||||||
export const Counter: FC = () => {
|
export const Counter: FC = () => {
|
||||||
const [currentCount, setCurrentCount] = React.useState(0)
|
|
||||||
|
|
||||||
function incrementCounterAndSend() {
|
const [currentCount, setCurrentCount] = React.useState(0);
|
||||||
|
|
||||||
|
async function incrementCounterAndSend() {
|
||||||
if (ws.isOpen()) {
|
if (ws.isOpen()) {
|
||||||
ws.send((currentCount + 1).toString())
|
await ws.send((currentCount + 1).toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function receiveMessage(data: MessageEvent<string>) {
|
function receiveMessage(data: MessageEvent<string>) {
|
||||||
const count = parseInt(data.data)
|
const count = parseInt(data.data);
|
||||||
if (!isNaN(count)) setCurrentCount(count)
|
if (!isNaN(count))
|
||||||
|
setCurrentCount(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
ws.onReceive = receiveMessage
|
ws.onReceive = receiveMessage;
|
||||||
ws.open()
|
ws.open();
|
||||||
return () => ws.close()
|
return () => ws.close();
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -29,13 +31,9 @@ export const Counter: FC = () => {
|
|||||||
|
|
||||||
<p>This is a simple example of a React component.</p>
|
<p>This is a simple example of a React component.</p>
|
||||||
|
|
||||||
<p aria-live="polite">
|
<p aria-live="polite">Current count: <strong>{currentCount}</strong></p>
|
||||||
Current count: <strong>{currentCount}</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button className="btn btn-primary" onClick={incrementCounterAndSend}>
|
<button className="btn btn-primary" onClick={incrementCounterAndSend}>Increment</button>
|
||||||
Increment
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
import React, { FC } from "react"
|
import React, {FC} from "react";
|
||||||
import { GameComponent } from "../components/gameComponent"
|
import {GameComponent} from "../components/gameComponent";
|
||||||
import { useAtomValue } from "jotai"
|
import {useAtomValue} from "jotai";
|
||||||
import { selectedMapAtom, thisPlayerAtom } from "../utils/state"
|
import {selectedMapAtom, thisPlayerAtom} from "../utils/state";
|
||||||
|
|
||||||
const GamePage: FC = () => {
|
const GamePage: FC = () => {
|
||||||
const player = useAtomValue(thisPlayerAtom)
|
const player = useAtomValue(thisPlayerAtom);
|
||||||
const map = useAtomValue(selectedMapAtom)
|
const map = useAtomValue(selectedMapAtom);
|
||||||
|
|
||||||
if (player && map) {
|
if (player && map) {
|
||||||
return <GameComponent player={player} map={map} />
|
return <GameComponent player={player} map={map}/>;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return null
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default GamePage
|
export default GamePage;
|
||||||
|
@ -1,35 +1,27 @@
|
|||||||
import React, { FC } from "react"
|
import React, {FC} from "react";
|
||||||
import { Link } from "react-router-dom"
|
import {Link} from "react-router-dom";
|
||||||
import { useAtomValue } from "jotai"
|
import {useAtomValue} from "jotai";
|
||||||
import { thisPlayerAtom } from "../utils/state"
|
import {thisPlayerAtom} from "../utils/state";
|
||||||
|
|
||||||
const HomePage: FC = () => {
|
const HomePage: FC = () => {
|
||||||
const player = useAtomValue(thisPlayerAtom)
|
const player = useAtomValue(thisPlayerAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"container max-w-[800px] mx-auto px-2"}>
|
<div className={"container max-w-[800px] mx-auto px-2"}>
|
||||||
<h1 className={"w-fit mx-auto"}>Hello {player?.username ?? "Player"}. Do you want to play a game?</h1>
|
<h1 className={"w-fit mx-auto"}>Hello {player?.username ?? "Player"}. Do you want to play a game?</h1>
|
||||||
<p className={"text-center mt-5"}>
|
<p className={"text-center mt-5"}>
|
||||||
{!player ? (
|
{!player ?
|
||||||
<>
|
<>Start by {" "}
|
||||||
Start by{" "}
|
<Link to={"/login"} className={"text-blue-600"}>logging in</Link>.
|
||||||
<Link to={"/login"} className={"text-blue-600"}>
|
|
||||||
logging in
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</>
|
</>
|
||||||
) : (
|
:
|
||||||
<>
|
<>Go to the {" "}
|
||||||
Go to the{" "}
|
<Link to={"/lobby"} className={"text-blue-600"}>lobby</Link> to select a game.
|
||||||
<Link to={"/lobby"} className={"text-blue-600"}>
|
|
||||||
lobby
|
|
||||||
</Link>{" "}
|
|
||||||
to select a game.
|
|
||||||
</>
|
</>
|
||||||
)}
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default HomePage
|
export default HomePage;
|
@ -1,65 +1,66 @@
|
|||||||
import React, { FC, Suspense } from "react"
|
import React, {FC, Suspense} from "react";
|
||||||
import { atom, useAtomValue } from "jotai"
|
import {atom, useAtomValue} from "jotai";
|
||||||
import { Button } from "../components/button"
|
import {Button} from "../components/button";
|
||||||
import { selectedMapAtom, thisPlayerAtom } from "../utils/state"
|
import {selectedMapAtom, thisPlayerAtom} from "../utils/state";
|
||||||
import { getData, postData } from "../utils/api"
|
import {getData, postData} from "../utils/api";
|
||||||
import { getPacManSpawns } from "../game/map"
|
import {getPacManSpawns} from "../game/map";
|
||||||
import { useNavigate } from "react-router-dom"
|
import {useNavigate} from "react-router-dom";
|
||||||
|
|
||||||
const fetchAtom = atom(async () => {
|
const fetchAtom = atom(async () => {
|
||||||
const response = await getData("/game/all")
|
const response = await getData("/game/all");
|
||||||
return (await response.json()) as Game[]
|
return await response.json() as Game[];
|
||||||
})
|
});
|
||||||
|
|
||||||
const LobbyPage: FC = () => {
|
const LobbyPage: FC = () => {
|
||||||
const thisPlayer = useAtomValue(thisPlayerAtom)
|
|
||||||
const navigate = useNavigate()
|
const thisPlayer = useAtomValue(thisPlayerAtom);
|
||||||
const map = useAtomValue(selectedMapAtom)
|
const navigate = useNavigate();
|
||||||
|
const map = useAtomValue(selectedMapAtom);
|
||||||
|
|
||||||
async function createGame(): Promise<void> {
|
async function createGame(): Promise<void> {
|
||||||
|
|
||||||
const response = await postData("/game/create", {
|
const response = await postData("/game/create", {
|
||||||
body: {
|
body: {player: thisPlayer, spawns: getPacManSpawns(map)} as CreateGameData
|
||||||
player: thisPlayer,
|
});
|
||||||
spawns: getPacManSpawns(map),
|
|
||||||
} as CreateGameData,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
const data = await response.json();
|
||||||
navigate("/game/" + data.id)
|
navigate("/game/" + data.id)
|
||||||
} else {
|
} else {
|
||||||
const data = await response.text()
|
const data = await response.text();
|
||||||
console.error("Error: ", data)
|
console.error("Error: ", data);
|
||||||
// TODO display error
|
// TODO display error
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button onClick={createGame}>New game</Button>
|
<Button onClick={createGame}>New game</Button>
|
||||||
<Suspense fallback={"Please wait"}>
|
<Suspense fallback={"Please wait"}>
|
||||||
<GameTable className={"mx-auto"} />
|
<GameTable className={"mx-auto"}/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LobbyPage
|
export default LobbyPage;
|
||||||
|
|
||||||
const GameTable: FC<ComponentProps> = ({ className }) => {
|
const GameTable: FC<ComponentProps> = ({className}) => {
|
||||||
const data = useAtomValue(fetchAtom)
|
|
||||||
const thisPlayer = useAtomValue(thisPlayerAtom)
|
const data = useAtomValue(fetchAtom);
|
||||||
const navigate = useNavigate()
|
const thisPlayer = useAtomValue(thisPlayerAtom);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
async function joinGame(gameId: string): Promise<void> {
|
async function joinGame(gameId: string): Promise<void> {
|
||||||
if (thisPlayer === undefined) throw new Error("Player is undefined")
|
if (thisPlayer === undefined) throw new Error("Player is undefined");
|
||||||
|
|
||||||
const result = await postData("/game/join/" + gameId, { body: thisPlayer })
|
const result = await postData("/game/join/" + gameId, {body: thisPlayer});
|
||||||
|
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
navigate("/game/" + gameId)
|
navigate("/game/" + gameId);
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to join game " + gameId, await result.text())
|
console.error("Failed to join game " + gameId, await result.text());
|
||||||
// TODO show error message
|
// TODO show error message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,34 +68,33 @@ const GameTable: FC<ComponentProps> = ({ className }) => {
|
|||||||
return (
|
return (
|
||||||
<table className={`rounded overflow-hidden ${className}`}>
|
<table className={`rounded overflow-hidden ${className}`}>
|
||||||
<thead className={"bg-gray-500 text-white"}>
|
<thead className={"bg-gray-500 text-white"}>
|
||||||
<tr className={"my-5"}>
|
<tr className={"my-5"}>
|
||||||
<th className={"p-2"}>Id</th>
|
<th className={"p-2"}>Id</th>
|
||||||
<th className={"p-2"}>Count</th>
|
<th className={"p-2"}>Count</th>
|
||||||
<th className={"p-2"}>State</th>
|
<th className={"p-2"}>State</th>
|
||||||
<th className={"p-2"}>Join</th>
|
<th className={"p-2"}>Join</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data?.map(game => (
|
{data?.map(game =>
|
||||||
<tr key={game.id} className={"even:bg-gray-200"}>
|
<tr key={game.id} className={"even:bg-gray-200"}>
|
||||||
<td className={"p-2"}>{game.id}</td>
|
<td className={"p-2"}>{game.id}</td>
|
||||||
<td className={"text-center"}>{game.count}</td>
|
<td className={"text-center"}>{game.count}</td>
|
||||||
<td>{game.isGameStarted ? "Closed" : "Open"}</td>
|
<td>{game.isGameStarted ? "Closed" : "Open"}</td>
|
||||||
<td className={"p-2"}>
|
<td className={"p-2"}>
|
||||||
<Button disabled={game.isGameStarted} onClick={() => joinGame(game.id)}>
|
<Button disabled={game.isGameStarted} onClick={() => joinGame(game.id)}>
|
||||||
Join
|
Join
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
)}
|
||||||
{data?.length === 0 && (
|
{
|
||||||
|
data?.length === 0 &&
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={4} className={"text-center"}>
|
<td colSpan={4} className={"text-center"}>No games found</td>
|
||||||
No games found
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
import React, { FC, FormEvent, useState } from "react"
|
import React, {FC, FormEvent, useState} from "react";
|
||||||
import { Button } from "../components/button"
|
import {Button} from "../components/button";
|
||||||
import Input from "../components/input"
|
import Input from "../components/input";
|
||||||
import { useSetAtom } from "jotai"
|
import {useSetAtom} from "jotai";
|
||||||
import { thisPlayerAtom } from "../utils/state"
|
import {thisPlayerAtom} from "../utils/state";
|
||||||
import Player from "../game/player"
|
import Player from "../game/player";
|
||||||
import { useNavigate } from "react-router-dom"
|
import {useNavigate} from "react-router-dom";
|
||||||
import { postData } from "../utils/api"
|
import {postData} from "../utils/api";
|
||||||
|
|
||||||
const LoginPage: FC = () => {
|
const LoginPage: FC = () => {
|
||||||
const setThisPlayer = useSetAtom(thisPlayerAtom)
|
|
||||||
const navigate = useNavigate()
|
const setThisPlayer = useSetAtom(thisPlayerAtom);
|
||||||
const [error, setError] = useState<string | undefined>()
|
const navigate = useNavigate();
|
||||||
|
const [error, setError] = useState<string | undefined>();
|
||||||
|
|
||||||
async function handleLogin(e: FormEvent<HTMLFormElement>): Promise<void> {
|
async function handleLogin(e: FormEvent<HTMLFormElement>): Promise<void> {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
const fields = e.currentTarget.querySelectorAll("input")
|
const fields = e.currentTarget.querySelectorAll("input");
|
||||||
|
|
||||||
let user: User = { username: "", password: "" }
|
let user: User = {username: "", password: ""};
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
user = {
|
user = {
|
||||||
...user,
|
...user,
|
||||||
@ -25,41 +26,36 @@ const LoginPage: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await postData("/player/login", {
|
const response = await postData("/player/login", {
|
||||||
body: { username: user.username, password: user.password } as User,
|
body: {username: user.username, password: user.password} as User
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = (await response.json()) as PlayerProps
|
const data = await response.json() as PlayerProps;
|
||||||
setThisPlayer(new Player(data))
|
setThisPlayer(new Player(data));
|
||||||
navigate("/lobby")
|
navigate("/lobby");
|
||||||
} else {
|
} else {
|
||||||
const data = await response.text()
|
const data = await response.text();
|
||||||
console.error(data)
|
console.error(data);
|
||||||
setError(data)
|
setError(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const username = "username",
|
const username = "username", password = "password";
|
||||||
password = "password"
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleLogin} className={"container w-fit mx-auto flex flex-col gap-2"}>
|
<form onSubmit={handleLogin} className={"container w-fit mx-auto flex flex-col gap-2"}>
|
||||||
<h1 className={"my-5"}>Login</h1>
|
<h1 className={"my-5"}>Login</h1>
|
||||||
{error && <p className={"text-red-500"}>{error}</p>}
|
{error && <p className={"text-red-500"}>{error}</p>}
|
||||||
<label htmlFor={username}>Username:</label>
|
<label htmlFor={username}>Username:</label>
|
||||||
<Input id={username} name={username} placeholder={"Username"} autoComplete={"username"} required />
|
<Input id={username} name={username} placeholder={"Username"} autoComplete={"username"} required/>
|
||||||
<label htmlFor={password}>Password:</label>
|
<label htmlFor={password}>Password:</label>
|
||||||
<Input
|
<Input id={password} name={password} type={"password"} placeholder={"Password"}
|
||||||
id={password}
|
autoComplete={"current-password"} required/>
|
||||||
name={password}
|
|
||||||
type={"password"}
|
|
||||||
placeholder={"Password"}
|
|
||||||
autoComplete={"current-password"}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Button type={"submit"}>Login</Button>
|
<Button type={"submit"}>Login</Button>
|
||||||
</form>
|
</form>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LoginPage
|
export default LoginPage;
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
const reportWebVitals = onPerfEntry => {
|
const reportWebVitals = (onPerfEntry) => {
|
||||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
getCLS(onPerfEntry)
|
getCLS(onPerfEntry);
|
||||||
getFID(onPerfEntry)
|
getFID(onPerfEntry);
|
||||||
getFCP(onPerfEntry)
|
getFCP(onPerfEntry);
|
||||||
getLCP(onPerfEntry)
|
getLCP(onPerfEntry);
|
||||||
getTTFB(onPerfEntry)
|
getTTFB(onPerfEntry);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export default reportWebVitals
|
export default reportWebVitals;
|
||||||
|
@ -1,27 +1,25 @@
|
|||||||
type FRComponent<T = ComponentProps, HTML extends HTMLElement = HTMLElement> = React.ForwardRefExoticComponent<
|
type FRComponent<T = ComponentProps, HTML extends HTMLElement = HTMLElement> = React.ForwardRefExoticComponent<React.PropsWithoutRef<T> & React.RefAttributes<HTML>>;
|
||||||
React.PropsWithoutRef<T> & React.RefAttributes<HTML>
|
|
||||||
>
|
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
className?: string
|
className?: string,
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties,
|
||||||
id?: string
|
id?: string,
|
||||||
title?: string
|
title?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChildProps extends ComponentProps {
|
interface ChildProps extends ComponentProps {
|
||||||
children?: React.JSX.Element | string
|
children?: React.JSX.Element | string,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LinkProps extends ChildProps {
|
interface LinkProps extends ChildProps {
|
||||||
to: string
|
to: string,
|
||||||
newTab?: boolean
|
newTab?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ButtonProps extends ChildProps {
|
interface ButtonProps extends ChildProps {
|
||||||
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
|
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void,
|
||||||
disabled?: boolean
|
disabled?: boolean,
|
||||||
type?: "button" | "submit" | "reset"
|
type?: "button" | "submit" | "reset",
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InputProps extends ComponentProps {
|
interface InputProps extends ComponentProps {
|
||||||
@ -33,23 +31,23 @@ interface InputProps extends ComponentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface CharacterProps {
|
interface CharacterProps {
|
||||||
colour: import("../game/colour").Colour
|
colour: import("../game/colour").Colour,
|
||||||
position?: Path | null
|
position?: Path | null,
|
||||||
isEatable?: boolean
|
isEatable?: boolean,
|
||||||
spawnPosition?: DirectionalPosition | null
|
spawnPosition?: DirectionalPosition | null,
|
||||||
type?: import("../game/character").CharacterType
|
type?: import("../game/character").CharacterType,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BoxProps {
|
interface BoxProps {
|
||||||
pellets?: number
|
pellets?: number,
|
||||||
powerPellets?: number
|
powerPellets?: number,
|
||||||
readonly colour: import("../game/colour").Colour
|
readonly colour: import("../game/colour").Colour,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PlayerProps {
|
interface PlayerProps {
|
||||||
readonly username: string
|
readonly username: string,
|
||||||
readonly pacMan?: CharacterProps
|
readonly pacMan?: CharacterProps,
|
||||||
readonly colour: import("../game/colour").Colour
|
readonly colour: import("../game/colour").Colour,
|
||||||
readonly box?: BoxProps
|
readonly box?: BoxProps,
|
||||||
state?: import("../game/player").State
|
state?: import("../game/player").State,
|
||||||
}
|
}
|
||||||
|
@ -1,68 +1,68 @@
|
|||||||
type MessageEventFunction<T = any> = (data: MessageEvent<T>) => void
|
type MessageEventFunction<T = any> = (data: MessageEvent<T>) => void;
|
||||||
|
|
||||||
type Setter<T> = React.Dispatch<React.SetStateAction<T>>
|
type Setter<T> = React.Dispatch<React.SetStateAction<T>>;
|
||||||
|
|
||||||
type GUID = `${string}-${string}-${string}-${string}-${string}`
|
type GUID = `${string}-${string}-${string}-${string}-${string}`;
|
||||||
|
|
||||||
type WebSocketData = string | ArrayBufferLike | Blob | ArrayBufferView
|
type WebSocketData = string | ArrayBufferLike | Blob | ArrayBufferView;
|
||||||
|
|
||||||
type ActionMessage<T = any> = {
|
type ActionMessage<T = any> = {
|
||||||
readonly action: import("../utils/actions").GameAction
|
readonly action: import("../utils/actions").GameAction,
|
||||||
readonly data?: T
|
readonly data?: T
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action<T> = (obj: T) => void
|
type Action<T> = (obj: T) => void;
|
||||||
|
|
||||||
type BiAction<T1, T2> = (obj1: T1, obj2: T2) => void
|
type BiAction<T1, T2> = (obj1: T1, obj2: T2) => void;
|
||||||
|
|
||||||
type Predicate<T> = (obj: T) => boolean
|
type Predicate<T> = (obj: T) => boolean;
|
||||||
|
|
||||||
type SelectedDice = {
|
type SelectedDice = {
|
||||||
value: number
|
value: number,
|
||||||
index: number
|
index: number
|
||||||
}
|
};
|
||||||
|
|
||||||
type Position = { x: number; y: number }
|
type Position = { x: number, y: number };
|
||||||
|
|
||||||
type GameMap = number[][]
|
type GameMap = number[][];
|
||||||
|
|
||||||
type DirectionalPosition = {
|
type DirectionalPosition = {
|
||||||
at: Position
|
at: Position,
|
||||||
direction: import("../game/direction").Direction
|
direction: import("../game/direction").Direction
|
||||||
}
|
}
|
||||||
|
|
||||||
type Path = {
|
type Path = {
|
||||||
path?: Position[] | null
|
path?: Position[] | null,
|
||||||
// TODO replace with DirectionalPosition
|
// TODO replace with DirectionalPosition
|
||||||
end: Position
|
end: Position,
|
||||||
direction: import("../game/direction").Direction
|
direction: import("../game/direction").Direction
|
||||||
}
|
}
|
||||||
|
|
||||||
type Game = {
|
type Game = {
|
||||||
readonly id: string
|
readonly id: string,
|
||||||
readonly count: number
|
readonly count: number,
|
||||||
readonly isGameStarted: boolean
|
readonly isGameStarted: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
type User = {
|
type User = {
|
||||||
readonly username: string
|
readonly username: string,
|
||||||
readonly password: string
|
readonly password: string,
|
||||||
readonly colour?: import("../game/colour").Colour
|
readonly colour?: import("../game/colour").Colour
|
||||||
}
|
}
|
||||||
|
|
||||||
type Api<T = ApiRequest> = (path: string, data?: ApiRequest & T) => Promise<Response>
|
type Api<T = ApiRequest> = (path: string, data?: ApiRequest & T) => Promise<Response>;
|
||||||
|
|
||||||
type ApiRequest = {
|
type ApiRequest = {
|
||||||
headers?: HeadersInit
|
headers?: HeadersInit,
|
||||||
body?: any
|
body?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
type JoinGameData = {
|
type JoinGameData = {
|
||||||
readonly username: string
|
readonly username: string,
|
||||||
readonly gameId: GUID
|
readonly gameId: GUID,
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateGameData = {
|
type CreateGameData = {
|
||||||
readonly player: PlayerProps
|
readonly player: PlayerProps,
|
||||||
readonly spawns: DirectionalPosition[]
|
readonly spawns: DirectionalPosition[],
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import Player from "../game/player"
|
import Player from "../game/player";
|
||||||
import { CharacterType, Ghost } from "../game/character"
|
import {CharacterType, Ghost} from "../game/character";
|
||||||
import { getCharacterSpawns } from "../game/map"
|
import {getCharacterSpawns} from "../game/map";
|
||||||
import { TileType } from "../game/tileType"
|
import {TileType} from "../game/tileType";
|
||||||
import { getDefaultStore } from "jotai"
|
import {getDefaultStore} from "jotai";
|
||||||
import { currentPlayerNameAtom, diceAtom, ghostsAtom, playersAtom, rollDiceButtonAtom, selectedMapAtom } from "./state"
|
import {currentPlayerNameAtom, diceAtom, ghostsAtom, playersAtom, rollDiceButtonAtom, selectedMapAtom} from "./state";
|
||||||
import { Colour } from "../game/colour"
|
import {Colour} from "../game/colour";
|
||||||
|
|
||||||
export enum GameAction {
|
export enum GameAction {
|
||||||
error,
|
error,
|
||||||
@ -17,112 +17,107 @@ export enum GameAction {
|
|||||||
// TODO add updatePellets
|
// TODO add updatePellets
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = getDefaultStore()
|
const store = getDefaultStore();
|
||||||
const map = store.get(selectedMapAtom)
|
const map = store.get(selectedMapAtom);
|
||||||
|
|
||||||
const ghostsProps: CharacterProps[] = [{ colour: Colour.purple }, { colour: Colour.purple }]
|
const ghostsProps: CharacterProps[] = [
|
||||||
let spawns = getCharacterSpawns(map).filter(spawn => spawn.type === CharacterType.ghost)
|
{colour: Colour.purple},
|
||||||
|
{colour: Colour.purple},
|
||||||
|
];
|
||||||
|
let spawns = getCharacterSpawns(map).filter(spawn => spawn.type === CharacterType.ghost);
|
||||||
ghostsProps.forEach(ghost => {
|
ghostsProps.forEach(ghost => {
|
||||||
ghost.spawnPosition = spawns.pop()?.position
|
ghost.spawnPosition = spawns.pop()?.position;
|
||||||
})
|
|
||||||
const ghosts = ghostsProps.map(props => new Ghost(props))
|
|
||||||
|
|
||||||
store.set(ghostsAtom, ghosts)
|
});
|
||||||
|
const ghosts = ghostsProps.map(props => new Ghost(props));
|
||||||
|
|
||||||
|
store.set(ghostsAtom, ghosts);
|
||||||
|
|
||||||
export const doAction: MessageEventFunction<string> = (event): void => {
|
export const doAction: MessageEventFunction<string> = (event): void => {
|
||||||
const message: ActionMessage = JSON.parse(event.data)
|
const message: ActionMessage = JSON.parse(event.data);
|
||||||
console.debug("Received message:", message)
|
console.debug("Received message:", message);
|
||||||
|
|
||||||
switch (message.action as GameAction) {
|
switch (message.action as GameAction) {
|
||||||
case GameAction.error:
|
case GameAction.error:
|
||||||
console.error("Error:", message.data)
|
console.error("Error:", message.data);
|
||||||
break
|
break;
|
||||||
case GameAction.rollDice:
|
case GameAction.rollDice:
|
||||||
setDice(message.data)
|
setDice(message.data);
|
||||||
break
|
break;
|
||||||
case GameAction.moveCharacter:
|
case GameAction.moveCharacter:
|
||||||
moveCharacter(message.data)
|
moveCharacter(message.data);
|
||||||
break
|
break;
|
||||||
case GameAction.joinGame:
|
case GameAction.joinGame:
|
||||||
joinGame(message.data)
|
joinGame(message.data);
|
||||||
break
|
break;
|
||||||
case GameAction.ready:
|
case GameAction.ready:
|
||||||
ready(message.data)
|
ready(message.data);
|
||||||
break
|
break;
|
||||||
case GameAction.nextPlayer:
|
case GameAction.nextPlayer:
|
||||||
nextPlayer(message.data)
|
nextPlayer(message.data);
|
||||||
break
|
break;
|
||||||
case GameAction.disconnect:
|
case GameAction.disconnect:
|
||||||
updatePlayers(message.data)
|
updatePlayers(message.data);
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
function setDice(data?: number[]): void {
|
function setDice(data?: number[]): void {
|
||||||
store.set(diceAtom, data)
|
store.set(diceAtom, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
type MoveCharacterData = {
|
type MoveCharacterData = { dice: number[], players: PlayerProps[], ghosts: CharacterProps[], eatenPellets: Position[] };
|
||||||
dice: number[]
|
|
||||||
players: PlayerProps[]
|
|
||||||
ghosts: CharacterProps[]
|
|
||||||
eatenPellets: Position[]
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveCharacter(data?: MoveCharacterData): void {
|
function moveCharacter(data?: MoveCharacterData): void {
|
||||||
store.set(diceAtom, data?.dice)
|
store.set(diceAtom, data?.dice);
|
||||||
updatePlayers(data?.players)
|
updatePlayers(data?.players);
|
||||||
updateGhosts(data)
|
updateGhosts(data);
|
||||||
removeEatenPellets(data)
|
removeEatenPellets(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePlayers(updatedPlayers?: PlayerProps[]): void {
|
function updatePlayers(updatedPlayers?: PlayerProps[]): void {
|
||||||
if (updatedPlayers) {
|
if (updatedPlayers) {
|
||||||
const newList: Player[] = updatedPlayers.map(p => new Player(p))
|
const newList: Player[] = updatedPlayers.map(p => new Player(p));
|
||||||
store.set(playersAtom, newList)
|
store.set(playersAtom, newList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateGhosts(data?: MoveCharacterData): void {
|
function updateGhosts(data?: MoveCharacterData): void {
|
||||||
const updatedGhosts = data?.ghosts
|
const updatedGhosts = data?.ghosts;
|
||||||
|
|
||||||
if (updatedGhosts) {
|
if (updatedGhosts) {
|
||||||
const newList: Ghost[] = updatedGhosts.map(g => new Ghost(g))
|
const newList: Ghost[] = updatedGhosts.map(g => new Ghost(g));
|
||||||
store.set(ghostsAtom, newList)
|
store.set(ghostsAtom, newList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeEatenPellets(data?: MoveCharacterData): void {
|
function removeEatenPellets(data?: MoveCharacterData): void {
|
||||||
const pellets = data?.eatenPellets
|
const pellets = data?.eatenPellets;
|
||||||
|
|
||||||
for (const pellet of pellets ?? []) {
|
for (const pellet of pellets ?? []) {
|
||||||
map[pellet.y][pellet.x] = TileType.empty
|
map[pellet.y][pellet.x] = TileType.empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function joinGame(data?: PlayerProps[]): void {
|
function joinGame(data?: PlayerProps[]): void { // TODO missing data when refreshing page
|
||||||
// TODO missing data when refreshing page
|
const playerProps = data ?? [];
|
||||||
const playerProps = data ?? []
|
spawns = getCharacterSpawns(map).filter(spawn => spawn.type === CharacterType.pacMan);
|
||||||
spawns = getCharacterSpawns(map).filter(spawn => spawn.type === CharacterType.pacMan)
|
store.set(playersAtom, playerProps.map(p => new Player(p)));
|
||||||
store.set(
|
|
||||||
playersAtom,
|
|
||||||
playerProps.map(p => new Player(p)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReadyData = { allReady: boolean; players: PlayerProps[] }
|
type ReadyData = { allReady: boolean, players: PlayerProps[] };
|
||||||
|
|
||||||
function ready(data?: ReadyData): void {
|
function ready(data?: ReadyData): void {
|
||||||
if (data) {
|
if (data) {
|
||||||
const players = data.players.map(p => new Player(p))
|
const players = data.players.map(p => new Player(p));
|
||||||
store.set(playersAtom, players)
|
store.set(playersAtom, players);
|
||||||
if (data.allReady) {
|
if (data.allReady) {
|
||||||
store.set(currentPlayerNameAtom, data.players[0].username)
|
store.set(currentPlayerNameAtom, data.players[0].username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextPlayer(currentPlayerName?: string): void {
|
function nextPlayer(currentPlayerName?: string): void {
|
||||||
store.set(currentPlayerNameAtom, currentPlayerName)
|
store.set(currentPlayerNameAtom, currentPlayerName);
|
||||||
store.set(rollDiceButtonAtom, true)
|
store.set(rollDiceButtonAtom, true);
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,12 @@
|
|||||||
/**
|
export const getData: Api = async (path, {headers} = {}) => {
|
||||||
* getData is an asynchronous function that makes an API request to retrieve data.
|
if (import.meta.env.MODE === "test") return Promise.resolve(new Response(JSON.stringify([])));
|
||||||
* 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 } = {}) => {
|
|
||||||
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, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: headers,
|
headers: headers
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const postData: Api = async (path, {body, 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 } = {}) => {
|
|
||||||
return await fetch(import.meta.env.VITE_API_HTTP + path, {
|
return await fetch(import.meta.env.VITE_API_HTTP + path, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -1,75 +1,74 @@
|
|||||||
import Player from "../game/player"
|
import Player from "../game/player";
|
||||||
import { atom } from "jotai"
|
import {atom} from "jotai";
|
||||||
import { Ghost } from "../game/character"
|
import {Ghost} from "../game/character";
|
||||||
import { customMap } from "../game/map"
|
import {customMap} from "../game/map";
|
||||||
|
|
||||||
const playerStorage = "player"
|
const playerStorage = "player";
|
||||||
/**
|
/**
|
||||||
* All players in the game.
|
* All players in the game.
|
||||||
*/
|
*/
|
||||||
export const playersAtom = atom<Player[]>([])
|
export const playersAtom = atom<Player[]>([]);
|
||||||
/**
|
/**
|
||||||
* All player characters (Pac-Man) in the game.
|
* All player characters (Pac-Man) in the game.
|
||||||
*/
|
*/
|
||||||
export const playerCharactersAtom = atom(get => get(playersAtom).map(player => player.pacMan))
|
export const playerCharactersAtom = atom(get => get(playersAtom).map(player => player.pacMan));
|
||||||
/**
|
/**
|
||||||
* All ghosts in the game.
|
* All ghosts in the game.
|
||||||
*/
|
*/
|
||||||
export const ghostsAtom = atom<Ghost[]>([])
|
export const ghostsAtom = atom<Ghost[]>([]);
|
||||||
/**
|
/**
|
||||||
* All characters in the game.
|
* All characters in the game.
|
||||||
*/
|
*/
|
||||||
export const allCharactersAtom = atom(get => [...get(playerCharactersAtom), ...get(ghostsAtom)])
|
export const allCharactersAtom = atom(get => [...get(playerCharactersAtom), ...get(ghostsAtom)]);
|
||||||
/**
|
/**
|
||||||
* The player that is currently logged in.
|
* The player that is currently logged in.
|
||||||
*/
|
*/
|
||||||
const playerAtom = atom<Player | undefined>(undefined)
|
const playerAtom = atom<Player | undefined>(undefined);
|
||||||
/**
|
/**
|
||||||
* Gets a getter and setter to get or set the player that is currently logged in.
|
* Gets a getter and setter to get or set the player that is currently logged in.
|
||||||
* Returns A tuple containing a getter and setter to get or set the player that is currently logged in.
|
* Returns A tuple containing a getter and setter to get or set the player that is currently logged in.
|
||||||
*/
|
*/
|
||||||
export const thisPlayerAtom = atom(
|
export const thisPlayerAtom = atom(get => {
|
||||||
get => {
|
const atomValue = get(playerAtom);
|
||||||
const atomValue = get(playerAtom)
|
if (!atomValue) {
|
||||||
if (!atomValue) {
|
const item = sessionStorage.getItem(playerStorage);
|
||||||
const item = sessionStorage.getItem(playerStorage)
|
if (item) {
|
||||||
if (item) {
|
const playerProps = JSON.parse(item) as PlayerProps;
|
||||||
const playerProps = JSON.parse(item) as PlayerProps
|
return new Player(playerProps);
|
||||||
return new Player(playerProps)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return atomValue
|
}
|
||||||
},
|
return atomValue;
|
||||||
(_get, set, player: Player | undefined) => {
|
}, (_get, set, player: Player | undefined) => {
|
||||||
if (player) sessionStorage.setItem(playerStorage, JSON.stringify(player))
|
if (player)
|
||||||
else sessionStorage.removeItem(playerStorage)
|
sessionStorage.setItem(playerStorage, JSON.stringify(player));
|
||||||
set(playerAtom, player)
|
else
|
||||||
},
|
sessionStorage.removeItem(playerStorage);
|
||||||
)
|
set(playerAtom, player);
|
||||||
|
});
|
||||||
/**
|
/**
|
||||||
* All dice that have been rolled.
|
* All dice that have been rolled.
|
||||||
*/
|
*/
|
||||||
export const diceAtom = atom<number[] | undefined>(undefined)
|
export const diceAtom = atom<number[] | undefined>(undefined);
|
||||||
/**
|
/**
|
||||||
* The dice that have been selected by the player.
|
* The dice that have been selected by the player.
|
||||||
*/
|
*/
|
||||||
export const selectedDiceAtom = atom<SelectedDice | undefined>(undefined)
|
export const selectedDiceAtom = atom<SelectedDice | undefined>(undefined);
|
||||||
/**
|
/**
|
||||||
* The name of the player whose turn it is.
|
* The name of the player whose turn it is.
|
||||||
*/
|
*/
|
||||||
export const currentPlayerNameAtom = atom<string | undefined>(undefined)
|
export const currentPlayerNameAtom = atom<string | undefined>(undefined);
|
||||||
/**
|
/**
|
||||||
* The player whose turn it is.
|
* The player whose turn it is.
|
||||||
*/
|
*/
|
||||||
export const currentPlayerAtom = atom<Player | undefined>(get => {
|
export const currentPlayerAtom = atom<Player | undefined>(get => {
|
||||||
const currentPlayerName = get(currentPlayerNameAtom)
|
const currentPlayerName = get(currentPlayerNameAtom);
|
||||||
return get(playersAtom).find(player => player.username === currentPlayerName)
|
return get(playersAtom).find(player => player.username === currentPlayerName);
|
||||||
})
|
});
|
||||||
/**
|
/**
|
||||||
* Whether the roll dice button should be enabled.
|
* Whether the roll dice button should be enabled.
|
||||||
*/
|
*/
|
||||||
export const rollDiceButtonAtom = atom(true)
|
export const rollDiceButtonAtom = atom(true);
|
||||||
/**
|
/**
|
||||||
* The map that is currently selected.
|
* The map that is currently selected.
|
||||||
*/
|
*/
|
||||||
export const selectedMapAtom = atom(customMap)
|
export const selectedMapAtom = atom(customMap);
|
||||||
|
@ -5,14 +5,14 @@
|
|||||||
* @returns A promise that resolves when the predicate is true.
|
* @returns A promise that resolves when the predicate is true.
|
||||||
*/
|
*/
|
||||||
export function wait(predicate: Predicate<void>, timeout: number = 50): Promise<void> {
|
export function wait(predicate: Predicate<void>, timeout: number = 50): Promise<void> {
|
||||||
return new Promise<void>(resolve => {
|
return new Promise<void>((resolve) => {
|
||||||
const f = () => {
|
const f = () => {
|
||||||
if (predicate()) {
|
if (predicate()) {
|
||||||
return resolve()
|
return resolve();
|
||||||
}
|
}
|
||||||
setTimeout(f, timeout)
|
setTimeout(f, timeout);
|
||||||
}
|
};
|
||||||
|
|
||||||
f()
|
f();
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
10
pac-man-board-game/ClientApp/src/vite-env.d.ts
vendored
10
pac-man-board-game/ClientApp/src/vite-env.d.ts
vendored
@ -1,11 +1,11 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_API_URI: string
|
readonly VITE_API_URI: string,
|
||||||
readonly VITE_API_HTTP: string
|
readonly VITE_API_HTTP: string,
|
||||||
readonly VITE_API_WS: string
|
readonly VITE_API_WS: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv
|
readonly env: ImportMetaEnv;
|
||||||
}
|
}
|
@ -1,9 +1,9 @@
|
|||||||
import { wait } from "../utils/utils"
|
import {wait} from "../utils/utils";
|
||||||
|
|
||||||
interface IWebSocket {
|
interface IWebSocket {
|
||||||
onOpen?: VoidFunction
|
onOpen?: VoidFunction,
|
||||||
onReceive?: MessageEventFunction
|
onReceive?: MessageEventFunction,
|
||||||
onClose?: VoidFunction
|
onClose?: VoidFunction,
|
||||||
onError?: VoidFunction
|
onError?: VoidFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -11,59 +11,59 @@ interface IWebSocket {
|
|||||||
* WebSocketService class provides a WebSocket client interface for easy communication with a WebSocket server.
|
* WebSocketService class provides a WebSocket client interface for easy communication with a WebSocket server.
|
||||||
*/
|
*/
|
||||||
export default class WebSocketService {
|
export default class WebSocketService {
|
||||||
private ws?: WebSocket
|
private ws?: WebSocket;
|
||||||
private readonly _url: string
|
private readonly _url: string;
|
||||||
|
|
||||||
constructor(url: string, { onOpen, onReceive, onClose, onError }: IWebSocket = {}) {
|
constructor(url: string, {onOpen, onReceive, onClose, onError}: IWebSocket = {}) {
|
||||||
this._url = url
|
this._url = url;
|
||||||
this._onOpen = onOpen
|
this._onOpen = onOpen;
|
||||||
this._onReceive = onReceive
|
this._onReceive = onReceive;
|
||||||
this._onClose = onClose
|
this._onClose = onClose;
|
||||||
this._onError = onError
|
this._onError = onError;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onOpen?: VoidFunction
|
private _onOpen?: VoidFunction;
|
||||||
|
|
||||||
set onOpen(onOpen: VoidFunction) {
|
set onOpen(onOpen: VoidFunction) {
|
||||||
this._onOpen = onOpen
|
this._onOpen = onOpen;
|
||||||
if (!this.ws) return
|
if (!this.ws) return;
|
||||||
this.ws.onopen = onOpen
|
this.ws.onopen = onOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onReceive?: MessageEventFunction
|
private _onReceive?: MessageEventFunction;
|
||||||
|
|
||||||
set onReceive(onReceive: MessageEventFunction) {
|
set onReceive(onReceive: MessageEventFunction) {
|
||||||
this._onReceive = onReceive
|
this._onReceive = onReceive;
|
||||||
if (!this.ws) return
|
if (!this.ws) return;
|
||||||
this.ws.onmessage = onReceive
|
this.ws.onmessage = onReceive;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onClose?: VoidFunction
|
private _onClose?: VoidFunction;
|
||||||
|
|
||||||
set onClose(onClose: VoidFunction) {
|
set onClose(onClose: VoidFunction) {
|
||||||
this._onClose = onClose
|
this._onClose = onClose;
|
||||||
if (!this.ws) return
|
if (!this.ws) return;
|
||||||
this.ws.onclose = onClose
|
this.ws.onclose = onClose;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onError?: VoidFunction
|
private _onError?: VoidFunction;
|
||||||
|
|
||||||
set onError(onError: VoidFunction) {
|
set onError(onError: VoidFunction) {
|
||||||
this._onError = onError
|
this._onError = onError;
|
||||||
if (!this.ws) return
|
if (!this.ws) return;
|
||||||
this.ws.onerror = onError
|
this.ws.onerror = onError;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a WebSocket connection with the specified URL and sets the event callbacks.
|
* Opens a WebSocket connection with the specified URL and sets the event callbacks.
|
||||||
*/
|
*/
|
||||||
public open(): void {
|
public open(): void {
|
||||||
if (typeof WebSocket === "undefined" || this.isConnecting()) return
|
if (typeof WebSocket === "undefined" || this.isConnecting()) return;
|
||||||
this.ws = new WebSocket(this._url)
|
this.ws = new WebSocket(this._url);
|
||||||
if (this._onOpen) this.ws.onopen = this._onOpen
|
if (this._onOpen) this.ws.onopen = this._onOpen;
|
||||||
if (this._onReceive) this.ws.onmessage = this._onReceive
|
if (this._onReceive) this.ws.onmessage = this._onReceive;
|
||||||
if (this._onClose) this.ws.onclose = this._onClose
|
if (this._onClose) this.ws.onclose = this._onClose;
|
||||||
if (this._onError) this.ws.onerror = this._onError
|
if (this._onError) this.ws.onerror = this._onError;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,8 +72,8 @@ export default class WebSocketService {
|
|||||||
* @returns {Promise<void>} - A promise that resolves when the "isOpen" condition is met.
|
* @returns {Promise<void>} - A promise that resolves when the "isOpen" condition is met.
|
||||||
*/
|
*/
|
||||||
public async waitForOpen(): Promise<void> {
|
public async waitForOpen(): Promise<void> {
|
||||||
await wait(() => this.isOpen())
|
await wait(() => this.isOpen());
|
||||||
if (this._onOpen) this.onOpen = this._onOpen
|
if (this._onOpen) this.onOpen = this._onOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -83,16 +83,16 @@ export default class WebSocketService {
|
|||||||
*/
|
*/
|
||||||
public send(data: ActionMessage | string): void {
|
public send(data: ActionMessage | string): void {
|
||||||
if (typeof data !== "string") {
|
if (typeof data !== "string") {
|
||||||
data = JSON.stringify(data)
|
data = JSON.stringify(data);
|
||||||
}
|
}
|
||||||
this.ws?.send(data)
|
this.ws?.send(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes the WebSocket connection.
|
* Closes the WebSocket connection.
|
||||||
*/
|
*/
|
||||||
public close(): void {
|
public close(): void {
|
||||||
this.ws?.close()
|
this.ws?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -100,7 +100,7 @@ export default class WebSocketService {
|
|||||||
* @returns {boolean} Returns true if the WebSocket is open, otherwise false.
|
* @returns {boolean} Returns true if the WebSocket is open, otherwise false.
|
||||||
*/
|
*/
|
||||||
public isOpen(): boolean {
|
public isOpen(): boolean {
|
||||||
return this.ws?.readyState === WebSocket?.OPEN
|
return this.ws?.readyState === WebSocket?.OPEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -109,7 +109,7 @@ export default class WebSocketService {
|
|||||||
* @returns {boolean} - Returns 'true' if the WebSocket is connecting, otherwise 'false'.
|
* @returns {boolean} - Returns 'true' if the WebSocket is connecting, otherwise 'false'.
|
||||||
*/
|
*/
|
||||||
public isConnecting(): boolean {
|
public isConnecting(): boolean {
|
||||||
return this.ws?.readyState === WebSocket?.CONNECTING
|
return this.ws?.readyState === WebSocket?.CONNECTING;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -118,6 +118,6 @@ export default class WebSocketService {
|
|||||||
* @returns {boolean} Returns true if the WebSocket connection is closed, false otherwise.
|
* @returns {boolean} Returns true if the WebSocket connection is closed, false otherwise.
|
||||||
*/
|
*/
|
||||||
public isClosed(): boolean {
|
public isClosed(): boolean {
|
||||||
return this.ws?.readyState === WebSocket?.CLOSED
|
return this.ws?.readyState === WebSocket?.CLOSED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using pacMan.DTOs;
|
|
||||||
using pacMan.Exceptions;
|
using pacMan.Exceptions;
|
||||||
using pacMan.GameStuff;
|
using pacMan.GameStuff;
|
||||||
using pacMan.GameStuff.Items;
|
using pacMan.GameStuff.Items;
|
||||||
@ -9,43 +8,37 @@ 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 : GenericController
|
||||||
: GenericController(logger, webSocketService)
|
|
||||||
{
|
{
|
||||||
[HttpGet("[action]")]
|
private readonly IActionService _actionService;
|
||||||
public override Task Connect() => base.Connect();
|
private readonly GameService _gameService;
|
||||||
|
|
||||||
/// <summary>
|
public GameController(ILogger<GameController> logger, GameService webSocketService, IActionService actionService) :
|
||||||
/// Retrieves all games from the WebSocketService.
|
base(logger, webSocketService)
|
||||||
/// </summary>
|
|
||||||
/// <returns>
|
|
||||||
/// An IEnumerable of Game objects representing all the games.
|
|
||||||
/// </returns>
|
|
||||||
[HttpGet("[action]")]
|
|
||||||
public IEnumerable<Game> All()
|
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Returning all games");
|
_gameService = webSocketService;
|
||||||
return webSocketService.Games;
|
_actionService = actionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
[HttpGet("connect")]
|
||||||
/// Adds a player to a game.
|
public override async Task Accept() => await base.Accept();
|
||||||
/// </summary>
|
|
||||||
/// <param name="gameId">The unique identifier of the game.</param>
|
[HttpGet("all")]
|
||||||
/// <param name="player">The player to be joined.</param>
|
public IEnumerable<Game> GetAllGames()
|
||||||
/// <returns>An IActionResult representing the result of the operation.</returns>
|
|
||||||
[HttpPost("[action]/{gameId:guid}")]
|
|
||||||
public IActionResult Join(Guid gameId, [FromBody] Player player) // TODO what if player is in a game already?
|
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Joining game {}", gameId);
|
Logger.Log(LogLevel.Debug, "Returning all games");
|
||||||
|
return _gameService.Games;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("join/{gameId:guid}")]
|
||||||
|
public IActionResult JoinGame(Guid gameId, [FromBody] Player player) // TODO what if player is in a game already?
|
||||||
|
{
|
||||||
|
Logger.Log(LogLevel.Debug, "Joining game {}", gameId);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
webSocketService.JoinById(gameId, player);
|
_gameService.JoinById(gameId, player);
|
||||||
return Ok("Game joined successfully");
|
return Ok("Game joined successfully");
|
||||||
}
|
}
|
||||||
catch (GameNotFoundException e)
|
catch (GameNotFoundException e)
|
||||||
@ -58,112 +51,65 @@ public class GameController(ILogger<GameController> logger, IGameService webSock
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
[HttpGet("exists/{gameId:guid}")]
|
||||||
/// Checks if a game with the specified ID exists.
|
public IActionResult GameExists(Guid gameId)
|
||||||
/// </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}")]
|
|
||||||
public IActionResult Exists(Guid gameId)
|
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Checking if game {} exists", gameId);
|
Logger.Log(LogLevel.Debug, "Checking if game {} exists", gameId);
|
||||||
return webSocketService.Games.Any(game => game.Id == gameId) ? Ok() : NotFound();
|
return _gameService.Games.Any(game => game.Id == gameId) ? Ok() : NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
[HttpPost("create")]
|
||||||
/// Creates a new game and adds the specified player to it.
|
public IActionResult CreateGame([FromBody] CreateGameData data)
|
||||||
/// </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]")]
|
|
||||||
public IActionResult Create([FromBody] CreateGameData data)
|
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Creating game");
|
Logger.Log(LogLevel.Debug, "Creating game");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var game = webSocketService.CreateAndJoin(data.Player, data.Spawns);
|
var game = _gameService.CreateAndJoin(data.Player, data.Spawns);
|
||||||
return Created($"/{game.Id}", game);
|
return Created($"/{game.Id}", game);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return BadRequest(e.Message);
|
return BadRequest(e.Message); // TODO not necessary?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Task Echo()
|
protected override Task Echo()
|
||||||
{
|
{
|
||||||
actionService.WebSocket = WebSocket ?? throw new NullReferenceException("WebSocket is null");
|
_actionService.WebSocket = WebSocket ?? throw new NullReferenceException("WebSocket is null");
|
||||||
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);
|
||||||
|
|
||||||
Logger.LogInformation("Received: {}", stringResult);
|
Logger.Log(LogLevel.Information, "Received: {}", stringResult);
|
||||||
var action = ActionMessage.FromJson(stringResult);
|
var action = ActionMessage.FromJson(stringResult);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
DoAction(action);
|
_actionService.DoAction(action);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.LogError("{}", e.Message);
|
Logger.Log(LogLevel.Error, "{}", e.Message);
|
||||||
action = new ActionMessage { Action = GameAction.Error, Data = e.Message };
|
action = new ActionMessage { Action = GameAction.Error, Data = e.Message };
|
||||||
}
|
}
|
||||||
|
|
||||||
return action.ToArraySegment();
|
return action.ToArraySegment();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
protected override async void Send(ArraySegment<byte> segment)
|
||||||
/// 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)
|
||||||
webSocketService.Send(WebSocket, segment);
|
await _gameService.Send(WebSocket, segment);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override ArraySegment<byte>? Disconnect() =>
|
protected override ArraySegment<byte>? Disconnect() =>
|
||||||
new ActionMessage { Action = GameAction.Disconnect, Data = actionService.Disconnect() }
|
new ActionMessage { Action = GameAction.Disconnect, Data = _actionService.Disconnect() }
|
||||||
.ToArraySegment();
|
.ToArraySegment();
|
||||||
|
|
||||||
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) =>
|
|
||||||
message.Data = message.Action switch
|
|
||||||
{
|
|
||||||
GameAction.RollDice => actionService.RollDice(),
|
|
||||||
GameAction.MoveCharacter => actionService.HandleMoveCharacter(message.Data),
|
|
||||||
GameAction.JoinGame => actionService.FindGame(message.Data),
|
|
||||||
GameAction.Ready => actionService.Ready(),
|
|
||||||
GameAction.NextPlayer => actionService.FindNextPlayer(),
|
|
||||||
GameAction.Disconnect => actionService.LeaveGame(),
|
|
||||||
_ => message.Data
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -1,42 +1,29 @@
|
|||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using pacMan.Services;
|
using pacMan.Interfaces;
|
||||||
|
|
||||||
namespace pacMan.Controllers;
|
namespace pacMan.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
public abstract class GenericController : ControllerBase
|
||||||
/// Represents a generic controller for handling WebSocket connections.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class GenericController(ILogger<GenericController> logger, IWebSocketService webSocketService)
|
|
||||||
: ControllerBase
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Buffer size used for processing data.
|
|
||||||
/// </summary>
|
|
||||||
private const int BufferSize = 1024 * 4;
|
private const int BufferSize = 1024 * 4;
|
||||||
|
private readonly IWebSocketService _webSocketService;
|
||||||
protected readonly ILogger<GenericController> Logger = logger;
|
protected readonly ILogger<GenericController> Logger;
|
||||||
protected WebSocket? WebSocket;
|
protected WebSocket? WebSocket;
|
||||||
|
|
||||||
/// <summary>
|
protected GenericController(ILogger<GenericController> logger, IWebSocketService webSocketService)
|
||||||
/// Establishes a WebSocket connection with the client.
|
{
|
||||||
/// </summary>
|
Logger = logger;
|
||||||
/// <remarks>
|
_webSocketService = webSocketService;
|
||||||
/// This method checks if the HTTP request is a WebSocket request. If it is, it accepts the WebSocket connection, logs
|
Logger.Log(LogLevel.Debug, "WebSocket Controller created");
|
||||||
/// 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.
|
public virtual async Task Accept()
|
||||||
/// 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()
|
|
||||||
{
|
{
|
||||||
if (HttpContext.WebSockets.IsWebSocketRequest)
|
if (HttpContext.WebSockets.IsWebSocketRequest)
|
||||||
{
|
{
|
||||||
using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||||
Logger.LogInformation("WebSocket connection established to {}", HttpContext.Connection.Id);
|
Logger.Log(LogLevel.Information, "WebSocket connection established to {}", HttpContext.Connection.Id);
|
||||||
WebSocket = webSocket;
|
WebSocket = webSocket;
|
||||||
await Echo();
|
await Echo();
|
||||||
}
|
}
|
||||||
@ -46,21 +33,16 @@ 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 == null) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
WebSocketReceiveResult? result;
|
WebSocketReceiveResult? result;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
var buffer = new byte[BufferSize];
|
var buffer = new byte[BufferSize];
|
||||||
result = await webSocketService.Receive(WebSocket, buffer);
|
result = await _webSocketService.Receive(WebSocket, buffer);
|
||||||
|
|
||||||
if (result.CloseStatus.HasValue) break;
|
if (result.CloseStatus.HasValue) break;
|
||||||
|
|
||||||
@ -68,27 +50,23 @@ public abstract class GenericController(ILogger<GenericController> logger, IWebS
|
|||||||
|
|
||||||
Send(segment);
|
Send(segment);
|
||||||
} while (true);
|
} while (true);
|
||||||
|
|
||||||
var disconnectSegment = Disconnect();
|
var disconnectSegment = Disconnect();
|
||||||
if (disconnectSegment is not null)
|
if (disconnectSegment != null)
|
||||||
SendDisconnectMessage((ArraySegment<byte>)disconnectSegment);
|
SendDisconnectMessage((ArraySegment<byte>)disconnectSegment);
|
||||||
|
|
||||||
await webSocketService.Close(WebSocket, result.CloseStatus.Value, result.CloseStatusDescription);
|
await _webSocketService.Close(WebSocket, result.CloseStatus.Value, result.CloseStatusDescription);
|
||||||
}
|
}
|
||||||
catch (WebSocketException e)
|
catch (WebSocketException e)
|
||||||
{
|
{
|
||||||
Logger.LogError("{}", e.Message);
|
Logger.Log(LogLevel.Error, "{}", e.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
protected virtual async void Send(ArraySegment<byte> segment)
|
||||||
/// 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 == null) return;
|
||||||
webSocketService.Send(WebSocket, segment);
|
await _webSocketService.Send(WebSocket, segment);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract ArraySegment<byte> Run(WebSocketReceiveResult result, byte[] data);
|
protected abstract ArraySegment<byte> Run(WebSocketReceiveResult result, byte[] data);
|
||||||
|
@ -6,22 +6,21 @@ using pacMan.GameStuff.Items;
|
|||||||
namespace pacMan.Controllers;
|
namespace pacMan.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]/[action]")]
|
[Route("api/[controller]")]
|
||||||
public class PlayerController(UserService userService) : ControllerBase
|
public class PlayerController : ControllerBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
private readonly UserService _userService;
|
||||||
/// Logs in a user.
|
|
||||||
/// </summary>
|
public PlayerController(UserService userService) => _userService = userService;
|
||||||
/// <param name="user">The user object containing the username and password.</param>
|
|
||||||
/// <returns>Returns an IActionResult indicating the login result.</returns>
|
[HttpPost("login")]
|
||||||
[HttpPost]
|
|
||||||
public async Task<IActionResult> Login([FromBody] User user)
|
public async Task<IActionResult> Login([FromBody] User user)
|
||||||
{
|
{
|
||||||
var result = await userService.Login(user.Username, user.Password);
|
var result = await _userService.Login(user.Username, user.Password);
|
||||||
if (result is null) return Unauthorized("Invalid username or password");
|
if (result is null) return Unauthorized("Invalid username or password");
|
||||||
return Ok((Player)result);
|
return Ok((Player)result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost("register")]
|
||||||
public Task<IActionResult> Register([FromBody] User user) => throw new NotSupportedException();
|
public async Task<IActionResult> Register([FromBody] User user) => throw new NotSupportedException();
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,12 @@ namespace pacMan.Controllers;
|
|||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class WsController(ILogger<WsController> logger, IWebSocketService gameService) :
|
public class WsController : GenericController
|
||||||
GenericController(logger, gameService)
|
|
||||||
{
|
{
|
||||||
|
public WsController(ILogger<WsController> logger, GameService gameService) : base(logger, gameService) { }
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public override async Task Connect() => await base.Connect();
|
public override async Task Accept() => await base.Accept();
|
||||||
|
|
||||||
protected override ArraySegment<byte> Run(WebSocketReceiveResult result, byte[] data)
|
protected override ArraySegment<byte> Run(WebSocketReceiveResult result, byte[] data)
|
||||||
{
|
{
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
using pacMan.GameStuff;
|
|
||||||
using pacMan.GameStuff.Items;
|
|
||||||
|
|
||||||
namespace pacMan.DTOs;
|
|
||||||
|
|
||||||
public readonly record struct JoinGameData(
|
|
||||||
[property: JsonInclude, JsonPropertyName("username"), JsonRequired]
|
|
||||||
string Username,
|
|
||||||
[property: JsonInclude, JsonPropertyName("gameId"), JsonRequired]
|
|
||||||
Guid GameId
|
|
||||||
)
|
|
||||||
{
|
|
||||||
public void Deconstruct(out string username, out Guid gameId) => (username, gameId) = (Username, GameId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly record struct CreateGameData(
|
|
||||||
[property: JsonInclude, JsonPropertyName("player"), JsonRequired]
|
|
||||||
Player Player,
|
|
||||||
[property: JsonInclude, JsonPropertyName("spawns"), JsonRequired]
|
|
||||||
Queue<DirectionalPosition> Spawns
|
|
||||||
);
|
|
||||||
|
|
||||||
public readonly record struct ReadyData(
|
|
||||||
[property: JsonInclude, JsonPropertyName("allReady"), JsonRequired]
|
|
||||||
bool AllReady,
|
|
||||||
[property: JsonInclude, JsonPropertyName("players"), JsonRequired]
|
|
||||||
IEnumerable<Player> Players
|
|
||||||
);
|
|
||||||
|
|
||||||
public readonly record struct MovePlayerData(
|
|
||||||
[property: JsonInclude, JsonPropertyName("players"), JsonRequired]
|
|
||||||
List<Player> Players,
|
|
||||||
[property: JsonInclude, JsonPropertyName("ghosts"), JsonRequired]
|
|
||||||
List<Character> Ghosts,
|
|
||||||
[property: JsonInclude, JsonPropertyName("dice"), JsonRequired]
|
|
||||||
List<int> Dice,
|
|
||||||
[property: JsonInclude, JsonPropertyName("eatenPellets"), JsonRequired]
|
|
||||||
List<Position> EatenPellets
|
|
||||||
);
|
|
@ -1,9 +1,9 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
EXPOSE 443
|
EXPOSE 443
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
|
||||||
|
|
||||||
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
namespace pacMan.Exceptions;
|
|
||||||
|
|
||||||
public class GameNotFoundException(string message = "Game not found") : Exception(message);
|
|
||||||
|
|
||||||
public class GameNotPlayableException(string message = "Game is not allowed to be played") : Exception(message);
|
|
6
pac-man-board-game/Exceptions/GameNotFoundException.cs
Normal file
6
pac-man-board-game/Exceptions/GameNotFoundException.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace pacMan.Exceptions;
|
||||||
|
|
||||||
|
public class GameNotFoundException : Exception
|
||||||
|
{
|
||||||
|
public GameNotFoundException(string message = "Game not found") : base(message) { }
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
namespace pacMan.Exceptions;
|
||||||
|
|
||||||
|
public class GameNotPlayableException : Exception
|
||||||
|
{
|
||||||
|
public GameNotPlayableException(string message = "Game is not allowed to be played") : base(message) { }
|
||||||
|
}
|
@ -1,3 +0,0 @@
|
|||||||
namespace pacMan.Exceptions;
|
|
||||||
|
|
||||||
public class PlayerNotFoundException(string? message = "Player not found") : Exception(message);
|
|
6
pac-man-board-game/Exceptions/PlayerNotFoundException.cs
Normal file
6
pac-man-board-game/Exceptions/PlayerNotFoundException.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace pacMan.Exceptions;
|
||||||
|
|
||||||
|
public class PlayerNotFoundException : Exception
|
||||||
|
{
|
||||||
|
public PlayerNotFoundException(string? message = "Player not found") : base(message) { }
|
||||||
|
}
|
@ -3,9 +3,6 @@ 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,
|
||||||
@ -17,23 +14,13 @@ 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)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ActionMessage : ActionMessage<dynamic>;
|
public class ActionMessage : ActionMessage<dynamic> { }
|
||||||
|
@ -8,7 +8,6 @@ public class Character : IEquatable<Character>
|
|||||||
|
|
||||||
[JsonPropertyName("position")] public MovePath? Position { get; set; }
|
[JsonPropertyName("position")] public MovePath? Position { get; set; }
|
||||||
|
|
||||||
// TODO isEdible
|
|
||||||
[JsonInclude]
|
[JsonInclude]
|
||||||
[JsonPropertyName("isEatable")]
|
[JsonPropertyName("isEatable")]
|
||||||
public bool IsEatable { get; set; } = true;
|
public bool IsEatable { get; set; } = true;
|
||||||
@ -32,12 +31,9 @@ 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, Type);
|
public override int GetHashCode() => HashCode.Combine(Colour, Position, IsEatable, SpawnPosition, (int?)Type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the types of characters in a game.
|
|
||||||
/// </summary>
|
|
||||||
public enum CharacterType
|
public enum CharacterType
|
||||||
{
|
{
|
||||||
PacMan,
|
PacMan,
|
||||||
|
@ -4,17 +4,10 @@ namespace pacMan.GameStuff.Items;
|
|||||||
|
|
||||||
public class Box : IEquatable<Box>
|
public class Box : IEquatable<Box>
|
||||||
{
|
{
|
||||||
[JsonInclude]
|
[JsonPropertyName("pellets")] public int Pellets { get; init; }
|
||||||
[JsonPropertyName("pellets")]
|
[JsonPropertyName("powerPellets")] public int PowerPellet { get; init; }
|
||||||
public int Pellets { get; init; }
|
|
||||||
|
|
||||||
[JsonInclude]
|
[JsonPropertyName("colour")] public required string Colour { get; init; }
|
||||||
[JsonPropertyName("powerPellets")]
|
|
||||||
public int PowerPellet { get; init; }
|
|
||||||
|
|
||||||
[JsonInclude]
|
|
||||||
[JsonPropertyName("colour")]
|
|
||||||
public required string Colour { get; init; }
|
|
||||||
|
|
||||||
public bool Equals(Box? other)
|
public bool Equals(Box? other)
|
||||||
{
|
{
|
||||||
|
@ -6,14 +6,7 @@ public class Dice
|
|||||||
{
|
{
|
||||||
private readonly Random _random = new();
|
private readonly Random _random = new();
|
||||||
|
|
||||||
/// <summary>
|
[JsonInclude] public int Value { get; private set; }
|
||||||
/// 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);
|
||||||
}
|
}
|
||||||
|
@ -2,24 +2,18 @@ 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 Dice(), new Dice()];
|
private readonly List<Dice> _dices;
|
||||||
|
|
||||||
/// <summary>
|
public DiceCup() =>
|
||||||
/// Gets a list of integer values representing the values of the dices.
|
_dices = new List<Dice>
|
||||||
/// </summary>
|
{
|
||||||
/// <value>
|
new(),
|
||||||
/// A list of integer values representing the values of the dices.
|
new()
|
||||||
/// </value>
|
};
|
||||||
[JsonInclude]
|
|
||||||
public List<int> Values => _dices.Select(dice => dice.Value).ToList();
|
[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());
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,6 @@ 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,
|
||||||
@ -14,9 +11,6 @@ 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; }
|
||||||
|
@ -2,9 +2,6 @@ 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]
|
||||||
@ -22,11 +19,6 @@ 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()
|
||||||
{
|
{
|
||||||
@ -41,12 +33,9 @@ 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(End, (int)Direction);
|
public override int GetHashCode() => HashCode.Combine(Path, 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; }
|
||||||
@ -70,9 +59,6 @@ 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,
|
||||||
@ -81,9 +67,6 @@ 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; }
|
||||||
@ -97,11 +80,6 @@ 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()
|
||||||
{
|
{
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
namespace pacMan.GameStuff;
|
namespace pacMan.GameStuff;
|
||||||
|
|
||||||
/// <summary>
|
public class Rules
|
||||||
/// The Rules class holds constant values related to the game rules.
|
|
||||||
/// </summary>
|
|
||||||
public static class Rules
|
|
||||||
{
|
{
|
||||||
public const int MinPlayers = 2;
|
public const int MinPlayers = 2;
|
||||||
public const int MaxPlayers = 4;
|
public const int MaxPlayers = 4;
|
||||||
|
10
pac-man-board-game/Interfaces/IWebSocketService.cs
Normal file
10
pac-man-board-game/Interfaces/IWebSocketService.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using System.Net.WebSockets;
|
||||||
|
|
||||||
|
namespace pacMan.Interfaces;
|
||||||
|
|
||||||
|
public interface IWebSocketService
|
||||||
|
{
|
||||||
|
Task Send(WebSocket webSocket, ArraySegment<byte> segment);
|
||||||
|
Task<WebSocketReceiveResult> Receive(WebSocket webSocket, byte[] buffer);
|
||||||
|
Task Close(WebSocket webSocket, WebSocketCloseStatus closeStatus, string? closeStatusDescription);
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using DAL.Database.Service;
|
using DAL.Database.Service;
|
||||||
|
using pacMan.Interfaces;
|
||||||
using pacMan.Services;
|
using pacMan.Services;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@ -7,7 +8,6 @@ var builder = WebApplication.CreateBuilder(args);
|
|||||||
|
|
||||||
builder.Services.AddControllersWithViews();
|
builder.Services.AddControllersWithViews();
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddSingleton<IGameService, GameService>()
|
|
||||||
.AddSingleton<IWebSocketService, WebSocketService>()
|
.AddSingleton<IWebSocketService, WebSocketService>()
|
||||||
.AddSingleton<GameService>()
|
.AddSingleton<GameService>()
|
||||||
.AddScoped<UserService>()
|
.AddScoped<UserService>()
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using pacMan.DTOs;
|
using System.Text.Json.Serialization;
|
||||||
using pacMan.Exceptions;
|
using pacMan.Exceptions;
|
||||||
|
using pacMan.GameStuff;
|
||||||
using pacMan.GameStuff.Items;
|
using pacMan.GameStuff.Items;
|
||||||
|
|
||||||
namespace pacMan.Services;
|
namespace pacMan.Services;
|
||||||
@ -10,10 +11,11 @@ public interface IActionService
|
|||||||
{
|
{
|
||||||
Player Player { set; }
|
Player Player { set; }
|
||||||
Game? Game { get; set; }
|
Game? Game { get; set; }
|
||||||
WebSocket WebSocket { set; }
|
WebSocket? WebSocket { set; }
|
||||||
|
void DoAction(ActionMessage message);
|
||||||
List<int> RollDice();
|
List<int> RollDice();
|
||||||
List<Player> FindGame(JsonElement? jsonElement);
|
List<Player> FindGame(JsonElement? jsonElement);
|
||||||
MovePlayerData HandleMoveCharacter(JsonElement? jsonElement);
|
object? HandleMoveCharacter(JsonElement? jsonElement);
|
||||||
ReadyData Ready();
|
ReadyData Ready();
|
||||||
string FindNextPlayer();
|
string FindNextPlayer();
|
||||||
List<Player> LeaveGame();
|
List<Player> LeaveGame();
|
||||||
@ -21,65 +23,68 @@ public interface IActionService
|
|||||||
List<Player>? Disconnect();
|
List<Player>? Disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public class ActionService : IActionService
|
||||||
/// Provides various actions that can be performed in a game
|
|
||||||
/// </summary>
|
|
||||||
public class ActionService(ILogger<ActionService> logger, IGameService gameService) : IActionService
|
|
||||||
{
|
{
|
||||||
public WebSocket WebSocket { private get; set; } = null!;
|
private readonly GameService _gameService;
|
||||||
|
private readonly ILogger<ActionService> _logger;
|
||||||
|
|
||||||
|
public ActionService(ILogger<ActionService> logger, GameService gameService)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_gameService = gameService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebSocket? WebSocket { private get; set; }
|
||||||
|
|
||||||
public Game? Game { get; set; }
|
public Game? Game { get; set; }
|
||||||
|
|
||||||
public Player? Player { get; set; }
|
public Player? Player { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
public void DoAction(ActionMessage message)
|
||||||
/// Rolls the dice and returns the result. If the game is null, an empty list is returned.
|
{
|
||||||
/// </summary>
|
message.Data = message.Action switch
|
||||||
/// <returns>A list of integers representing the values rolled on the dice.</returns>
|
{
|
||||||
|
GameAction.RollDice => RollDice(),
|
||||||
|
GameAction.MoveCharacter => HandleMoveCharacter(message.Data),
|
||||||
|
GameAction.JoinGame => FindGame(message.Data),
|
||||||
|
GameAction.Ready => Ready(),
|
||||||
|
GameAction.NextPlayer => FindNextPlayer(),
|
||||||
|
GameAction.Disconnect => LeaveGame(),
|
||||||
|
_ => message.Data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public List<int> RollDice()
|
public List<int> RollDice()
|
||||||
{
|
{
|
||||||
Game?.DiceCup.Roll();
|
Game?.DiceCup.Roll();
|
||||||
var rolls = Game?.DiceCup.Values ?? [];
|
var rolls = Game?.DiceCup.Values ?? new List<int>();
|
||||||
logger.LogInformation("Rolled [{}]", string.Join(", ", rolls));
|
_logger.Log(LogLevel.Information, "Rolled [{}]", string.Join(", ", rolls));
|
||||||
|
|
||||||
return rolls;
|
return rolls;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public object? HandleMoveCharacter(JsonElement? jsonElement)
|
||||||
/// 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)
|
|
||||||
{
|
{
|
||||||
var data = jsonElement?.Deserialize<MovePlayerData>() ?? throw new NullReferenceException("Data is null");
|
if (Game != null && jsonElement.HasValue)
|
||||||
if (Game is not null)
|
|
||||||
{
|
{
|
||||||
Game.Ghosts = data.Ghosts;
|
Game.Ghosts = jsonElement.Value.GetProperty("ghosts").Deserialize<List<Character>>() ??
|
||||||
Game.Players = data.Players;
|
throw new NullReferenceException("Ghosts is null");
|
||||||
|
Game.Players = jsonElement.Value.GetProperty("players").Deserialize<List<Player>>() ??
|
||||||
|
throw new NullReferenceException("Players is null");
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return jsonElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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 data = jsonElement?.Deserialize<JoinGameData>() ?? throw new NullReferenceException("Data is null");
|
||||||
jsonElement?.Deserialize<JoinGameData>() ?? throw new NullReferenceException("Data is null");
|
|
||||||
|
|
||||||
var game = gameService.FindGameById(gameId) ??
|
var game = _gameService.Games.FirstOrDefault(game => game.Id == data.GameId) ??
|
||||||
throw new GameNotFoundException($"Game was not found, id \"{gameId}\" does not exist");
|
throw new GameNotFoundException($"Game was not found, id \"{data.GameId}\" does not exist");
|
||||||
|
|
||||||
var player = game.FindPlayerByUsername(username) ??
|
var player = game.Players.Find(p => p.Username == data.Username)
|
||||||
throw new PlayerNotFoundException($"Player \"{username}\" was not found in game");
|
?? throw new PlayerNotFoundException($"Player \"{data.Username}\" was not found in game");
|
||||||
|
|
||||||
player.State = game.IsGameStarted ? State.InGame : State.WaitingForPlayers; // TODO doesn't work anymore
|
player.State = game.IsGameStarted ? State.InGame : State.WaitingForPlayers; // TODO doesn't work anymore
|
||||||
Player = player;
|
Player = player;
|
||||||
@ -89,18 +94,10 @@ public class ActionService(ILogger<ActionService> logger, IGameService gameServi
|
|||||||
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 == null || Game == null)
|
||||||
throw new PlayerNotFoundException("Player not found, please create a new player");
|
throw new PlayerNotFoundException("Player not found, please create a new player");
|
||||||
if (Game is null)
|
|
||||||
throw new GameNotFoundException();
|
|
||||||
|
|
||||||
var players = Game.SetReady(Player.Username).ToArray();
|
var players = Game.SetReady(Player.Username).ToArray();
|
||||||
// TODO roll to start
|
// TODO roll to start
|
||||||
@ -110,55 +107,61 @@ public class ActionService(ILogger<ActionService> logger, IGameService gameServi
|
|||||||
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 == null || Player == null) throw new NullReferenceException("Game or Player is null");
|
||||||
if (Player is null) throw new NullReferenceException("Player is null");
|
|
||||||
Game.RemovePlayer(Player.Username);
|
Game.RemovePlayer(Player.Username);
|
||||||
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 == null) return null;
|
||||||
Player.State = State.Disconnected;
|
Player.State = State.Disconnected;
|
||||||
if (Game is not null) Game.Connections -= SendSegment;
|
if (Game != null) Game.Connections -= SendSegment;
|
||||||
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);
|
||||||
|
|
||||||
/// <summary>
|
private async Task SendSegment(ArraySegment<byte> segment)
|
||||||
/// Sends an array segment of bytes through the WebSocket connection.
|
{
|
||||||
/// </summary>
|
if (WebSocket != null) await _gameService.Send(WebSocket, segment);
|
||||||
/// <param name="segment">The array segment of bytes to send.</param>
|
else await Task.FromCanceled(new CancellationToken(true));
|
||||||
/// <returns>A task that represents the asynchronous send operation.</returns>
|
}
|
||||||
private Task SendSegment(ArraySegment<byte> segment) => gameService.Send(WebSocket, segment);
|
}
|
||||||
|
|
||||||
|
public struct JoinGameData
|
||||||
|
{
|
||||||
|
[JsonInclude]
|
||||||
|
[JsonPropertyName("username")]
|
||||||
|
public required string Username { get; init; }
|
||||||
|
|
||||||
|
[JsonInclude]
|
||||||
|
[JsonPropertyName("gameId")]
|
||||||
|
public required Guid GameId { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct CreateGameData
|
||||||
|
{
|
||||||
|
[JsonInclude]
|
||||||
|
[JsonPropertyName("player")]
|
||||||
|
public required Player Player { get; init; }
|
||||||
|
|
||||||
|
[JsonInclude]
|
||||||
|
[JsonPropertyName("spawns")]
|
||||||
|
public required Queue<DirectionalPosition> Spawns { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ReadyData
|
||||||
|
{
|
||||||
|
[JsonInclude]
|
||||||
|
[JsonPropertyName("allReady")]
|
||||||
|
public required bool AllReady { get; init; }
|
||||||
|
|
||||||
|
[JsonInclude]
|
||||||
|
[JsonPropertyName("players")]
|
||||||
|
public required IEnumerable<Player> Players { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -5,26 +5,20 @@ using pacMan.GameStuff.Items;
|
|||||||
|
|
||||||
namespace pacMan.Services;
|
namespace pacMan.Services;
|
||||||
|
|
||||||
/// <summary>
|
public class Game
|
||||||
/// Represents a game instance.
|
|
||||||
/// </summary>
|
|
||||||
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 = [];
|
private List<Player> _players = new();
|
||||||
|
|
||||||
|
public Game(Queue<DirectionalPosition> spawns) => Spawns = spawns;
|
||||||
|
|
||||||
[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
|
||||||
{
|
{
|
||||||
get => _players;
|
get => _players;
|
||||||
// TODO what if different length?
|
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (_players.Count > 0)
|
if (_players.Count > 0)
|
||||||
@ -40,36 +34,17 @@ public class Game(Queue<DirectionalPosition> spawns)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonIgnore] public List<Character> Ghosts { get; set; } = []; // TODO include
|
[JsonIgnore] public List<Character> Ghosts { get; set; } = new(); // TODO include
|
||||||
|
|
||||||
/// <summary>
|
[JsonIgnore] private Queue<DirectionalPosition> Spawns { get; }
|
||||||
/// 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?
|
||||||
/// <summary>
|
[JsonInclude] public bool IsGameStarted => Count > 0 && Players.Any(player => player.State is State.InGame);
|
||||||
/// Whether 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
|
||||||
@ -78,7 +53,7 @@ public class Game(Queue<DirectionalPosition> spawns)
|
|||||||
}
|
}
|
||||||
catch (DivideByZeroException)
|
catch (DivideByZeroException)
|
||||||
{
|
{
|
||||||
throw new PlayerNotFoundException("There are no players in the game.");
|
throw new InvalidOperationException("There are no players in the game.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Players[_currentPlayerIndex];
|
return Players[_currentPlayerIndex];
|
||||||
@ -86,22 +61,8 @@ 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)
|
||||||
@ -125,10 +86,6 @@ 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;
|
||||||
@ -137,17 +94,8 @@ 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>All players</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);
|
||||||
@ -157,25 +105,11 @@ 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 != State.Ready)) return false;
|
||||||
|
|
||||||
foreach (var player in Players) player.State = State.InGame;
|
foreach (var player in Players) player.State = State.InGame;
|
||||||
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) =>
|
|
||||||
Players.FirstOrDefault(player => player.Username == username);
|
|
||||||
}
|
}
|
||||||
|
@ -4,27 +4,20 @@ using pacMan.GameStuff.Items;
|
|||||||
|
|
||||||
namespace pacMan.Services;
|
namespace pacMan.Services;
|
||||||
|
|
||||||
public interface IGameService : IWebSocketService
|
|
||||||
{
|
|
||||||
SynchronizedCollection<Game> Games { get; }
|
|
||||||
Game JoinById(Guid id, Player player);
|
|
||||||
Game CreateAndJoin(Player player, Queue<DirectionalPosition> spawns);
|
|
||||||
Game? FindGameById(Guid id);
|
|
||||||
Game? FindGameByUsername(string username);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The GameService class provides functionality for managing games in a WebSocket environment. It inherits from the
|
/// The GameService class provides functionality for managing games in a WebSocket environment. It inherits from the
|
||||||
/// WebSocketService class.
|
/// WebSocketService class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class GameService(ILogger<WebSocketService> logger) : WebSocketService(logger), IGameService
|
public class GameService : WebSocketService
|
||||||
{
|
{
|
||||||
|
public GameService(ILogger<GameService> logger) : base(logger) { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A thread-safe collection (SynchronizedCollection) of "Game" objects. Utilized for managing multiple game instances
|
/// A thread-safe collection (SynchronizedCollection) of "Game" objects. Utilized for managing multiple game instances
|
||||||
/// simultaneously.
|
/// simultaneously.
|
||||||
/// It represents all the current games being managed by GameService.
|
/// It represents all the current games being managed by GameService.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SynchronizedCollection<Game> Games { get; } = [];
|
public SynchronizedCollection<Game> Games { get; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This method tries to find a game with the specified id, add a player to it and return the updated game.
|
/// This method tries to find a game with the specified id, add a player to it and return the updated game.
|
||||||
@ -64,20 +57,8 @@ public class GameService(ILogger<WebSocketService> logger) : WebSocketService(lo
|
|||||||
return game;
|
return game;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public Game? FindGameByUsername(string username)
|
||||||
/// Finds a game by its ID.
|
{
|
||||||
/// </summary>
|
return Games.FirstOrDefault(game => game.Players.Exists(player => player.Username == username));
|
||||||
/// <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);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Finds a game by the given 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));
|
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,20 @@
|
|||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
|
using pacMan.Interfaces;
|
||||||
using pacMan.Utils;
|
using pacMan.Utils;
|
||||||
|
|
||||||
namespace pacMan.Services;
|
namespace pacMan.Services;
|
||||||
|
|
||||||
public interface IWebSocketService
|
|
||||||
{
|
|
||||||
Task Send(WebSocket webSocket, ArraySegment<byte> segment);
|
|
||||||
Task<WebSocketReceiveResult> Receive(WebSocket webSocket, byte[] buffer);
|
|
||||||
Task Close(WebSocket webSocket, WebSocketCloseStatus closeStatus, string? closeStatusDescription);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public class WebSocketService : IWebSocketService
|
||||||
/// WebSocketService class provides methods to send, receive and close a WebSocket connection.
|
|
||||||
/// </summary>
|
|
||||||
public class WebSocketService(ILogger<WebSocketService> logger) : IWebSocketService
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
protected readonly ILogger<WebSocketService> Logger;
|
||||||
/// Sends the specified byte array as a text message through the WebSocket connection.
|
|
||||||
/// </summary>
|
public WebSocketService(ILogger<WebSocketService> logger)
|
||||||
/// <param name="webSocket">The WebSocket connection.</param>
|
{
|
||||||
/// <param name="segment">The byte array to send.</param>
|
Logger = logger;
|
||||||
/// <returns>
|
logger.Log(LogLevel.Debug, "WebSocket Service created");
|
||||||
/// 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(
|
||||||
@ -36,34 +23,18 @@ public class WebSocketService(ILogger<WebSocketService> logger) : IWebSocketServ
|
|||||||
true,
|
true,
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
logger.LogDebug("Message sent through WebSocket");
|
Logger.Log(LogLevel.Debug, "Message sent to 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);
|
||||||
logger.LogDebug(
|
Logger.Log(LogLevel.Debug,
|
||||||
"Message \"{}\" received from WebSocket",
|
"Message \"{}\" received from WebSocket",
|
||||||
buffer.GetString(result.Count));
|
buffer.GetString(result.Count));
|
||||||
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(
|
||||||
@ -71,6 +42,6 @@ public class WebSocketService(ILogger<WebSocketService> logger) : IWebSocketServ
|
|||||||
closeStatusDescription,
|
closeStatusDescription,
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
logger.LogInformation("WebSocket connection closed");
|
Logger.Log(LogLevel.Information, "WebSocket connection closed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,24 +6,13 @@ 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);
|
||||||
// Removes invalid characters from the string
|
// Removes invalid characters from the string
|
||||||
return InvalidCharacters().Replace(s, string.Empty);
|
return InvalidCharacters().Replace(s, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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);
|
||||||
@ -31,10 +20,6 @@ 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();
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||||
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
|
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
|
||||||
@ -13,71 +13,70 @@
|
|||||||
<RootNamespace>pacMan</RootNamespace>
|
<RootNamespace>pacMan</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
<LangVersion>13</LangVersion>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="8.0.0"/>
|
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="7.0.10" />
|
||||||
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="5.3.3">
|
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="5.2.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>
|
||||||
<PackageReference Include="System.ServiceModel.Primitives" Version="8.0.0" />
|
<PackageReference Include="System.ServiceModel.Primitives" Version="6.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- Don't publish the SPA source files, but do show them in the project files list -->
|
<!-- Don't publish the SPA source files, but do show them in the project files list -->
|
||||||
<Content Remove="$(SpaRoot)**"/>
|
<Content Remove="$(SpaRoot)**" />
|
||||||
<Content Include="..\.dockerignore">
|
<Content Include="..\.dockerignore">
|
||||||
<Link>.dockerignore</Link>
|
<Link>.dockerignore</Link>
|
||||||
</Content>
|
</Content>
|
||||||
<None Remove="$(SpaRoot)**"/>
|
<None Remove="$(SpaRoot)**" />
|
||||||
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**"/>
|
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<TypeScriptCompile Remove="ClientApp\src\components\Counter.tsx"/>
|
<TypeScriptCompile Remove="ClientApp\src\components\Counter.tsx" />
|
||||||
<TypeScriptCompile Remove="ClientApp\src\components\FetchData.tsx"/>
|
<TypeScriptCompile Remove="ClientApp\src\components\FetchData.tsx" />
|
||||||
<TypeScriptCompile Remove="ClientApp\src\components\Home.tsx"/>
|
<TypeScriptCompile Remove="ClientApp\src\components\Home.tsx" />
|
||||||
<TypeScriptCompile Remove="ClientApp\src\pages\FetchData.tsx"/>
|
<TypeScriptCompile Remove="ClientApp\src\pages\FetchData.tsx" />
|
||||||
<TypeScriptCompile Remove="ClientApp\src\classes\tileMap.ts"/>
|
<TypeScriptCompile Remove="ClientApp\src\classes\tileMap.ts" />
|
||||||
<TypeScriptCompile Remove="ClientApp\src\game\tileMap.ts"/>
|
<TypeScriptCompile Remove="ClientApp\src\game\tileMap.ts" />
|
||||||
<TypeScriptCompile Remove="ClientApp\src\components\gameCanvas.tsx"/>
|
<TypeScriptCompile Remove="ClientApp\src\components\gameCanvas.tsx" />
|
||||||
<TypeScriptCompile Remove="ClientApp\src\game\game.ts"/>
|
<TypeScriptCompile Remove="ClientApp\src\game\game.ts" />
|
||||||
<TypeScriptCompile Remove="ClientApp\src\App.test.tsx"/>
|
<TypeScriptCompile Remove="ClientApp\src\App.test.tsx" />
|
||||||
<TypeScriptCompile Remove="ClientApp\src\game\playerStats.tsx"/>
|
<TypeScriptCompile Remove="ClientApp\src\game\playerStats.tsx" />
|
||||||
<TypeScriptCompile Remove="ClientApp\src\websockets\actions.ts"/>
|
<TypeScriptCompile Remove="ClientApp\src\websockets\actions.ts" />
|
||||||
<TypeScriptCompile Remove="ClientApp\src\utils\colours.ts"/>
|
<TypeScriptCompile Remove="ClientApp\src\utils\colours.ts" />
|
||||||
<TypeScriptCompile Remove="ClientApp\src\utils\dom.ts"/>
|
<TypeScriptCompile Remove="ClientApp\src\utils\dom.ts" />
|
||||||
<TypeScriptCompile Remove="ClientApp\src\game\pellet.ts"/>
|
<TypeScriptCompile Remove="ClientApp\src\game\pellet.ts" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="ClientApp\tests\utils\"/>
|
<Folder Include="ClientApp\tests\utils\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\DataAccessLayer\DataAccessLayer.csproj"/>
|
<ProjectReference Include="..\DataAccessLayer\DataAccessLayer.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
|
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
|
||||||
<!-- Ensure Node.js is installed -->
|
<!-- Ensure Node.js is installed -->
|
||||||
<Exec Command="node --version" ContinueOnError="true">
|
<Exec Command="node --version" ContinueOnError="true">
|
||||||
<Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
|
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
|
||||||
</Exec>
|
</Exec>
|
||||||
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE."/>
|
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
|
||||||
<Message Importance="high" Text="Restoring dependencies using 'pnpm'. This may take several minutes..."/>
|
<Message Importance="high" Text="Restoring dependencies using 'pnpm'. This may take several minutes..." />
|
||||||
<Exec WorkingDirectory="$(SpaRoot)" Command="pnpm install"/>
|
<Exec WorkingDirectory="$(SpaRoot)" Command="pnpm install" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
|
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
|
||||||
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
|
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
|
||||||
<Exec WorkingDirectory="$(SpaRoot)" Command="pnpm install"/>
|
<Exec WorkingDirectory="$(SpaRoot)" Command="pnpm install" />
|
||||||
<Exec WorkingDirectory="$(SpaRoot)" Command="pnpm build"/>
|
<Exec WorkingDirectory="$(SpaRoot)" Command="pnpm build" />
|
||||||
|
|
||||||
<!-- Include the newly-built files in the publish output -->
|
<!-- Include the newly-built files in the publish output -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<DistFiles Include="$(SpaRoot)build\**"/>
|
<DistFiles Include="$(SpaRoot)build\**" />
|
||||||
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
|
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
|
||||||
<RelativePath>wwwroot\%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
|
<RelativePath>wwwroot\%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
|
||||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user