2023-07-18 13:50:46 +02:00
|
|
|
using System.Text.Json.Serialization;
|
2023-07-11 14:23:41 +02:00
|
|
|
using pacMan.Exceptions;
|
2023-07-18 17:09:27 +02:00
|
|
|
using pacMan.GameStuff;
|
|
|
|
using pacMan.GameStuff.Items;
|
2023-06-24 19:43:03 +02:00
|
|
|
|
|
|
|
namespace pacMan.Services;
|
|
|
|
|
2023-12-06 23:43:21 +01:00
|
|
|
/// <summary>
|
|
|
|
/// Represents a game instance.
|
|
|
|
/// </summary>
|
2023-11-18 23:47:55 +01:00
|
|
|
public class Game(Queue<DirectionalPosition> spawns)
|
2023-06-24 19:43:03 +02:00
|
|
|
{
|
2023-07-02 16:44:30 +02:00
|
|
|
private readonly Random _random = new();
|
2023-07-16 12:10:53 +02:00
|
|
|
private int _currentPlayerIndex;
|
2023-12-06 23:43:21 +01:00
|
|
|
private List<Player> _players = [];
|
2023-07-14 19:14:19 +02:00
|
|
|
|
2023-07-18 13:50:46 +02:00
|
|
|
[JsonInclude] public Guid Id { get; } = Guid.NewGuid();
|
2023-07-02 16:44:30 +02:00
|
|
|
|
2023-12-06 23:43:21 +01:00
|
|
|
/// <summary>
|
|
|
|
/// Gets or sets the list of players.
|
|
|
|
/// When setting, the mutable values of the players are updated instead of replacing the list.
|
|
|
|
/// </summary>
|
2023-07-21 13:54:44 +02:00
|
|
|
[JsonIgnore]
|
|
|
|
public List<Player> Players
|
|
|
|
{
|
|
|
|
get => _players;
|
2023-08-27 13:21:54 +02:00
|
|
|
set
|
|
|
|
{
|
|
|
|
if (_players.Count > 0)
|
|
|
|
_players = _players.Select((player, index) =>
|
|
|
|
{
|
|
|
|
player.State = value[index].State;
|
|
|
|
player.PacMan = value[index].PacMan;
|
|
|
|
player.Box = value[index].Box;
|
|
|
|
return player;
|
|
|
|
}).ToList();
|
|
|
|
else
|
|
|
|
_players = value;
|
|
|
|
}
|
2023-07-21 13:54:44 +02:00
|
|
|
}
|
2023-07-12 21:33:52 +02:00
|
|
|
|
2023-12-06 23:43:21 +01:00
|
|
|
[JsonIgnore] public List<Character> Ghosts { get; set; } = []; // TODO include
|
2023-07-19 17:09:47 +02:00
|
|
|
|
2023-12-06 23:43:21 +01:00
|
|
|
/// <summary>
|
|
|
|
/// The spawn locations on the map.
|
|
|
|
/// </summary>
|
|
|
|
/// <value>
|
|
|
|
/// A Queue of DirectionalPositions representing the spawn locations on the map.
|
|
|
|
/// </value>
|
|
|
|
[JsonIgnore]
|
|
|
|
private Queue<DirectionalPosition> Spawns { get; } = spawns;
|
2023-07-16 12:10:53 +02:00
|
|
|
|
2023-08-27 12:01:30 +02:00
|
|
|
[JsonIgnore] public DiceCup DiceCup { get; } = new(); // TODO include
|
2023-07-19 16:19:51 +02:00
|
|
|
|
2023-07-18 13:50:46 +02:00
|
|
|
[JsonInclude] public int Count => Players.Count;
|
2023-07-12 21:33:52 +02:00
|
|
|
|
2023-08-27 13:21:54 +02:00
|
|
|
// TODO edge-case when game has started but all players have disconnected, Disconnected property?
|
2023-12-06 23:43:21 +01:00
|
|
|
/// <summary>
|
|
|
|
/// Whether or not the game has started.
|
|
|
|
/// </summary>
|
|
|
|
/// <remarks>
|
|
|
|
/// The game is considered started if the count is greater than zero and at least one player is in the "InGame" state.
|
|
|
|
/// </remarks>
|
|
|
|
[JsonInclude]
|
|
|
|
public bool IsGameStarted => Count > 0 && Players.Any(player => player.State is State.InGame);
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the next player in the game.
|
|
|
|
/// </summary>
|
|
|
|
/// <returns>The next player.</returns>
|
|
|
|
/// <exception cref="PlayerNotFoundException">Thrown when there are no players in the game.</exception>
|
2023-07-20 18:06:30 +02:00
|
|
|
public Player NextPlayer()
|
2023-07-16 13:23:12 +02:00
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
_currentPlayerIndex = (_currentPlayerIndex + 1) % Count;
|
|
|
|
}
|
|
|
|
catch (DivideByZeroException)
|
|
|
|
{
|
2023-11-18 23:47:55 +01:00
|
|
|
throw new PlayerNotFoundException("There are no players in the game.");
|
2023-07-16 13:23:12 +02:00
|
|
|
}
|
2023-07-18 13:50:46 +02:00
|
|
|
|
2023-07-16 13:23:12 +02:00
|
|
|
return Players[_currentPlayerIndex];
|
|
|
|
}
|
|
|
|
|
2023-07-16 12:10:53 +02:00
|
|
|
public void Shuffle() => Players.Sort((_, _) => _random.Next(-1, 2));
|
|
|
|
|
2023-12-06 23:43:21 +01:00
|
|
|
/// <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>
|
2023-06-24 19:43:03 +02:00
|
|
|
public event Func<ArraySegment<byte>, Task>? Connections;
|
|
|
|
|
2023-12-06 23:43:21 +01:00
|
|
|
/// <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>
|
2023-08-27 13:21:54 +02:00
|
|
|
public void AddPlayer(Player player)
|
2023-06-24 19:43:03 +02:00
|
|
|
{
|
2023-08-27 13:21:54 +02:00
|
|
|
if (Players.Count >= Rules.MaxPlayers)
|
2023-07-18 18:19:54 +02:00
|
|
|
throw new GameNotPlayableException("Game is full");
|
|
|
|
if (IsGameStarted)
|
|
|
|
throw new GameNotPlayableException("Game has already started");
|
2023-06-24 19:43:03 +02:00
|
|
|
|
2023-07-01 18:54:59 +02:00
|
|
|
player.State = State.WaitingForPlayers;
|
2023-08-27 13:21:54 +02:00
|
|
|
if (Players.Exists(p => p.Username == player.Username)) return;
|
|
|
|
|
2023-06-24 19:43:03 +02:00
|
|
|
Players.Add(player);
|
2023-07-14 19:14:19 +02:00
|
|
|
if (player.PacMan.SpawnPosition is null) SetSpawn(player);
|
2023-06-24 19:43:03 +02:00
|
|
|
}
|
|
|
|
|
2023-07-20 18:06:30 +02:00
|
|
|
public Player? RemovePlayer(string username)
|
2023-07-20 15:00:03 +02:00
|
|
|
{
|
|
|
|
var index = Players.FindIndex(p => p.Username == username);
|
|
|
|
if (index == -1) return null;
|
|
|
|
var removedPlayer = Players[index];
|
|
|
|
Players.RemoveAt(index);
|
|
|
|
return removedPlayer;
|
|
|
|
}
|
|
|
|
|
2023-12-06 23:43:21 +01:00
|
|
|
/// <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>
|
2023-07-20 18:06:30 +02:00
|
|
|
private void SetSpawn(Player player)
|
2023-07-14 19:14:19 +02:00
|
|
|
{
|
|
|
|
if (player.PacMan.SpawnPosition is not null) return;
|
|
|
|
var spawn = Spawns.Dequeue();
|
|
|
|
player.PacMan.SpawnPosition = spawn;
|
|
|
|
player.PacMan.Position = spawn;
|
|
|
|
}
|
|
|
|
|
2023-12-06 23:43:21 +01:00
|
|
|
/// <summary>
|
|
|
|
/// Sends the specified byte segment to all connected clients.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="segment">The byte segment to send.</param>
|
2023-07-02 16:44:30 +02:00
|
|
|
public void SendToAll(ArraySegment<byte> segment) => Connections?.Invoke(segment);
|
2023-07-01 18:54:59 +02:00
|
|
|
|
2023-12-06 23:43:21 +01:00
|
|
|
/// <summary>
|
|
|
|
/// Sets the state of the player with the specified username to Ready.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="username">The username of the player.</param>
|
|
|
|
/// <returns>An enumerable collection of Player objects.</returns>
|
2023-07-20 22:56:35 +02:00
|
|
|
public IEnumerable<Player> SetReady(string username)
|
2023-07-01 18:54:59 +02:00
|
|
|
{
|
2023-07-20 22:56:35 +02:00
|
|
|
var player = Players.FirstOrDefault(p => p.Username == username);
|
|
|
|
if (player is null)
|
2023-07-11 14:23:41 +02:00
|
|
|
throw new PlayerNotFoundException("The player was not found in the game group.");
|
2023-07-01 18:54:59 +02:00
|
|
|
player.State = State.Ready;
|
2023-07-02 16:44:30 +02:00
|
|
|
return Players;
|
2023-07-01 18:54:59 +02:00
|
|
|
}
|
2023-07-04 22:42:44 +02:00
|
|
|
|
2023-12-06 23:43:21 +01:00
|
|
|
/// <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>
|
2023-07-11 14:31:50 +02:00
|
|
|
public bool SetAllInGame()
|
2023-07-04 22:42:44 +02:00
|
|
|
{
|
2023-11-18 23:47:55 +01:00
|
|
|
if (Players.Any(player => player.State is not State.Ready)) return false;
|
2023-07-11 14:31:50 +02:00
|
|
|
|
2023-07-04 22:42:44 +02:00
|
|
|
foreach (var player in Players) player.State = State.InGame;
|
2023-07-11 14:31:50 +02:00
|
|
|
return true;
|
2023-07-04 22:42:44 +02:00
|
|
|
}
|
2023-11-18 23:47:55 +01:00
|
|
|
|
2023-12-06 23:43:21 +01:00
|
|
|
/// <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>
|
2023-11-18 23:47:55 +01:00
|
|
|
public Player? FindPlayerByUsername(string username) =>
|
|
|
|
Players.FirstOrDefault(player => player.Username == username);
|
2023-07-02 16:44:30 +02:00
|
|
|
}
|