Compare commits
25 Commits
feature/ca
...
mauster
| Author | SHA1 | Date | |
|---|---|---|---|
| 309f467f99 | |||
| ef8329b565 | |||
| 5be5914d5d | |||
| 094b5488a1 | |||
| 401e9fbf8a | |||
| b1083be1e7 | |||
| 681cfa13bd | |||
| 25b7a8fb1e | |||
| 0918b2b3cc | |||
| 84891e37c1 | |||
| 2ef1de9d64 | |||
|
|
3b765841b7 | ||
|
|
f5c2b937b0 | ||
|
|
90c9b0031c | ||
|
|
694bc6147a | ||
| 0b53650392 | |||
|
|
0c2cb8272d | ||
|
|
3c2032a800 | ||
|
|
1e51defad5 | ||
|
|
b97eb309c0 | ||
| 256d6d30ea | |||
| ed69e06ab9 | |||
| 9b544ce0a1 | |||
| 9d3b2c44db | |||
| 75365e663f |
@@ -1,4 +1,26 @@
|
|||||||
.idea/
|
**/.dockerignore
|
||||||
.git/
|
**/.env
|
||||||
.gitignore
|
**/.git
|
||||||
|
**/.gitea
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/.idea
|
||||||
|
**/*.*proj.user
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/azds.yaml
|
||||||
|
**/bin
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/Dockerfile*
|
||||||
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
**/obj
|
||||||
|
**/secrets.dev.yaml
|
||||||
|
**/values.dev.yaml
|
||||||
|
LICENSE
|
||||||
README.md
|
README.md
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
name: Build Mau & Deploy Mau
|
name: Build Mau & Deploy Mau
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'mauster'
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: mau
|
group: mau
|
||||||
@@ -22,7 +28,7 @@ jobs:
|
|||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v3
|
uses: actions/setup-dotnet@v3
|
||||||
with:
|
with:
|
||||||
dotnet-version: 7.0.x
|
dotnet-version: 8.0.x
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
|
|||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,8 +1,7 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
/packages/
|
||||||
|
riderModule.iml
|
||||||
|
/_ReSharper.Caches/
|
||||||
|
|
||||||
.idea
|
.idea
|
||||||
bin
|
|
||||||
obj
|
|
||||||
Properties
|
|
||||||
appsettings.Local.json
|
|
||||||
.git
|
|
||||||
*.DotSettings.user
|
|
||||||
*.sln
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using MauMau_Server.Websockets;
|
using MauMau_Server.Room;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace MauMau_Server.Controllers;
|
namespace MauMau_Server.Controllers;
|
||||||
@@ -24,26 +24,27 @@ public class RoomController : ControllerBase
|
|||||||
return Ok(rooms);
|
return Ok(rooms);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}/{name}")]
|
||||||
public async Task ConnectToRoom(string id)
|
public async Task ConnectToRoom(string id, string name)
|
||||||
{
|
{
|
||||||
if (HttpContext.WebSockets.IsWebSocketRequest)
|
var response = HttpContext.Response;
|
||||||
|
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||||
{
|
{
|
||||||
if (_roomManager.RoomExists(id))
|
response.StatusCode = 400;
|
||||||
|
await response.BodyWriter.WriteAsync("Request is not a websocket request"u8.ToArray());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_roomManager.RoomExists(id))
|
||||||
{
|
{
|
||||||
|
response.StatusCode = 404;
|
||||||
|
await response.BodyWriter.WriteAsync("Room could not be found"u8.ToArray());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||||
var room = _roomManager.GetRoom(id);
|
var room = _roomManager.GetRoom(id);
|
||||||
await room.InstantiateConnection(webSocket);
|
await room.InstantiateConnection(webSocket, name);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
HttpContext.Response.StatusCode = 404;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
HttpContext.Response.StatusCode = 400;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
|||||||
36
Dockerfile
36
Dockerfile
@@ -1,35 +1,23 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/runtime-deps:7.0-alpine AS base
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||||
|
USER $APP_UID
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 5000
|
EXPOSE 8080
|
||||||
|
EXPOSE 8081
|
||||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:7.0-alpine AS build
|
|
||||||
ARG TARGETARCH
|
|
||||||
ARG BUILDPLATFORM
|
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY *.csproj .
|
COPY MauMau-Server.csproj .
|
||||||
RUN dotnet restore "MauMau-Server.csproj"
|
RUN dotnet restore "MauMau-Server.csproj"
|
||||||
COPY . .
|
COPY . .
|
||||||
WORKDIR "/src"
|
WORKDIR "/src"
|
||||||
RUN dotnet build "MauMau-Server.csproj" -c Release -o /app/build -a $TARGETARCH
|
RUN dotnet build "MauMau-Server.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||||
|
|
||||||
FROM build AS publish
|
FROM build AS publish
|
||||||
RUN dotnet publish "MauMau-Server.csproj" -c Release -o /app/publish \
|
ARG BUILD_CONFIGURATION=Release
|
||||||
--self-contained true \
|
RUN dotnet publish "MauMau-Server.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||||
/p:PublishTrimmed=true \
|
|
||||||
/p:PublishSingleFile=true \
|
|
||||||
-a $TARGETARCH
|
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM base AS final
|
FROM base AS final
|
||||||
ARG TARGETARCH
|
|
||||||
ARG BUILDPLATFORM
|
|
||||||
|
|
||||||
RUN adduser --disabled-password \
|
|
||||||
--home /app \
|
|
||||||
--gecos '' dotnetuser && chown -R dotnetuser /app
|
|
||||||
|
|
||||||
USER dotnetuser
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=publish /app/publish .
|
COPY --from=publish /app/publish .
|
||||||
ENTRYPOINT ["./MauMau-Server"]
|
ENTRYPOINT ["dotnet", "MauMau-Server.dll"]
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
public class ActionDTO
|
public class ActionDTO
|
||||||
{
|
{
|
||||||
public string Action { get; set; }
|
public string Type { get; set; }
|
||||||
public string Data { get; set; }
|
public string Data { get; set; }
|
||||||
|
|
||||||
public ActionDTO(string action, string data)
|
public ActionDTO(string type, string data)
|
||||||
{
|
{
|
||||||
Action = action;
|
Type = type;
|
||||||
Data = data;
|
Data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
40
Mau/Card.cs
40
Mau/Card.cs
@@ -25,6 +25,46 @@ public class Card
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class CardExtensions
|
||||||
|
{
|
||||||
|
public static bool IsSameCard(this Card card1, Card card2)
|
||||||
|
{
|
||||||
|
return card1.IsSameCardType(card2) && card1.IsSameCardValue(card2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsSameCardType(this Card card1, Card card2)
|
||||||
|
{
|
||||||
|
return card1.CardType == card2.CardType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsSameCardValue(this Card card1, Card card2)
|
||||||
|
{
|
||||||
|
return card1.CardValue == card2.CardValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool CanBePlayedOn(this Card playedCard, Card currentCard)
|
||||||
|
{
|
||||||
|
return playedCard.IsSameCardType(currentCard)
|
||||||
|
|| playedCard.IsSameCardValue(currentCard)
|
||||||
|
|| playedCard.CardType == CardType.JOKER;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsSpecialCard(this Card card)
|
||||||
|
{
|
||||||
|
return card.IsMauCard() || card.CardValue is
|
||||||
|
CardValue.SEVEN or
|
||||||
|
CardValue.EIGHT or
|
||||||
|
CardValue.JACK or
|
||||||
|
CardValue.KING or
|
||||||
|
CardValue.ACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsMauCard(this Card card)
|
||||||
|
{
|
||||||
|
return card.CardType == CardType.JOKER || card.CardValue is CardValue.TWO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum CardType
|
public enum CardType
|
||||||
{
|
{
|
||||||
SPADES,
|
SPADES,
|
||||||
|
|||||||
140
Mau/Deck.cs
140
Mau/Deck.cs
@@ -2,43 +2,83 @@
|
|||||||
|
|
||||||
public class Deck
|
public class Deck
|
||||||
{
|
{
|
||||||
public List<Card> UnusedDeck = new();
|
private readonly List<Card> _unusedDeck = new();
|
||||||
public List<Card> UsedDeck = new();
|
private readonly List<Card> _usedDeck = new();
|
||||||
|
public Card CurrentCard { get; private set; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Creates a new deck instance with a new shuffled set of cards
|
||||||
|
* </summary>
|
||||||
|
*/
|
||||||
public Deck()
|
public Deck()
|
||||||
{
|
{
|
||||||
foreach (CardType cardType in Enum.GetValues(typeof(CardType)))
|
// Create a new set of cards
|
||||||
{
|
CreateSet();
|
||||||
if (cardType == CardType.JOKER)
|
|
||||||
{
|
// Shuffle the deck
|
||||||
UnusedDeck.Add(new Card(cardType, CardValue.RED));
|
|
||||||
UnusedDeck.Add(new Card(cardType, CardValue.BLACK));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
foreach (CardValue cardValue in Enum.GetValues(typeof(CardValue)))
|
|
||||||
{
|
|
||||||
if (cardValue is CardValue.RED or CardValue.BLACK) continue;
|
|
||||||
UnusedDeck.Add(new Card(cardType, cardValue));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ShuffleDeck();
|
ShuffleDeck();
|
||||||
|
|
||||||
|
// Draw the first card
|
||||||
|
var initialCard = DrawCard();
|
||||||
|
|
||||||
|
// Set the current card to the first card
|
||||||
|
CurrentCard = initialCard;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Card> GetUnusedDeck()
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Adds the given card to the used cards deck and sets the current card to the given card.
|
||||||
|
* </summary>
|
||||||
|
* <param name="card">The card to add to the used cards deck.</param>
|
||||||
|
*/
|
||||||
|
public void AddCardToUsedDeck(Card card)
|
||||||
{
|
{
|
||||||
return UnusedDeck;
|
// Add the card to the used deck
|
||||||
|
_usedDeck.Add(card);
|
||||||
|
|
||||||
|
// Set the current card to the given card
|
||||||
|
CurrentCard = card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Adds the given list of cards to the used cards deck.
|
||||||
|
* </summary>
|
||||||
|
* <param name="cards">The list of cards to add to the used cards deck.</param>
|
||||||
|
*/
|
||||||
|
public void AddCardsToUsedDeck(IEnumerable<Card> cards)
|
||||||
|
{
|
||||||
|
_usedDeck.AddRange(cards);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Draws a card from the deck.
|
||||||
|
* If the deck is empty, the deck is reshuffled with <see cref="ReshuffleDeck"/>.
|
||||||
|
* </summary>
|
||||||
|
*/
|
||||||
public Card DrawCard()
|
public Card DrawCard()
|
||||||
{
|
{
|
||||||
if (UnusedDeck.Count == 0) ReshuffleDeck();
|
// Check if the deck is empty, if so, reshuffle it
|
||||||
var card = UnusedDeck[0];
|
if (_unusedDeck.Count == 0) ReshuffleDeck();
|
||||||
UnusedDeck.RemoveAt(0);
|
|
||||||
|
// Grab the first card from the deck and remove it
|
||||||
|
var card = _unusedDeck[0];
|
||||||
|
_unusedDeck.RemoveAt(0);
|
||||||
|
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Card> DrawCards(int amount)
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Take a given amount of cards from the deck. This method calls <see cref="DrawCard"/> for each card.
|
||||||
|
* </summary>
|
||||||
|
* <param name="amount">The amount of cards to draw from the deck.</param>
|
||||||
|
*/
|
||||||
|
public IEnumerable<Card> DrawCards(int amount)
|
||||||
{
|
{
|
||||||
|
// Create a list of cards and add the drawn cards to it
|
||||||
var cards = new List<Card>();
|
var cards = new List<Card>();
|
||||||
for (var i = 0; i < amount; i++)
|
for (var i = 0; i < amount; i++)
|
||||||
{
|
{
|
||||||
@@ -47,20 +87,66 @@ public class Deck
|
|||||||
return cards;
|
return cards;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddCardToUsedDeck(Card card)
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Creates a new deck of cards and adds them to the unused deck.
|
||||||
|
* </summary>
|
||||||
|
*/
|
||||||
|
private void CreateSet()
|
||||||
{
|
{
|
||||||
UsedDeck.Add(card);
|
foreach (CardType cardType in Enum.GetValues(typeof(CardType)))
|
||||||
|
{
|
||||||
|
if (cardType == CardType.JOKER)
|
||||||
|
{
|
||||||
|
_unusedDeck.Add(new Card(cardType, CardValue.RED));
|
||||||
|
_unusedDeck.Add(new Card(cardType, CardValue.BLACK));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach (CardValue cardValue in Enum.GetValues(typeof(CardValue)))
|
||||||
|
{
|
||||||
|
if (cardValue is CardValue.RED or CardValue.BLACK) continue;
|
||||||
|
_unusedDeck.Add(new Card(cardType, cardValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Moves all the used cards back to the unused deck and shuffles it.
|
||||||
|
* If there are no cards to reshuffle, a new set of cards is created and shuffled.
|
||||||
|
* </summary>
|
||||||
|
*/
|
||||||
private void ReshuffleDeck()
|
private void ReshuffleDeck()
|
||||||
{
|
{
|
||||||
UnusedDeck.AddRange(UsedDeck);
|
// Move all used cards back to the unused deck
|
||||||
UsedDeck.Clear();
|
_unusedDeck.AddRange(_usedDeck);
|
||||||
|
_usedDeck.Clear();
|
||||||
|
|
||||||
|
// If there are no cards left, create a new set and add it
|
||||||
|
if (!_unusedDeck.Any())
|
||||||
|
{
|
||||||
|
CreateSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffle the deck
|
||||||
ShuffleDeck();
|
ShuffleDeck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Shuffles all the cards in the deck using the Fisher-Yates algorithm.
|
||||||
|
* </summary>
|
||||||
|
*/
|
||||||
private void ShuffleDeck()
|
private void ShuffleDeck()
|
||||||
{
|
{
|
||||||
UnusedDeck = UnusedDeck.OrderBy(x => Guid.NewGuid()).ToList();
|
// Clear the unused ceck
|
||||||
|
var unusedDeckCopy = new List<Card>(_unusedDeck);
|
||||||
|
_unusedDeck.Clear();
|
||||||
|
|
||||||
|
// Shuffle the deck
|
||||||
|
unusedDeckCopy = unusedDeckCopy.OrderBy(x => Guid.NewGuid()).ToList();
|
||||||
|
|
||||||
|
// Add the shuffled deck back to the unused deck
|
||||||
|
_unusedDeck.AddRange(unusedDeckCopy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
439
Mau/Game.cs
439
Mau/Game.cs
@@ -1,78 +1,281 @@
|
|||||||
using System.Net.WebSockets;
|
using System.Text.Json;
|
||||||
using System.Text.Json;
|
using MauMau_Server.Mau.GameMessages;
|
||||||
|
using MauMau_Server.Mau.Managers;
|
||||||
using MauMau_Server.Websockets;
|
using MauMau_Server.Websockets;
|
||||||
|
using MauMau_Server.Room;
|
||||||
|
using MauMau_Server.Room.Messages;
|
||||||
|
|
||||||
namespace MauMau_Server.Mau;
|
namespace MauMau_Server.Mau;
|
||||||
|
|
||||||
public class Game
|
public class Game : RoomType
|
||||||
{
|
{
|
||||||
public readonly Deck Deck = new();
|
// Helpers
|
||||||
public Card CurrentCard;
|
private readonly Deck _deck = new();
|
||||||
public List<Player> Players = new();
|
private readonly TurnManager _turnManager = new();
|
||||||
public Player CurrentPlayer;
|
|
||||||
public int TurnDirection = 1;
|
|
||||||
|
|
||||||
public Game()
|
// Game state
|
||||||
|
private readonly List<Card> _mauCardBuffer = new();
|
||||||
|
private CardType? NextAllowedCardType { get; set; }
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
private const int NumberOfFaultcards = 5;
|
||||||
|
private const int NumberOfStartCards = 8;
|
||||||
|
|
||||||
|
public Game(Room.Room room, IEnumerable<ConnectionInstance> connections) : base(room)
|
||||||
{
|
{
|
||||||
CurrentCard = Deck.DrawCard();
|
// If the current card is a joker, set the next allowed card type to a random card type
|
||||||
Deck.AddCardToUsedDeck(CurrentCard);
|
if (_deck.CurrentCard.CardType == CardType.JOKER)
|
||||||
|
{
|
||||||
|
// In the case the random card type is a joker, try again until it is not
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var cardTypes = Enum.GetValues(typeof(CardType));
|
||||||
|
var randomIndex = new Random().Next(cardTypes.Length);
|
||||||
|
NextAllowedCardType = (CardType?)cardTypes.GetValue(randomIndex) ?? CardType.SPADES;
|
||||||
|
} while (NextAllowedCardType == CardType.JOKER);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddPlayerToGame(ConnectionInstance connection)
|
// Convert all the connections to players
|
||||||
|
List<Player> players = new();
|
||||||
|
foreach (var player in connections.Select(connection => new Player(connection)))
|
||||||
{
|
{
|
||||||
var player = new Player(connection)
|
// Give the new player a hand of cards
|
||||||
{
|
var initialHand = _deck.DrawCards(NumberOfStartCards);
|
||||||
Hand = Deck.DrawCards(8)
|
player.GiveCards(initialHand);
|
||||||
};
|
players.Add(player);
|
||||||
Players.Add(player);
|
|
||||||
if (Players.Count == 1) CurrentPlayer = player;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemovePlayer(string playerId)
|
// Add all the players to the turn manager
|
||||||
{
|
_turnManager.Initialize(players);
|
||||||
var player = GetPlayer(playerId);
|
|
||||||
Players.Remove(player);
|
// Broadcast new game state
|
||||||
|
SendGameState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void HandleAction(string playerId, ActionDTO action)
|
/**
|
||||||
|
* <inheritdoc cref="RoomType.OnMessage"/>
|
||||||
|
*/
|
||||||
|
public override void OnMessage(ConnectionInstance sender, RoomMessage<string> message)
|
||||||
{
|
{
|
||||||
var player = GetPlayer(playerId);
|
// If the message type is not a game message, ignore the message
|
||||||
if (CurrentPlayer != player) return;
|
if (message.Type is not ("CHOOSE" or "DRAW" or "PLAY"))
|
||||||
switch (action.Action)
|
|
||||||
{
|
{
|
||||||
case "PLAYCARD":
|
return;
|
||||||
{
|
|
||||||
var card = JsonSerializer.Deserialize<CardDTO>(action.Data).ToCard();
|
|
||||||
PlayCard(player, card);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the player that sent the message
|
||||||
|
var player = _turnManager.Players.FirstOrDefault(x => x.IsMe(sender.Id));
|
||||||
|
|
||||||
|
// If the player is not the player that is currently playing, ignore the message
|
||||||
|
if (_turnManager.CurrentPlayer != player) return;
|
||||||
|
|
||||||
|
// Based on the message intent, handle the message
|
||||||
|
switch (message.Type)
|
||||||
|
{
|
||||||
case "CHOOSE":
|
case "CHOOSE":
|
||||||
var choice = action.Data;
|
Choose(player, message.Data);
|
||||||
if (!Enum.TryParse(choice, out CardType cardType))
|
|
||||||
{
|
|
||||||
break;
|
break;
|
||||||
}
|
case "PLAY":
|
||||||
CurrentCard = new Card(cardType, CardValue.JACK);
|
Play(player, message.Data);
|
||||||
CurrentPlayer.State = PlayerState.WAIT;
|
|
||||||
CurrentPlayer = CurrentCard.CardType == CardType.JOKER ? GetNextPlayer() : GetNextPlayer(2);
|
|
||||||
CurrentPlayer.State = PlayerState.TURN;
|
|
||||||
break;
|
break;
|
||||||
case "DRAW":
|
default:
|
||||||
DrawCard(player);
|
Draw(player);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PlayCard(Player player, Card card)
|
/**
|
||||||
|
* <inheritdoc cref="RoomType.OnConnect"/>
|
||||||
|
*/
|
||||||
|
public override void OnConnect(ConnectionInstance connection)
|
||||||
{
|
{
|
||||||
var hand = player.Hand;
|
// Broadcast that a player joined
|
||||||
if (!IsCardInHand(hand, card) || !IsCardPlayable(CurrentCard, card)) return;
|
var joinMessage = new JoinMessage(_room.Connections, connection);
|
||||||
Deck.AddCardToUsedDeck(card);
|
_room.BroadCast(new RoomMessage<JoinMessage>("JOIN", joinMessage));
|
||||||
hand.Remove(GetSameCardFromHand(hand, card));
|
|
||||||
CurrentCard = card;
|
// Create a new player, give them a new hand and add them to the game
|
||||||
HandleNextPlayer(card);
|
var player = new Player(connection);
|
||||||
|
var initialHand = _deck.DrawCards(8);
|
||||||
|
player.GiveCards(initialHand);
|
||||||
|
_turnManager.Players.Add(player);
|
||||||
|
|
||||||
|
// Broadcast new game state
|
||||||
|
SendGameState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <inheritdoc cref="RoomType.OnDisconnect"/>
|
||||||
|
*/
|
||||||
|
public override void OnDisconnect(ConnectionInstance connection)
|
||||||
|
{
|
||||||
|
// Broadcast that the player left
|
||||||
|
var leaveMessage = new LeaveMessage(connection);
|
||||||
|
_room.BroadCast(new RoomMessage<LeaveMessage>("LEAVE", leaveMessage));
|
||||||
|
|
||||||
|
// Get the player that left
|
||||||
|
var player = _turnManager.Players.FirstOrDefault(x => x.IsMe(connection.Id));
|
||||||
|
if (player is null) return;
|
||||||
|
|
||||||
|
// Add the player's hand to the used deck
|
||||||
|
var playerHand = player.Hand;
|
||||||
|
_deck.AddCardsToUsedDeck(playerHand);
|
||||||
|
|
||||||
|
// Change the turn if the player that left was the current player
|
||||||
|
if (player == _turnManager.CurrentPlayer)
|
||||||
|
{
|
||||||
|
_turnManager.ChangeTurnTo();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the player from the game
|
||||||
|
_turnManager.Players.Remove(player);
|
||||||
|
|
||||||
|
// Broadcast new game state
|
||||||
|
SendGameState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>
|
||||||
|
* The player had to choose a new card type
|
||||||
|
* </summary>
|
||||||
|
* <param name="player">The player that chose a new card type</param>
|
||||||
|
* <param name="data">A string that represents a CardType</param>
|
||||||
|
*/
|
||||||
|
private void Choose(Player player, string data)
|
||||||
|
{
|
||||||
|
// TODO: Validate if choosing a card is allowed
|
||||||
|
|
||||||
|
// Convert the data to a CardType, if it fails, ignore the message
|
||||||
|
if (!Enum.TryParse(data, out CardType cardType))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the next card type that is allowed to be played
|
||||||
|
NextAllowedCardType = cardType;
|
||||||
|
|
||||||
|
// Change the turns
|
||||||
|
_turnManager.ChangeTurnTo();
|
||||||
|
|
||||||
|
// Broadcast new game state
|
||||||
|
SendGameState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>
|
||||||
|
* The player either:
|
||||||
|
* <list type="bullet">
|
||||||
|
* <item>Could not play a card and had to draw a card</item>
|
||||||
|
* <item>Chose to draw a card as a strategic move</item>
|
||||||
|
* </list>
|
||||||
|
* When there are multiple mau cards played, the player has to draw the combined amount of mau cards played.
|
||||||
|
* </summary>
|
||||||
|
* <param name="player">The player that drew a card</param>
|
||||||
|
*/
|
||||||
|
private void Draw(Player player)
|
||||||
|
{
|
||||||
|
// If there are cards in the MauCardBuffer, this means there are multiple cards that need to be drawn
|
||||||
|
// Otherwise, just draw a single card
|
||||||
|
if (_mauCardBuffer.Count > 0)
|
||||||
|
{
|
||||||
|
// Count the amount of cards that need to be drawn
|
||||||
|
var totalCards = CountMauCardBuffer();
|
||||||
|
|
||||||
|
// Draw the cards from the deck
|
||||||
|
var drawnCards = _deck.DrawCards(totalCards);
|
||||||
|
|
||||||
|
// Give the cards to the player
|
||||||
|
player.GiveCards(drawnCards);
|
||||||
|
|
||||||
|
// Change the turn
|
||||||
|
_turnManager.ChangeTurnTo();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Draw a card from the deck
|
||||||
|
var drawnCard = _deck.DrawCard();
|
||||||
|
|
||||||
|
// Give the card to the player
|
||||||
|
player.GiveCard(drawnCard);
|
||||||
|
|
||||||
|
// Change the player if the drawn card cannot be played
|
||||||
|
if (!CardCanBePlayed(drawnCard))
|
||||||
|
{
|
||||||
|
_turnManager.ChangeTurnTo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast new game state
|
||||||
|
SendGameState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>
|
||||||
|
* The player plays a card, this will only be possible if:
|
||||||
|
* <list type="bullet">
|
||||||
|
* <item>The player has the correct state</item>
|
||||||
|
* <item>The player has the card in their hand</item>
|
||||||
|
* <item>The card is playable on the current card</item>
|
||||||
|
* </list>
|
||||||
|
* </summary>
|
||||||
|
* <param name="player">The player that played</param>
|
||||||
|
* <param name="data">A string that can be serialized to a playcard instance</param>
|
||||||
|
*/
|
||||||
|
private void Play(Player player, string data)
|
||||||
|
{
|
||||||
|
// Check if the player has the correct state to play a card
|
||||||
|
if (player.State != PlayerState.TURN)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the data to a Card instance
|
||||||
|
var cardData = JsonSerializer.Deserialize<PlayCard>(data).ToCard();
|
||||||
|
|
||||||
|
// Check if the player indeed has the card they claim to have
|
||||||
|
var playerCard = player.TakeCardFromHand(cardData);
|
||||||
|
if (playerCard is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CardCanBePlayed(playerCard)) return;
|
||||||
|
|
||||||
|
// Remove the card from the player's hand
|
||||||
|
player.Hand.Remove(playerCard);
|
||||||
|
|
||||||
|
// If the player's last played card is a special card, give the player 5 fault cards
|
||||||
|
if (player.Hand.Count < 1 && playerCard.IsSpecialCard())
|
||||||
|
{
|
||||||
|
var faultCards = _deck.DrawCards(NumberOfFaultcards);
|
||||||
|
player.GiveCards(faultCards);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the card to the used deck
|
||||||
|
_deck.AddCardToUsedDeck(playerCard);
|
||||||
|
|
||||||
|
// Reset the allowed card type, so the next player has the normal same type and same value rules
|
||||||
|
NextAllowedCardType = null;
|
||||||
|
|
||||||
|
// If the player has no cards left, end the game with player as winner
|
||||||
|
if (player.Hand.Count == 0)
|
||||||
|
{
|
||||||
|
EndGame(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change the turn based on the played card
|
||||||
|
HandleNextPlayer(playerCard);
|
||||||
|
|
||||||
|
// Broadcast new game state
|
||||||
|
SendGameState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Based on the played card, change the turn to the next player.
|
||||||
|
* </summary>
|
||||||
|
* <param name="card">The card that was played</param>
|
||||||
|
*/
|
||||||
private void HandleNextPlayer(Card card)
|
private void HandleNextPlayer(Card card)
|
||||||
{
|
{
|
||||||
switch (card.CardValue)
|
switch (card.CardValue)
|
||||||
@@ -80,45 +283,32 @@ public class Game
|
|||||||
case CardValue.RED:
|
case CardValue.RED:
|
||||||
case CardValue.BLACK:
|
case CardValue.BLACK:
|
||||||
{
|
{
|
||||||
var nextPlayer = GetNextPlayer();
|
_mauCardBuffer.Add(card);
|
||||||
var cardsToDraw = Deck.DrawCards(5);
|
_turnManager.CurrentPlayer.State = PlayerState.CHOOSE;
|
||||||
foreach (var drawnCard in cardsToDraw)
|
|
||||||
{
|
|
||||||
nextPlayer.Hand.Add(drawnCard);
|
|
||||||
}
|
|
||||||
CurrentPlayer.State = PlayerState.CHOOSE;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CardValue.TWO:
|
case CardValue.TWO:
|
||||||
{
|
{
|
||||||
var nextPlayer = GetNextPlayer();
|
_mauCardBuffer.Add(card);
|
||||||
var cardsToDraw = Deck.DrawCards(2);
|
_turnManager.ChangeTurnTo();
|
||||||
foreach (var drawnCard in cardsToDraw)
|
|
||||||
{
|
|
||||||
nextPlayer.Hand.Add(drawnCard);
|
|
||||||
}
|
|
||||||
CurrentPlayer.State = PlayerState.WAIT;
|
|
||||||
CurrentPlayer = GetNextPlayer(2);
|
|
||||||
CurrentPlayer.State = PlayerState.TURN;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CardValue.SEVEN:
|
case CardValue.SEVEN:
|
||||||
case CardValue.KING:
|
case CardValue.KING:
|
||||||
break;
|
break;
|
||||||
case CardValue.EIGHT:
|
case CardValue.EIGHT:
|
||||||
CurrentPlayer.State = PlayerState.WAIT;
|
_turnManager.ChangeTurnTo(_turnManager.GetNextPlayer(2));
|
||||||
CurrentPlayer = GetNextPlayer(2);
|
|
||||||
CurrentPlayer.State = PlayerState.TURN;
|
|
||||||
break;
|
break;
|
||||||
case CardValue.ACE:
|
case CardValue.ACE:
|
||||||
if (Players.Count > 2)
|
if (_turnManager.Players.Count > 2)
|
||||||
{
|
{
|
||||||
TurnDirection *= -1;
|
_turnManager.ChangeDirection();
|
||||||
CurrentPlayer = GetNextPlayer();
|
_turnManager.ChangeTurnTo();
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case CardValue.JACK:
|
case CardValue.JACK:
|
||||||
CurrentPlayer.State = PlayerState.CHOOSE;
|
_turnManager.CurrentPlayer.State = PlayerState.CHOOSE;
|
||||||
break;
|
break;
|
||||||
case CardValue.THREE:
|
case CardValue.THREE:
|
||||||
case CardValue.FOUR:
|
case CardValue.FOUR:
|
||||||
@@ -128,64 +318,95 @@ public class Game
|
|||||||
case CardValue.TEN:
|
case CardValue.TEN:
|
||||||
case CardValue.QUEEN:
|
case CardValue.QUEEN:
|
||||||
default:
|
default:
|
||||||
CurrentPlayer = GetNextPlayer();
|
_turnManager.ChangeTurnTo();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawCard(Player player)
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Check if the card can be played with the current game state.
|
||||||
|
* <list type="bullet">
|
||||||
|
* <item>The player can only play a mau card if there are pending mau cards (or draw)</item>
|
||||||
|
* <item>If there is a next allowed card type, the player can only play that card type, a joker or a card with the same value as the current card</item>
|
||||||
|
* <item>Otherwise, the player can play a card that has the same type, same value or is a joker</item>
|
||||||
|
* </list>
|
||||||
|
* </summary>
|
||||||
|
* <returns>True if the given card could be played</returns>
|
||||||
|
*/
|
||||||
|
private bool CardCanBePlayed(Card card)
|
||||||
{
|
{
|
||||||
player.Hand.Add(Deck.DrawCard());
|
// Check if there are pending mau cards played
|
||||||
CurrentPlayer = GetNextPlayer();
|
if (_mauCardBuffer.Count > 0)
|
||||||
player.State = PlayerState.WAIT;
|
{
|
||||||
CurrentPlayer.State = PlayerState.TURN;
|
// If so, the card must be a mau card
|
||||||
|
return card.IsMauCard();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Player GetNextPlayer(int numberOfPlayers = 1)
|
// Check if there is a next allowed card type
|
||||||
|
if (NextAllowedCardType != null)
|
||||||
{
|
{
|
||||||
var index = Players.IndexOf(CurrentPlayer);
|
// If so, the card must be the allowed card type, a joker or the same value as the current card
|
||||||
for (var i = 0; i < numberOfPlayers; i++)
|
return card.CardType == NextAllowedCardType || card.CardType == CardType.JOKER ||
|
||||||
{
|
card.CardValue == _deck.CurrentCard.CardValue;
|
||||||
index += TurnDirection;
|
|
||||||
if (index >= Players.Count) index = 0;
|
|
||||||
if (index < 0) index = Players.Count - 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Players[index];
|
// Otherwise, use the normal rules
|
||||||
|
return card.CanBePlayedOn(_deck.CurrentCard);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Player GetPlayer(string playerId)
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Count the amount of cards that need to be drawn from the MauCardBuffer. This method also clears the buffer.
|
||||||
|
* </summary>
|
||||||
|
* <returns>The amount of cards that need to be drawn</returns>
|
||||||
|
*/
|
||||||
|
private int CountMauCardBuffer()
|
||||||
{
|
{
|
||||||
return Players.FirstOrDefault(p => p.IsMe(playerId));
|
var totalCards = 0;
|
||||||
|
foreach (var card in _mauCardBuffer)
|
||||||
|
{
|
||||||
|
if (card.CardType == CardType.JOKER)
|
||||||
|
{
|
||||||
|
totalCards += 5;
|
||||||
|
}
|
||||||
|
else if (card.CardValue == CardValue.TWO)
|
||||||
|
{
|
||||||
|
totalCards += 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Card GetSameCardFromHand(IEnumerable<Card> hand, Card card)
|
_mauCardBuffer.Clear();
|
||||||
{
|
return totalCards;
|
||||||
return hand.FirstOrDefault(handCard => IsSameCard(handCard, card));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsCardPlayable(Card currentCard, Card playedCard)
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Create a game state for each player and send it to them.
|
||||||
|
* By making a unique game state for each player we can hide information (like other player's hands).
|
||||||
|
* </summary>
|
||||||
|
*/
|
||||||
|
private void SendGameState()
|
||||||
{
|
{
|
||||||
return IsSameCardType(currentCard, playedCard) || IsSameCardValue(currentCard, playedCard) || playedCard.CardType == CardType.JOKER;
|
foreach (var player in _turnManager.Players)
|
||||||
|
{
|
||||||
|
var gameState = new GameState(player, _deck.CurrentCard, NextAllowedCardType, _turnManager.CurrentPlayer,
|
||||||
|
_turnManager.Players);
|
||||||
|
player.Connection.SendMessageAsync(
|
||||||
|
JsonSerializer.Serialize(new RoomMessage<GameState>("GAME", gameState)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsCardInHand(IEnumerable<Card> hand, Card card)
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Broadcast the winner of the game and change the room's type to a lobby.
|
||||||
|
* </summary>
|
||||||
|
* <param name="winner">The player that won the game</param>
|
||||||
|
*/
|
||||||
|
private void EndGame(Player winner)
|
||||||
{
|
{
|
||||||
return hand.Any(handCard => IsSameCard(handCard, card));
|
var winMessage = new EndMessage(winner.Connection);
|
||||||
}
|
_room.BroadCast(new RoomMessage<EndMessage>("END", winMessage));
|
||||||
|
_room.RoomType = new Lobby(_room);
|
||||||
private static bool IsSameCard(Card card1, Card card2)
|
|
||||||
{
|
|
||||||
return IsSameCardType(card1, card2) && IsSameCardValue(card1, card2);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsSameCardType(Card card1, Card card2)
|
|
||||||
{
|
|
||||||
return card1.CardType == card2.CardType;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsSameCardValue(Card card1, Card card2)
|
|
||||||
{
|
|
||||||
return card1.CardValue == card2.CardValue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
30
Mau/GameMessages/PlayCard.cs
Normal file
30
Mau/GameMessages/PlayCard.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
namespace MauMau_Server.Mau.GameMessages;
|
||||||
|
|
||||||
|
public class PlayCard
|
||||||
|
{
|
||||||
|
public string CardType { get; set; }
|
||||||
|
public string CardValue { get; set; }
|
||||||
|
|
||||||
|
public PlayCard(Card card)
|
||||||
|
{
|
||||||
|
CardType = card.CardType.ToString();
|
||||||
|
CardValue = card.CardValue.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayCard(string cardType, string cardValue)
|
||||||
|
{
|
||||||
|
CardType = cardType;
|
||||||
|
CardValue = cardValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayCard()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public Card ToCard()
|
||||||
|
{
|
||||||
|
return new Card((CardType)Enum.Parse(typeof(CardType), CardType),
|
||||||
|
(CardValue)Enum.Parse(typeof(CardValue), CardValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,29 +2,44 @@
|
|||||||
|
|
||||||
public class GameState
|
public class GameState
|
||||||
{
|
{
|
||||||
public string PlayerName { get; set; }
|
public PlayerDTO Me { get; set; }
|
||||||
public string CurrentState { get; set; }
|
public string MyState { get; set; }
|
||||||
public List<string> Hand { get; set; } = new();
|
public List<string> Hand { get; set; } = new();
|
||||||
public string CurrentCard { get; set; }
|
public string CurrentCard { get; set; }
|
||||||
public string CurrentPlayer { get; set; }
|
public string? NextAllowedCardType { get; set; }
|
||||||
public List<string> Players { get; set; } = new();
|
public PlayerDTO CurrentPlayer { get; set; }
|
||||||
|
public List<PlayerDTO> Players { get; set; } = new();
|
||||||
|
|
||||||
public GameState(Game game, string playerId)
|
public GameState(Player me, Card currentCard, CardType? nextAllowedCardType, Player currentPlayer, List<Player> others)
|
||||||
{
|
{
|
||||||
var p = game.GetPlayer(playerId);
|
Me = new PlayerDTO(me);
|
||||||
PlayerName = p.Connection.ConnectionId;
|
MyState = me.State.ToString();
|
||||||
CurrentState = p.State.ToString();
|
foreach (var card in me.Hand)
|
||||||
foreach (var card in p.Hand)
|
|
||||||
{
|
{
|
||||||
Hand.Add(card.ToString());
|
Hand.Add(card.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var player in game.Players)
|
foreach (var player in others)
|
||||||
{
|
{
|
||||||
Players.Add(player.Connection.ConnectionId);
|
Players.Add(new PlayerDTO(player));
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentCard = game.CurrentCard.ToString();
|
NextAllowedCardType = nextAllowedCardType?.ToString();
|
||||||
CurrentPlayer = game.CurrentPlayer.Connection.ConnectionId;
|
CurrentCard = currentCard.ToString();
|
||||||
|
CurrentPlayer = new PlayerDTO(currentPlayer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayerDTO
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Id { get; set; }
|
||||||
|
public int CardsLeft { get; set; }
|
||||||
|
|
||||||
|
public PlayerDTO(Player player)
|
||||||
|
{
|
||||||
|
Name = player.Connection.Name;
|
||||||
|
Id = player.Connection.Id.ToString();
|
||||||
|
CardsLeft = player.Hand.Count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
80
Mau/Managers/TurnManager.cs
Normal file
80
Mau/Managers/TurnManager.cs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
namespace MauMau_Server.Mau.Managers;
|
||||||
|
|
||||||
|
public class TurnManager
|
||||||
|
{
|
||||||
|
private const int CLOCKWISE = 1;
|
||||||
|
private const int COUNTER_CLOCKWISE = -1;
|
||||||
|
|
||||||
|
private int _currentDirection { get; set; } = CLOCKWISE;
|
||||||
|
public List<Player> Players;
|
||||||
|
public Player CurrentPlayer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Initialize the turn manager with a list of players.
|
||||||
|
* </summary>
|
||||||
|
* <param name="players">The list of players to initialize the turn manager with.</param>
|
||||||
|
*/
|
||||||
|
public void Initialize(IEnumerable<Player> players)
|
||||||
|
{
|
||||||
|
Players = players.OrderBy(x => Guid.NewGuid()).ToList();
|
||||||
|
ShufflePlayers();
|
||||||
|
CurrentPlayer = Players.First();
|
||||||
|
CurrentPlayer.State = PlayerState.TURN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Change the direction of the turn.
|
||||||
|
* If the direction is clockwise, it will change to counter-clockwise and vice versa.
|
||||||
|
* </summary>
|
||||||
|
*/
|
||||||
|
public void ChangeDirection()
|
||||||
|
{
|
||||||
|
_currentDirection *= COUNTER_CLOCKWISE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Change the turn to another player.
|
||||||
|
* If no player is given, the next player in the current direction is chosen.
|
||||||
|
* </summary>
|
||||||
|
* <param name="playerayer">The player to change the turn to. Defaults to the next player in the current direction.</param>
|
||||||
|
*/
|
||||||
|
public void ChangeTurnTo(Player? player = null, PlayerState nextPlayerState = PlayerState.TURN)
|
||||||
|
{
|
||||||
|
var nextPlayer = player ?? GetNextPlayer();
|
||||||
|
CurrentPlayer.State = PlayerState.WAIT;
|
||||||
|
nextPlayer.State = nextPlayerState;
|
||||||
|
CurrentPlayer = nextPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Get a player that is a given amount of players away from the current player in the current direction.
|
||||||
|
* </summary>
|
||||||
|
* <param name="numberOfPlayers">The amount of players to skip. Defaults to 1</param>
|
||||||
|
*/
|
||||||
|
public Player GetNextPlayer(int numberOfPlayers = 1)
|
||||||
|
{
|
||||||
|
var playerIndex = Players.IndexOf(CurrentPlayer);
|
||||||
|
for (var i = 0; i < numberOfPlayers; i++)
|
||||||
|
{
|
||||||
|
playerIndex += _currentDirection;
|
||||||
|
if (playerIndex >= Players.Count) playerIndex = 0;
|
||||||
|
if (playerIndex < 0) playerIndex = Players.Count - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Players[playerIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Shuffle the list of players for a play random order.
|
||||||
|
* </summary>
|
||||||
|
*/
|
||||||
|
private void ShufflePlayers()
|
||||||
|
{
|
||||||
|
Players = Players.OrderBy(x => Guid.NewGuid()).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,5 +13,25 @@ public class Player
|
|||||||
Connection = connection;
|
Connection = connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsMe(string playerId) => Connection.ConnectionId == playerId;
|
public void GiveCard(Card card)
|
||||||
|
{
|
||||||
|
Hand.Add(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GiveCards(IEnumerable<Card> cards)
|
||||||
|
{
|
||||||
|
Hand.AddRange(cards);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Card? TakeCardFromHand(Card card)
|
||||||
|
{
|
||||||
|
return Hand.FirstOrDefault(handCard => handCard.IsSameCard(card));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsMe(Guid playerId) => Connection.Id == playerId;
|
||||||
|
|
||||||
|
public bool CanPlayCard(Card currentCard)
|
||||||
|
{
|
||||||
|
return Hand.Any(card => card.CanBePlayedOn(currentCard));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,5 +4,6 @@ public enum PlayerState
|
|||||||
{
|
{
|
||||||
TURN,
|
TURN,
|
||||||
CHOOSE,
|
CHOOSE,
|
||||||
WAIT
|
WAIT,
|
||||||
|
POST_DRAW
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<RootNamespace>MauMau_Server2</RootNamespace>
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Hangfire.Core" Version="1.8.12" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6"/>
|
||||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.8.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
|
||||||
<PackageReference Include="Microsoft.AspNet.SignalR" Version="2.4.3" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.3" />
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
16
MauMau-Server.sln
Normal file
16
MauMau-Server.sln
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MauMau-Server", "MauMau-Server.csproj", "{44311559-F848-4D9B-9DB6-372042C8E6DA}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{44311559-F848-4D9B-9DB6-372042C8E6DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{44311559-F848-4D9B-9DB6-372042C8E6DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{44311559-F848-4D9B-9DB6-372042C8E6DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{44311559-F848-4D9B-9DB6-372042C8E6DA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
25
Program.cs
25
Program.cs
@@ -1,24 +1,13 @@
|
|||||||
using Hangfire;
|
using MauMau_Server.Room;
|
||||||
using Hangfire.MemoryStorage;
|
|
||||||
using MauMau_Server.Websockets;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
var services = builder.Services;
|
builder.Services.AddControllers();
|
||||||
services.AddControllers();
|
|
||||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
services.AddScoped<IRoomManager, RoomManager>();
|
builder.Services.AddScoped<IRoomManager, RoomManager>();
|
||||||
// var roomManager = services.BuildServiceProvider().GetRequiredService<IRoomManager>();
|
|
||||||
//
|
|
||||||
// services.AddHangfire((sp, config) =>
|
|
||||||
// {
|
|
||||||
// config.UseRecommendedSerializerSettings();
|
|
||||||
// config.UseMemoryStorage();
|
|
||||||
// });
|
|
||||||
// services.AddHangfireServer();
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
@@ -36,9 +25,6 @@ var webSocketOptions = new WebSocketOptions()
|
|||||||
|
|
||||||
app.UseWebSockets(webSocketOptions);
|
app.UseWebSockets(webSocketOptions);
|
||||||
|
|
||||||
// var recurringJobManager = app.Services.GetRequiredService<IRecurringJobManagerV2>();
|
|
||||||
// recurringJobManager.AddOrUpdate("1", () => roomManager.ClearGhostRooms(), Cron.Hourly);
|
|
||||||
|
|
||||||
app.UseCors(policyBuilder =>
|
app.UseCors(policyBuilder =>
|
||||||
{
|
{
|
||||||
policyBuilder.AllowAnyOrigin();
|
policyBuilder.AllowAnyOrigin();
|
||||||
@@ -47,6 +33,5 @@ app.UseCors(policyBuilder =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
app.UseAuthorization();
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
app.Run();
|
app.Run();
|
||||||
41
Properties/launchSettings.json
Normal file
41
Properties/launchSettings.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:65148",
|
||||||
|
"sslPort": 44331
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": false,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "http://localhost:5039",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": false,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "https://localhost:7037;http://localhost:5039",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": false,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using System.Net.WebSockets;
|
|
||||||
using System.Text.Json;
|
|
||||||
using MauMau_Server.Websockets;
|
|
||||||
|
|
||||||
namespace MauMau_Server.Mau;
|
|
||||||
|
|
||||||
public class Chat
|
|
||||||
{
|
|
||||||
private readonly Room _room;
|
|
||||||
|
|
||||||
public Chat(Room room)
|
|
||||||
{
|
|
||||||
_room = room;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SendChatMessage(string connectionId, string message)
|
|
||||||
{
|
|
||||||
var chatMessage = new ChatOutput(connectionId, message);
|
|
||||||
var formattedMessage = new MessageDTO("CHAT", JsonSerializer.Serialize(chatMessage));
|
|
||||||
WebsocketManager.BroadcastAsync(_room.GetWebsockets(), JsonSerializer.Serialize(formattedMessage));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
namespace MauMau_Server.Mau;
|
|
||||||
|
|
||||||
public class ChatOutput
|
|
||||||
{
|
|
||||||
public string PlayerName { get; set; }
|
|
||||||
public string Message { get; set; }
|
|
||||||
|
|
||||||
public ChatOutput(string playerName, string message)
|
|
||||||
{
|
|
||||||
PlayerName = playerName;
|
|
||||||
Message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChatOutput()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
33
Room/Lobby.cs
Normal file
33
Room/Lobby.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using MauMau_Server.Mau;
|
||||||
|
using MauMau_Server.Room.Messages;
|
||||||
|
using MauMau_Server.Websockets;
|
||||||
|
|
||||||
|
namespace MauMau_Server.Room;
|
||||||
|
|
||||||
|
public class Lobby : RoomType
|
||||||
|
{
|
||||||
|
public Lobby(Room room) : base(room)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnMessage(ConnectionInstance sender, RoomMessage<string> message)
|
||||||
|
{
|
||||||
|
// TODO: Add a way to change game settings
|
||||||
|
if (message.Type == "LOBBY" && sender == _room.Host)
|
||||||
|
{
|
||||||
|
_room.RoomType = new Game(_room, _room.Connections);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnConnect(ConnectionInstance connection)
|
||||||
|
{
|
||||||
|
var joinMessage = new JoinMessage(_room.Connections, connection);
|
||||||
|
_room.BroadCast(new RoomMessage<JoinMessage>("JOIN", joinMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnDisconnect(ConnectionInstance connection)
|
||||||
|
{
|
||||||
|
var leaveMessage = new LeaveMessage(connection);
|
||||||
|
_room.BroadCast(new RoomMessage<LeaveMessage>("LEAVE", leaveMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace MauMau_Server.Mau;
|
namespace MauMau_Server.Room;
|
||||||
|
|
||||||
public class MessageDTO
|
public class MessageDTO
|
||||||
{
|
{
|
||||||
|
|||||||
14
Room/Messages/ChatMessage.cs
Normal file
14
Room/Messages/ChatMessage.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace MauMau_Server.Room.Messages;
|
||||||
|
|
||||||
|
public class ChatMessage
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
public string Sender { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
|
||||||
|
public ChatMessage(string sender, string message)
|
||||||
|
{
|
||||||
|
Sender = sender;
|
||||||
|
Message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Room/Messages/EndMessage.cs
Normal file
13
Room/Messages/EndMessage.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using MauMau_Server.Websockets;
|
||||||
|
|
||||||
|
namespace MauMau_Server.Room.Messages;
|
||||||
|
|
||||||
|
public class EndMessage
|
||||||
|
{
|
||||||
|
public ConnectionInstance Winner { get; set; }
|
||||||
|
|
||||||
|
public EndMessage(ConnectionInstance winner)
|
||||||
|
{
|
||||||
|
Winner = winner;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Room/Messages/JoinMessage.cs
Normal file
15
Room/Messages/JoinMessage.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using MauMau_Server.Websockets;
|
||||||
|
|
||||||
|
namespace MauMau_Server.Room.Messages;
|
||||||
|
|
||||||
|
public class JoinMessage
|
||||||
|
{
|
||||||
|
public List<ConnectionInstance> Connections { get; set; }
|
||||||
|
public ConnectionInstance NewConnection { get; set; }
|
||||||
|
|
||||||
|
public JoinMessage(List<ConnectionInstance> connections, ConnectionInstance newConnection)
|
||||||
|
{
|
||||||
|
Connections = connections;
|
||||||
|
NewConnection = newConnection;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Room/Messages/LeaveMessage.cs
Normal file
13
Room/Messages/LeaveMessage.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using MauMau_Server.Websockets;
|
||||||
|
|
||||||
|
namespace MauMau_Server.Room.Messages;
|
||||||
|
|
||||||
|
public class LeaveMessage
|
||||||
|
{
|
||||||
|
public ConnectionInstance Connection { get; set; }
|
||||||
|
|
||||||
|
public LeaveMessage(ConnectionInstance connection)
|
||||||
|
{
|
||||||
|
Connection = connection;
|
||||||
|
}
|
||||||
|
}
|
||||||
123
Room/Room.cs
123
Room/Room.cs
@@ -1,106 +1,131 @@
|
|||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using MauMau_Server.Mau;
|
using System.Text.RegularExpressions;
|
||||||
|
using MauMau_Server.Room.Messages;
|
||||||
|
using MauMau_Server.Websockets;
|
||||||
|
|
||||||
namespace MauMau_Server.Websockets;
|
namespace MauMau_Server.Room;
|
||||||
|
|
||||||
public class Room
|
public class Room
|
||||||
{
|
{
|
||||||
private readonly IRoomManager _roomManager;
|
private readonly IRoomManager _roomManager;
|
||||||
private readonly string _roomId;
|
private readonly string _roomId;
|
||||||
private readonly List<ConnectionInstance> _connections = new();
|
public readonly List<ConnectionInstance> Connections = [];
|
||||||
private ConnectionInstance _host;
|
public ConnectionInstance? Host;
|
||||||
private readonly Chat _chat;
|
public RoomType RoomType;
|
||||||
private readonly Game _game = new();
|
|
||||||
|
|
||||||
public Room(IRoomManager roomManager, string roomId)
|
public Room(IRoomManager roomManager, string roomId)
|
||||||
{
|
{
|
||||||
_roomManager = roomManager;
|
_roomManager = roomManager;
|
||||||
_chat = new Chat(this);
|
RoomType = new Lobby(this);
|
||||||
_roomId = roomId;
|
_roomId = roomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task InstantiateConnection(WebSocket socket)
|
public async Task InstantiateConnection(WebSocket socket, string name)
|
||||||
{
|
{
|
||||||
var connection = AddConnection(socket);
|
var connectionId = Guid.NewGuid();
|
||||||
if (IsEmpty()) _host = connection;
|
|
||||||
_game.AddPlayerToGame(connection);
|
// If the name is empty, set it to "Mau" + the first part of the connection ID, otherwise strip potential HTML from the name and use it
|
||||||
|
var validatedName = !string.IsNullOrWhiteSpace(name)
|
||||||
|
? StripHTML(name)
|
||||||
|
: "Mau" + connectionId.ToString().Split('-')[0];
|
||||||
|
|
||||||
|
var connection = new ConnectionInstance(validatedName, connectionId, socket);
|
||||||
|
if (IsEmpty()) Host = connection;
|
||||||
|
Connections.Add(connection);
|
||||||
|
|
||||||
|
RoomType.OnConnect(connection);
|
||||||
await HandleConnection(connection);
|
await HandleConnection(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleConnection(ConnectionInstance connection)
|
private async Task HandleConnection(ConnectionInstance connection)
|
||||||
{
|
{
|
||||||
BroadcastGameState();
|
|
||||||
var webSocketResponse = await WebsocketManager.ReceiveAsync(connection.Socket);
|
var webSocketResponse = await WebsocketManager.ReceiveAsync(connection.Socket);
|
||||||
while (!webSocketResponse.Result!.CloseStatus.HasValue)
|
while (!webSocketResponse.Result!.CloseStatus.HasValue)
|
||||||
{
|
{
|
||||||
var message = JsonSerializer.Deserialize<MessageDTO>(webSocketResponse.SlicedBuffer);
|
var message = JsonSerializer.Deserialize<RoomMessage<string>>(webSocketResponse.SlicedBuffer);
|
||||||
switch (message.Type)
|
switch (message.Type)
|
||||||
{
|
{
|
||||||
case "GAME":
|
|
||||||
{
|
|
||||||
var gameInput = JsonSerializer.Deserialize<ActionDTO>(message.Payload);
|
|
||||||
_game.HandleAction(connection.ConnectionId, gameInput);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "CHAT":
|
case "CHAT":
|
||||||
{
|
HandleChatMessage(connection, message.Data);
|
||||||
_chat.SendChatMessage(connection.ConnectionId, message.Payload);
|
break;
|
||||||
|
case "KICK":
|
||||||
|
await HandleKick(Guid.Parse(message.Data));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
RoomType.OnMessage(connection, message);
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BroadcastGameState();
|
|
||||||
webSocketResponse = await WebsocketManager.ReceiveAsync(connection.Socket);
|
webSocketResponse = await WebsocketManager.ReceiveAsync(connection.Socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
WebsocketManager.CloseAsync(connection.Socket, webSocketResponse.Result);
|
WebsocketManager.CloseAsync(connection.Socket, webSocketResponse.Result);
|
||||||
HandleDisconnect(connection.ConnectionId);
|
HandleDisconnect(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ConnectionInstance AddConnection(WebSocket socket)
|
private void HandleChatMessage(ConnectionInstance sender, string chatMessage)
|
||||||
{
|
{
|
||||||
var connectionId = Guid.NewGuid().ToString();
|
// Remove HTML from chat message to prevent HTML injection
|
||||||
var connection = new ConnectionInstance(connectionId, socket);
|
var cleanedMessage = StripHTML(chatMessage);
|
||||||
_connections.Add(connection);
|
|
||||||
return connection;
|
// If the message is empty, set it to "Mau!"
|
||||||
|
if (string.IsNullOrWhiteSpace(cleanedMessage)) cleanedMessage = "Mau!";
|
||||||
|
|
||||||
|
// Create a new chat message object with the sender and the message
|
||||||
|
var envelope = new ChatMessage(sender.Name, cleanedMessage);
|
||||||
|
|
||||||
|
// Broadcast the chat message to all connections in the room
|
||||||
|
BroadCast(new RoomMessage<ChatMessage>("CHAT", envelope));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveConnection(string socketId)
|
private async Task HandleKick(Guid connectionId)
|
||||||
{
|
{
|
||||||
_connections.RemoveAll(connection => connection.ConnectionId == socketId);
|
// Search for the connection with the given ID
|
||||||
|
var connection = Connections.FirstOrDefault(connection => connection.Id == connectionId);
|
||||||
|
|
||||||
|
// If the connection could not be found, return
|
||||||
|
if (connection == null) return;
|
||||||
|
|
||||||
|
// Handle the disconnect of the connection
|
||||||
|
HandleDisconnect(connection);
|
||||||
|
|
||||||
|
// Close the connection with the reason "You have been kicked"
|
||||||
|
await connection.Socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "You have been kicked",
|
||||||
|
CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BroadcastGameState()
|
private void HandleDisconnect(ConnectionInstance connection)
|
||||||
{
|
{
|
||||||
foreach (var connection in _connections)
|
Connections.Remove(connection);
|
||||||
{
|
RoomType.OnDisconnect(connection);
|
||||||
var gameState = new GameState(_game, connection.ConnectionId);
|
|
||||||
var message = new MessageDTO("GAME", JsonSerializer.Serialize(gameState));
|
|
||||||
WebsocketManager.SendAsync(connection.Socket, JsonSerializer.Serialize(message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleDisconnect(string socketId)
|
|
||||||
{
|
|
||||||
RemoveConnection(socketId);
|
|
||||||
_game.RemovePlayer(socketId);
|
|
||||||
if (IsEmpty())
|
if (IsEmpty())
|
||||||
{
|
{
|
||||||
_roomManager.RemoveRoom(_roomId);
|
_roomManager.RemoveRoom(_roomId);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else if (socketId == _host.ConnectionId)
|
|
||||||
|
if (connection == Host)
|
||||||
{
|
{
|
||||||
_host = _connections.First();
|
Host = Connections.First();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<WebSocket> GetWebsockets()
|
public void BroadCast<T>(RoomMessage<T> message)
|
||||||
{
|
{
|
||||||
return _connections.Select(connection => connection.Socket).ToList();
|
foreach (var connection in Connections)
|
||||||
|
{
|
||||||
|
connection.SendMessageAsync(JsonSerializer.Serialize(message));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsEmpty()
|
public bool IsEmpty()
|
||||||
{
|
{
|
||||||
return _connections.Count == 0;
|
return Connections.Count == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string StripHTML(string input)
|
||||||
|
{
|
||||||
|
return Regex.Replace(input, "<.*?>", string.Empty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace MauMau_Server.Websockets;
|
namespace MauMau_Server.Room;
|
||||||
|
|
||||||
public class RoomManager : IRoomManager
|
public class RoomManager : IRoomManager
|
||||||
{
|
{
|
||||||
|
|||||||
20
Room/RoomMessage.cs
Normal file
20
Room/RoomMessage.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace MauMau_Server.Room;
|
||||||
|
|
||||||
|
public class RoomMessage<T>
|
||||||
|
{
|
||||||
|
public string Type { get; set; }
|
||||||
|
public string Data { get; set; }
|
||||||
|
|
||||||
|
public RoomMessage(string type, T data)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
Data = JsonSerializer.Serialize(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RoomMessage()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
38
Room/RoomType.cs
Normal file
38
Room/RoomType.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using MauMau_Server.Websockets;
|
||||||
|
|
||||||
|
namespace MauMau_Server.Room;
|
||||||
|
|
||||||
|
public abstract class RoomType
|
||||||
|
{
|
||||||
|
protected readonly Room _room;
|
||||||
|
|
||||||
|
protected RoomType(Room room)
|
||||||
|
{
|
||||||
|
_room = room;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>
|
||||||
|
* This method is called when a message is received from a ConnectionInstance
|
||||||
|
* </summary>
|
||||||
|
* <param name="sender">The ConnectionInstance that sent the message</param>
|
||||||
|
* <param name="message">The message received</param>
|
||||||
|
*/
|
||||||
|
public abstract void OnMessage(ConnectionInstance sender, RoomMessage<string> message);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>
|
||||||
|
* This method is called when a new ConnectionInstance is added to the Room.
|
||||||
|
* </summary>
|
||||||
|
* <param name="connection">The ConnectionInstance that was added to the Room</param>
|
||||||
|
*/
|
||||||
|
public abstract void OnConnect(ConnectionInstance connection);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>
|
||||||
|
* This method is called when a ConnectionInstance is either removed from the Room or the ConnectionInstance's WebSocket is closed.
|
||||||
|
* </summary>
|
||||||
|
* <param name="connection">The ConnectionInstance that was removed from the Room</param>
|
||||||
|
*/
|
||||||
|
public abstract void OnDisconnect(ConnectionInstance connection);
|
||||||
|
}
|
||||||
@@ -1,15 +1,26 @@
|
|||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace MauMau_Server.Websockets;
|
namespace MauMau_Server.Websockets;
|
||||||
|
|
||||||
public class ConnectionInstance
|
public class ConnectionInstance(string name, Guid id, WebSocket socket)
|
||||||
{
|
{
|
||||||
public string ConnectionId { get; set; }
|
public Guid Id { get; set; } = id;
|
||||||
public WebSocket Socket { get; set; }
|
public string Name { get; set; } = name;
|
||||||
|
|
||||||
public ConnectionInstance(string connectionId, WebSocket socket)
|
[JsonIgnore]
|
||||||
|
public WebSocket Socket { get; set; } = socket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>
|
||||||
|
* Sends a message to the client. This method is asynchronous and formats the message to be ready to be sent.
|
||||||
|
* </summary>
|
||||||
|
*/
|
||||||
|
public void SendMessageAsync(string message)
|
||||||
{
|
{
|
||||||
ConnectionId = connectionId;
|
var bytes = Encoding.Default.GetBytes(message);
|
||||||
Socket = socket;
|
var arraySegment = new ArraySegment<byte>(bytes);
|
||||||
|
Socket.SendAsync(arraySegment, WebSocketMessageType.Text, true, CancellationToken.None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,21 +5,6 @@ namespace MauMau_Server.Websockets;
|
|||||||
|
|
||||||
public static class WebsocketManager
|
public static class WebsocketManager
|
||||||
{
|
{
|
||||||
public static void SendAsync(WebSocket socket, string message)
|
|
||||||
{
|
|
||||||
var bytes = Encoding.Default.GetBytes(message);
|
|
||||||
var arraySegment = new ArraySegment<byte>(bytes);
|
|
||||||
socket.SendAsync(arraySegment, WebSocketMessageType.Text, true, CancellationToken.None);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void BroadcastAsync(List<WebSocket> sockets, string message)
|
|
||||||
{
|
|
||||||
foreach (var socket in sockets)
|
|
||||||
{
|
|
||||||
SendAsync(socket, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<WebSocketResponse> ReceiveAsync(WebSocket webSocket)
|
public static async Task<WebSocketResponse> ReceiveAsync(WebSocket webSocket)
|
||||||
{
|
{
|
||||||
var buffer = new byte[4096];
|
var buffer = new byte[4096];
|
||||||
@@ -30,7 +15,10 @@ public static class WebsocketManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async void CloseAsync(WebSocket webSocket, WebSocketReceiveResult result)
|
public static async void CloseAsync(WebSocket webSocket, WebSocketReceiveResult result)
|
||||||
|
{
|
||||||
|
if (webSocket.State is WebSocketState.Open or WebSocketState.CloseReceived or WebSocketState.CloseSent)
|
||||||
{
|
{
|
||||||
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
|
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
version: '3.9'
|
|
||||||
services:
|
|
||||||
server:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: 'MauMau-Server'
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
networks:
|
|
||||||
- MauMau
|
|
||||||
networks:
|
|
||||||
MauMau:
|
|
||||||
driver: bridge
|
|
||||||
Reference in New Issue
Block a user