feat: add backend for multiplayer support
This commit is contained in:
193
backend/Program.cs
Normal file
193
backend/Program.cs
Normal file
@@ -0,0 +1,193 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var app = builder.Build();
|
||||
|
||||
var games = new ConcurrentDictionary<string, Game>();
|
||||
|
||||
app.UseWebSockets();
|
||||
|
||||
app.Map("/", async context =>
|
||||
{
|
||||
if (!context.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
var ws = await context.WebSockets.AcceptWebSocketAsync();
|
||||
var remoteAddress = context.Connection.RemoteIpAddress?.ToString();
|
||||
|
||||
var player = new Player(ws, remoteAddress);
|
||||
|
||||
try
|
||||
{
|
||||
var buffer = new byte[4096];
|
||||
var messageBuffer = new ArraySegment<byte>(buffer);
|
||||
while (ws.State == WebSocketState.Open)
|
||||
{
|
||||
var message = new MemoryStream();
|
||||
WebSocketReceiveResult result;
|
||||
|
||||
do
|
||||
{
|
||||
result = await ws.ReceiveAsync(messageBuffer, CancellationToken.None);
|
||||
message.Write(buffer, 0, result.Count);
|
||||
}
|
||||
while (!result.EndOfMessage);
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
break;
|
||||
|
||||
var msgText = Encoding.UTF8.GetString(message.ToArray());
|
||||
|
||||
if (msgText == "ping")
|
||||
{
|
||||
await player.SendTextAsync("pong");
|
||||
continue;
|
||||
}
|
||||
|
||||
var json = JsonDocument.Parse(msgText).RootElement;
|
||||
var action = json.GetProperty("action").GetString();
|
||||
var gameId = json.GetProperty("gameId").GetString();
|
||||
var clientId = json.GetProperty("clientId").GetString();
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case "enter_game":
|
||||
await GameHandlers.EnterGame(gameId, clientId, player, games);
|
||||
break;
|
||||
case "send_piece":
|
||||
await GameHandlers.SendToOpponent(gameId, player, "receive_piece", json.GetProperty("piece"), games);
|
||||
break;
|
||||
case "send_stack":
|
||||
await GameHandlers.SendToOpponent(gameId, player, "receive_stack", json.GetProperty("stack"), games);
|
||||
break;
|
||||
case "send_stats":
|
||||
await GameHandlers.SendToOpponent(gameId, player, "receive_stats", json.GetProperty("stats"), games);
|
||||
break;
|
||||
default:
|
||||
Console.WriteLine("Unsupported action.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.WriteLine("Disconnecting player...");
|
||||
await GameHandlers.ExitGame(player, games);
|
||||
Console.WriteLine("Connection closed.");
|
||||
}
|
||||
});
|
||||
|
||||
app.Run();
|
||||
|
||||
// -------------------------
|
||||
// Models
|
||||
// -------------------------
|
||||
|
||||
record Player(WebSocket Socket, string? RemoteAddress)
|
||||
{
|
||||
public async Task SendTextAsync(string message)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(message);
|
||||
await Socket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task SendJsonAsync(object obj)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(obj);
|
||||
await SendTextAsync(json);
|
||||
}
|
||||
}
|
||||
|
||||
class Game
|
||||
{
|
||||
public string GameId { get; }
|
||||
public List<Player> Players { get; } = new();
|
||||
public List<string> Clients { get; } = new();
|
||||
|
||||
public Game(string gameId)
|
||||
{
|
||||
GameId = gameId;
|
||||
}
|
||||
|
||||
public Player? GetOpponent(Player player) => Players.FirstOrDefault(p => p != player);
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// Handlers
|
||||
// -------------------------
|
||||
|
||||
static class GameHandlers
|
||||
{
|
||||
public static async Task EnterGame(string gameId, string clientId, Player player, ConcurrentDictionary<string, Game> games)
|
||||
{
|
||||
var game = games.GetOrAdd(gameId, _ => new Game(gameId));
|
||||
|
||||
if (!game.Clients.Contains(clientId))
|
||||
{
|
||||
game.Clients.Add(clientId);
|
||||
game.Players.Add(player);
|
||||
|
||||
if (game.Players.Count == 1)
|
||||
{
|
||||
await player.SendJsonAsync(new { type = "wait_for_opponent" });
|
||||
}
|
||||
else if (game.Players.Count == 2)
|
||||
{
|
||||
foreach (var p in game.Players)
|
||||
await p.SendJsonAsync(new { type = "start_game" });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Already in game...");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SendToOpponent(string gameId, Player sender, string type, JsonElement content, ConcurrentDictionary<string, Game> games)
|
||||
{
|
||||
if (!games.TryGetValue(gameId, out var game))
|
||||
return;
|
||||
|
||||
var opponent = game.GetOpponent(sender);
|
||||
if (opponent != null)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case "receive_piece":
|
||||
await opponent.SendJsonAsync(new { type, piece = content });
|
||||
break;
|
||||
case "receive_stack":
|
||||
await opponent.SendJsonAsync(new { type, stack = content });
|
||||
break;
|
||||
case "receive_stats":
|
||||
await opponent.SendJsonAsync(new { type, stats = content });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ExitGame(Player disconnectingPlayer, ConcurrentDictionary<string, Game> games)
|
||||
{
|
||||
var game = games.Values.FirstOrDefault(g => g.Players.Contains(disconnectingPlayer));
|
||||
if (game != null)
|
||||
{
|
||||
games.TryRemove(game.GameId, out _);
|
||||
|
||||
var opponent = game.GetOpponent(disconnectingPlayer);
|
||||
if (opponent != null)
|
||||
{
|
||||
await opponent.SendJsonAsync(new { type = "exit_game" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user