add explosive bullet power up

This commit is contained in:
Vinzenz Schroeter 2024-04-17 19:34:19 +02:00
parent 3f4a301993
commit a2d46bda92
30 changed files with 407 additions and 253 deletions

View file

@ -104,3 +104,8 @@ There are other commands implemented as well, e.g. for changing the brightness.
- 10: bullet
- 11: (reserved)
- client responds with empty message to request the next frame
## Backlog: Bugs, Wishes, Ideas
- Generalize drawing of entities as there are multiple classes with pretty much the same code
- Generalize hit box collision
- BUG: when standing next to a wall, the bullet sometimes misses the first pixel

76
TanksServer/Endpoints.cs Normal file
View file

@ -0,0 +1,76 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using TanksServer.GameLogic;
using TanksServer.Interactivity;
namespace TanksServer;
internal static class Endpoints
{
public static void MapEndpoints(WebApplication app)
{
var clientScreenServer = app.Services.GetRequiredService<ClientScreenServer>();
var playerService = app.Services.GetRequiredService<PlayerServer>();
var controlsServer = app.Services.GetRequiredService<ControlsServer>();
var mapService = app.Services.GetRequiredService<MapService>();
app.MapPost("/player", (string name, Guid? id) =>
{
name = name.Trim().ToUpperInvariant();
if (name == string.Empty)
return Results.BadRequest("name cannot be blank");
if (name.Length > 12)
return Results.BadRequest("name too long");
var player = playerService.GetOrAdd(name, id ?? Guid.NewGuid());
return player != null
? Results.Ok(new NameId(player.Name, player.Id))
: Results.Unauthorized();
});
app.MapGet("/player", ([FromQuery] Guid id) =>
playerService.TryGet(id, out var foundPlayer)
? Results.Ok((object?)foundPlayer)
: Results.NotFound()
);
app.MapGet("/scores", () => playerService.GetAll());
app.Map("/screen", async (HttpContext context, [FromQuery] Guid? player) =>
{
if (!context.WebSockets.IsWebSocketRequest)
return Results.BadRequest();
using var ws = await context.WebSockets.AcceptWebSocketAsync();
await clientScreenServer.HandleClient(ws, player);
return Results.Empty;
});
app.Map("/controls", async (HttpContext context, [FromQuery] Guid playerId) =>
{
if (!context.WebSockets.IsWebSocketRequest)
return Results.BadRequest();
if (!playerService.TryGet(playerId, out var player))
return Results.NotFound();
using var ws = await context.WebSockets.AcceptWebSocketAsync();
await controlsServer.HandleClient(ws, player);
return Results.Empty;
});
app.MapGet("/map", () => mapService.MapNames);
app.MapPost("/map", ([FromQuery] string name) =>
{
if (string.IsNullOrWhiteSpace(name))
return Results.BadRequest("invalid map name");
if (!mapService.TrySwitchTo(name))
return Results.NotFound("map with name not found");
return Results.Ok();
});
}
}

View file

@ -1,13 +0,0 @@
namespace TanksServer.GameLogic;
internal sealed class BulletManager
{
private readonly HashSet<Bullet> _bullets = [];
public void Spawn(Player tankOwner, FloatPosition position, double rotation)
=> _bullets.Add(new Bullet(tankOwner, position, rotation));
public IEnumerable<Bullet> GetAll() => _bullets;
public void RemoveWhere(Predicate<Bullet> predicate) => _bullets.RemoveWhere(predicate);
}

View file

@ -0,0 +1,30 @@
namespace TanksServer.GameLogic;
internal sealed class CollectPowerUp(
MapEntityManager entityManager
) : ITickStep
{
public Task TickAsync(TimeSpan delta)
{
entityManager.RemoveWhere(TryCollect);
return Task.CompletedTask;
}
private bool TryCollect(PowerUp obj)
{
var position = obj.Position;
foreach (var tank in entityManager.Tanks)
{
var (topLeft, bottomRight) = tank.Bounds;
if (position.X < topLeft.X || position.X > bottomRight.X ||
position.Y < topLeft.Y || position.Y > bottomRight.Y)
continue;
// this works because now the tank overlaps the power up
tank.ExplosiveBullets += 10;
return true;
}
return false;
}
}

View file

@ -0,0 +1,73 @@
namespace TanksServer.GameLogic;
internal sealed class CollideBullets(
MapEntityManager entityManager,
MapService map,
IOptions<GameRules> options,
TankSpawnQueue tankSpawnQueue
) : ITickStep
{
private const int ExplosionRadius = 3;
public Task TickAsync(TimeSpan _)
{
entityManager.RemoveBulletsWhere(BulletHitsTank);
entityManager.RemoveBulletsWhere(TryHitAndDestroyWall);
return Task.CompletedTask;
}
private bool TryHitAndDestroyWall(Bullet bullet)
{
var pixel = bullet.Position.ToPixelPosition();
if (!map.Current.IsWall(pixel))
return false;
var radius = bullet.IsExplosive ? ExplosionRadius : 0;
ExplodeAt(pixel, radius, bullet.Owner);
return true;
}
private bool BulletHitsTank(Bullet bullet)
{
if (!TryHitTankAt(bullet.Position, bullet.Owner))
return false;
if (bullet.IsExplosive)
ExplodeAt(bullet.Position.ToPixelPosition(), ExplosionRadius, bullet.Owner);
return true;
}
private bool TryHitTankAt(FloatPosition position, Player owner)
{
foreach (var tank in entityManager.Tanks)
{
var (topLeft, bottomRight) = tank.Bounds;
if (position.X < topLeft.X || position.X > bottomRight.X ||
position.Y < topLeft.Y || position.Y > bottomRight.Y)
continue;
if (owner != tank.Owner)
owner.Scores.Kills++;
tank.Owner.Scores.Deaths++;
entityManager.Remove(tank);
tankSpawnQueue.EnqueueForDelayedSpawn(tank.Owner);
return true;
}
return false;
}
private void ExplodeAt(PixelPosition pixel, int i, Player owner)
{
for (var x = pixel.X - i; x <= pixel.X + i; x++)
for (var y = pixel.Y - i; y <= pixel.Y + i; y++)
{
var offsetPixel = new PixelPosition(x, y);
if (options.Value.DestructibleWalls)
map.Current.DestroyWallAt(offsetPixel);
TryHitTankAt(offsetPixel.ToFloatPosition(), owner);
}
}
}

View file

@ -1,25 +0,0 @@
namespace TanksServer.GameLogic;
internal sealed class CollideBulletsWithMap(
BulletManager bullets,
MapService map,
IOptions<GameRulesConfiguration> options
) : ITickStep
{
public Task TickAsync(TimeSpan _)
{
bullets.RemoveWhere(TryHitAndDestroyWall);
return Task.CompletedTask;
}
private bool TryHitAndDestroyWall(Bullet bullet)
{
var pixel = bullet.Position.ToPixelPosition();
if (!map.Current.IsWall(pixel))
return false;
if (options.Value.DestructibleWalls)
map.Current.DestroyWallAt(pixel);
return true;
}
}

View file

@ -1,36 +0,0 @@
namespace TanksServer.GameLogic;
internal sealed class CollideBulletsWithTanks(
BulletManager bullets,
TankManager tanks,
SpawnQueue spawnQueue
) : ITickStep
{
public Task TickAsync(TimeSpan _)
{
bullets.RemoveWhere(BulletHitsTank);
return Task.CompletedTask;
}
private bool BulletHitsTank(Bullet bullet)
{
foreach (var tank in tanks)
{
var (topLeft, bottomRight) = tank.Bounds;
if (bullet.Position.X < topLeft.X || bullet.Position.X > bottomRight.X ||
bullet.Position.Y < topLeft.Y || bullet.Position.Y > bottomRight.Y)
continue;
if (bullet.Owner != tank.Owner)
bullet.Owner.Scores.Kills++;
tank.Owner.Scores.Deaths++;
tanks.Remove(tank);
spawnQueue.EnqueueForDelayedSpawn(tank.Owner);
return true;
}
return false;
}
}

View file

@ -1,6 +1,8 @@
namespace TanksServer.GameLogic;
public class GameRulesConfiguration
internal sealed class GameRules
{
public bool DestructibleWalls { get; set; } = true;
public double PowerUpSpawnChance { get; set; }
}

View file

@ -0,0 +1,65 @@
namespace TanksServer.GameLogic;
internal sealed class MapEntityManager(
ILogger<MapEntityManager> logger,
MapService map
)
{
private readonly HashSet<Bullet> _bullets = [];
private readonly HashSet<Tank> _tanks = [];
private readonly HashSet<PowerUp> _powerUps = [];
public IEnumerable<Bullet> Bullets => _bullets;
public IEnumerable<Tank> Tanks => _tanks;
public IEnumerable<PowerUp> PowerUps => _powerUps;
public void SpawnBullet(Player tankOwner, FloatPosition position, double rotation, bool isExplosive)
=> _bullets.Add(new Bullet(tankOwner, position, rotation, isExplosive));
public void RemoveBulletsWhere(Predicate<Bullet> predicate) => _bullets.RemoveWhere(predicate);
public void SpawnTank(Player player)
{
_tanks.Add(new Tank(player, ChooseSpawnPosition())
{
Rotation = Random.Shared.NextDouble()
});
logger.LogInformation("Tank added for player {}", player.Id);
}
public void SpawnPowerUp() => _powerUps.Add(new PowerUp(ChooseSpawnPosition()));
public void RemoveWhere(Predicate<PowerUp> predicate) => _powerUps.RemoveWhere(predicate);
public void Remove(Tank tank)
{
logger.LogInformation("Tank removed for player {}", tank.Owner.Id);
_tanks.Remove(tank);
}
public FloatPosition ChooseSpawnPosition()
{
Dictionary<TilePosition, double> candidates = [];
for (ushort x = 1; x < MapService.TilesPerRow - 1; x++)
for (ushort y = 1; y < MapService.TilesPerColumn - 1; y++)
{
var tile = new TilePosition(x, y);
if (map.Current.IsWall(tile))
continue;
var tilePixelCenter = tile.ToPixelPosition().GetPixelRelative(4, 4).ToFloatPosition();
var minDistance = Bullets
.Cast<IMapEntity>()
.Concat(Tanks)
.Select(entity => entity.Position.Distance(tilePixelCenter))
.Aggregate(double.MaxValue, Math.Min);
candidates.Add(tile, minDistance);
}
var min = candidates.MaxBy(pair => pair.Value).Key;
return min.ToPixelPosition().GetPixelRelative(4, 4).ToFloatPosition();
}
}

View file

@ -1,10 +1,13 @@
namespace TanksServer.GameLogic;
internal sealed class MoveBullets(BulletManager bullets, IOptions<TanksConfiguration> config) : ITickStep
internal sealed class MoveBullets(
MapEntityManager entityManager,
IOptions<TanksConfiguration> config
) : ITickStep
{
public Task TickAsync(TimeSpan delta)
{
foreach (var bullet in bullets.GetAll())
foreach (var bullet in entityManager.Bullets)
MoveBullet(bullet, delta);
return Task.CompletedTask;

View file

@ -1,7 +1,7 @@
namespace TanksServer.GameLogic;
internal sealed class MoveTanks(
TankManager tanks,
MapEntityManager entityManager,
IOptions<TanksConfiguration> options,
MapService map
) : ITickStep
@ -10,7 +10,7 @@ internal sealed class MoveTanks(
public Task TickAsync(TimeSpan delta)
{
foreach (var tank in tanks)
foreach (var tank in entityManager.Tanks)
tank.Moved = TryMoveTank(tank, delta);
return Task.CompletedTask;
@ -59,13 +59,13 @@ internal sealed class MoveTanks(
}
private bool HitsTank(Tank tank, FloatPosition newPosition) =>
tanks
entityManager.Tanks
.Where(otherTank => otherTank != tank)
.Any(otherTank => newPosition.Distance(otherTank.Position) < MapService.TileSize);
private bool HitsWall(FloatPosition newPosition)
{
var (topLeft, _) = Tank.GetBoundsForCenter(newPosition);
var (topLeft, _) = newPosition.GetBoundsForCenter(MapService.TileSize);
for (short y = 0; y < MapService.TileSize; y++)
for (short x = 0; x < MapService.TileSize; x++)

View file

@ -1,7 +1,7 @@
namespace TanksServer.GameLogic;
internal sealed class RotateTanks(
TankManager tanks,
MapEntityManager entityManager,
IOptions<TanksConfiguration> options,
ILogger<RotateTanks> logger
) : ITickStep
@ -10,7 +10,7 @@ internal sealed class RotateTanks(
public Task TickAsync(TimeSpan delta)
{
foreach (var tank in tanks)
foreach (var tank in entityManager.Tanks)
{
var player = tank.Owner;

View file

@ -3,16 +3,15 @@ using System.Diagnostics;
namespace TanksServer.GameLogic;
internal sealed class ShootFromTanks(
TankManager tanks,
IOptions<TanksConfiguration> options,
BulletManager bulletManager
MapEntityManager entityManager
) : ITickStep
{
private readonly TanksConfiguration _config = options.Value;
public Task TickAsync(TimeSpan _)
{
foreach (var tank in tanks.Where(t => !t.Moved))
foreach (var tank in entityManager.Tanks.Where(t => !t.Moved))
Shoot(tank);
return Task.CompletedTask;
@ -30,7 +29,7 @@ internal sealed class ShootFromTanks(
var rotation = tank.Orientation / 16d;
var angle = rotation * 2d * Math.PI;
/* TODO: when standing next to a wall, the bullet sometimes misses the first pixel.
/* When standing next to a wall, the bullet sometimes misses the first pixel.
Spawning the bullet to close to the tank instead means the tank instantly hits itself.
Because the tank has a float position, but hit boxes are based on pixels, this problem has been deemed complex
enough to do later. These values mostly work. */
@ -47,6 +46,13 @@ internal sealed class ShootFromTanks(
tank.Position.Y - Math.Cos(angle) * distance
);
bulletManager.Spawn(tank.Owner, position, rotation);
var explosive = false;
if (tank.ExplosiveBullets > 0)
{
tank.ExplosiveBullets--;
explosive = true;
}
entityManager.SpawnBullet(tank.Owner, position, rotation, explosive);
}
}

View file

@ -1,48 +0,0 @@
namespace TanksServer.GameLogic;
internal sealed class SpawnNewTanks(
TankManager tanks,
MapService map,
SpawnQueue queue,
BulletManager bullets
) : ITickStep
{
public Task TickAsync(TimeSpan _)
{
if (!queue.TryDequeueNext(out var player))
return Task.CompletedTask;
tanks.Add(new Tank(player, ChooseSpawnPosition())
{
Rotation = Random.Shared.NextDouble()
});
return Task.CompletedTask;
}
private FloatPosition ChooseSpawnPosition()
{
Dictionary<TilePosition, double> candidates = [];
for (ushort x = 1; x < MapService.TilesPerRow - 1; x++)
for (ushort y = 1; y < MapService.TilesPerColumn - 1; y++)
{
var tile = new TilePosition(x, y);
if (map.Current.IsWall(tile))
continue;
var tilePixelCenter = tile.ToPixelPosition().GetPixelRelative(4, 4).ToFloatPosition();
var minDistance = bullets.GetAll()
.Cast<IMapEntity>()
.Concat(tanks)
.Select(entity => entity.Position.Distance(tilePixelCenter))
.Aggregate(double.MaxValue, Math.Min);
candidates.Add(tile, minDistance);
}
var min = candidates.MaxBy(kvp => kvp.Value).Key;
return min.ToPixelPosition().GetPixelRelative(4, 4).ToFloatPosition();
}
}

View file

@ -0,0 +1,18 @@
namespace TanksServer.GameLogic;
internal sealed class SpawnPowerUp(
IOptions<GameRules> options,
MapEntityManager entityManager
) : ITickStep
{
private readonly double _spawnChance = options.Value.PowerUpSpawnChance;
public Task TickAsync(TimeSpan delta)
{
if (Random.Shared.NextDouble() > _spawnChance * delta.TotalSeconds)
return Task.CompletedTask;
entityManager.SpawnPowerUp();
return Task.CompletedTask;
}
}

View file

@ -1,23 +0,0 @@
using System.Collections;
namespace TanksServer.GameLogic;
internal sealed class TankManager(ILogger<TankManager> logger) : IEnumerable<Tank>
{
private readonly ConcurrentDictionary<Tank, byte> _tanks = new();
public void Add(Tank tank)
{
logger.LogInformation("Tank added for player {}", tank.Owner.Id);
_tanks.TryAdd(tank, 0);
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerator<Tank> GetEnumerator() => _tanks.Keys.GetEnumerator();
public void Remove(Tank tank)
{
logger.LogInformation("Tank removed for player {}", tank.Owner.Id);
_tanks.Remove(tank, out _);
}
}

View file

@ -2,9 +2,10 @@ using System.Diagnostics.CodeAnalysis;
namespace TanksServer.GameLogic;
internal sealed class SpawnQueue(
IOptions<PlayersConfiguration> options
)
internal sealed class TankSpawnQueue(
IOptions<PlayersConfiguration> options,
MapEntityManager entityManager
): ITickStep
{
private readonly ConcurrentQueue<Player> _queue = new();
private readonly ConcurrentDictionary<Player, DateTime> _spawnTimes = new();
@ -41,4 +42,13 @@ internal sealed class SpawnQueue(
return true;
}
public Task TickAsync(TimeSpan _)
{
if (!TryDequeueNext(out var player))
return Task.CompletedTask;
entityManager.SpawnTank(player);
return Task.CompletedTask;
}
}

View file

@ -2,11 +2,11 @@ using TanksServer.GameLogic;
namespace TanksServer.Graphics;
internal sealed class DrawBulletsStep(BulletManager bullets) : IDrawStep
internal sealed class DrawBulletsStep(MapEntityManager entityManager) : IDrawStep
{
public void Draw(GamePixelGrid pixels)
{
foreach (var bullet in bullets.GetAll())
foreach (var bullet in entityManager.Bullets)
{
var position = bullet.Position.ToPixelPosition();
pixels[position.X, position.Y].EntityType = GamePixelEntityType.Bullet;

View file

@ -0,0 +1,52 @@
using System.Diagnostics;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using TanksServer.GameLogic;
namespace TanksServer.Graphics;
internal sealed class DrawPowerUpsStep : IDrawStep
{
private readonly MapEntityManager _entityManager;
private readonly bool?[,] _explosiveSprite;
public DrawPowerUpsStep(MapEntityManager entityManager)
{
_entityManager = entityManager;
using var tankImage = Image.Load<Rgba32>("assets/powerup_explosive.png");
Debug.Assert(tankImage.Width == tankImage.Height && tankImage.Width == MapService.TileSize);
_explosiveSprite = new bool?[tankImage.Width, tankImage.Height];
var whitePixel = new Rgba32(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue);
for (var y = 0; y < tankImage.Height; y++)
for (var x = 0; x < tankImage.Width; x++)
{
var pixelValue = tankImage[x, y];
_explosiveSprite[x, y] = pixelValue.A == 0
? null
: pixelValue == whitePixel;
}
}
public void Draw(GamePixelGrid pixels)
{
foreach (var powerUp in _entityManager.PowerUps)
{
var position = powerUp.Bounds.TopLeft;
for (byte dy = 0; dy < MapService.TileSize; dy++)
for (byte dx = 0; dx < MapService.TileSize; dx++)
{
var pixelState = _explosiveSprite[dx, dy];
if (!pixelState.HasValue)
continue;
var (x, y) = position.GetPixelRelative(dx, dy);
pixels[x, y].EntityType = pixelState.Value
? GamePixelEntityType.PowerUp
: null;
}
}
}
}

View file

@ -6,13 +6,13 @@ namespace TanksServer.Graphics;
internal sealed class DrawTanksStep : IDrawStep
{
private readonly TankManager _tanks;
private readonly MapEntityManager _entityManager;
private readonly bool[] _tankSprite;
private readonly int _tankSpriteWidth;
public DrawTanksStep(TankManager tanks)
public DrawTanksStep(MapEntityManager entityManager)
{
_tanks = tanks;
_entityManager = entityManager;
using var tankImage = Image.Load<Rgba32>("assets/tank.png");
_tankSprite = new bool[tankImage.Height * tankImage.Width];
@ -28,7 +28,7 @@ internal sealed class DrawTanksStep : IDrawStep
public void Draw(GamePixelGrid pixels)
{
foreach (var tank in _tanks)
foreach (var tank in _entityManager.Tanks)
{
var tankPosition = tank.Bounds.TopLeft;

View file

@ -4,5 +4,6 @@ internal enum GamePixelEntityType : byte
{
Wall = 0x0,
Tank = 0x1,
Bullet = 0x2
Bullet = 0x2,
PowerUp = 0x3
}

View file

@ -26,7 +26,7 @@ internal sealed class PlayerScreenData(ILogger logger)
{
var result = (byte)(isCurrentPlayer ? 0x1 : 0x0);
var kind = (byte)entityKind;
Debug.Assert(kind < 3);
Debug.Assert(kind <= 3);
result += (byte)(kind << 2);
var index = _count / 2;

View file

@ -3,7 +3,7 @@ using TanksServer.GameLogic;
namespace TanksServer.Interactivity;
internal sealed class PlayerServer(ILogger<PlayerServer> logger, SpawnQueue spawnQueue)
internal sealed class PlayerServer(ILogger<PlayerServer> logger, TankSpawnQueue tankSpawnQueue)
{
private readonly ConcurrentDictionary<string, Player> _players = new();
@ -12,7 +12,7 @@ internal sealed class PlayerServer(ILogger<PlayerServer> logger, SpawnQueue spaw
Player AddAndSpawn()
{
var player = new Player(name, id);
spawnQueue.EnqueueForImmediateSpawn(player);
tankSpawnQueue.EnqueueForImmediateSpawn(player);
return player;
}

View file

@ -1,12 +1,14 @@
namespace TanksServer.Models;
internal sealed class Bullet(Player tankOwner, FloatPosition position, double rotation) : IMapEntity
internal sealed class Bullet(Player tankOwner, FloatPosition position, double rotation, bool isExplosive) : IMapEntity
{
public Player Owner { get; } = tankOwner;
public double Rotation { get; set; } = rotation;
public double Rotation { get; } = rotation;
public FloatPosition Position { get; set; } = position;
public bool IsExplosive { get; } = isExplosive;
public PixelBounds Bounds => new (Position.ToPixelPosition(), Position.ToPixelPosition());
}

View file

@ -28,4 +28,15 @@ internal static class PositionHelpers
Math.Pow(p1.X - p2.X, 2) +
Math.Pow(p1.Y - p2.Y, 2)
);
public static PixelBounds GetBoundsForCenter(this FloatPosition position, ushort size)
{
var sub = (short)(-size / 2d);
var add = (short)(size / 2d - 1);
var pixelPosition = position.ToPixelPosition();
return new PixelBounds(
pixelPosition.GetPixelRelative(sub, sub),
pixelPosition.GetPixelRelative(add, add)
);
}
}

View file

@ -0,0 +1,10 @@
using TanksServer.GameLogic;
namespace TanksServer.Models;
internal sealed class PowerUp(FloatPosition position): IMapEntity
{
public FloatPosition Position { get; set; } = position;
public PixelBounds Bounds => Position.GetBoundsForCenter(MapService.TileSize);
}

View file

@ -1,4 +1,5 @@
using System.Diagnostics;
using TanksServer.GameLogic;
namespace TanksServer.Models;
@ -25,16 +26,9 @@ internal sealed class Tank(Player player, FloatPosition spawnPosition) : IMapEnt
public FloatPosition Position { get; set; } = spawnPosition;
public PixelBounds Bounds => GetBoundsForCenter(Position);
public PixelBounds Bounds => Position.GetBoundsForCenter(MapService.TileSize);
public int Orientation => (int)Math.Round(Rotation * 16) % 16;
public static PixelBounds GetBoundsForCenter(FloatPosition position)
{
var pixelPosition = position.ToPixelPosition();
return new PixelBounds(
pixelPosition.GetPixelRelative(-4, -4),
pixelPosition.GetPixelRelative(3, 3)
);
}
public byte ExplosiveBullets { get; set; }
}

View file

@ -1,8 +1,6 @@
using System.IO;
using DisplayCommands;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
@ -20,70 +18,11 @@ public static class Program
{
var app = Configure(args);
var clientScreenServer = app.Services.GetRequiredService<ClientScreenServer>();
var playerService = app.Services.GetRequiredService<PlayerServer>();
var controlsServer = app.Services.GetRequiredService<ControlsServer>();
var mapService = app.Services.GetRequiredService<MapService>();
var clientFileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "client"));
app.UseDefaultFiles(new DefaultFilesOptions { FileProvider = clientFileProvider });
app.UseStaticFiles(new StaticFileOptions { FileProvider = clientFileProvider });
app.MapPost("/player", (string name, Guid? id) =>
{
name = name.Trim().ToUpperInvariant();
if (name == string.Empty)
return Results.BadRequest("name cannot be blank");
if (name.Length > 12)
return Results.BadRequest("name too long");
var player = playerService.GetOrAdd(name, id ?? Guid.NewGuid());
return player != null
? Results.Ok(new NameId(player.Name, player.Id))
: Results.Unauthorized();
});
app.MapGet("/player", ([FromQuery] Guid id) =>
playerService.TryGet(id, out var foundPlayer)
? Results.Ok((object?)foundPlayer)
: Results.NotFound()
);
app.MapGet("/scores", () => playerService.GetAll());
app.Map("/screen", async (HttpContext context, [FromQuery] Guid? player) =>
{
if (!context.WebSockets.IsWebSocketRequest)
return Results.BadRequest();
using var ws = await context.WebSockets.AcceptWebSocketAsync();
await clientScreenServer.HandleClient(ws, player);
return Results.Empty;
});
app.Map("/controls", async (HttpContext context, [FromQuery] Guid playerId) =>
{
if (!context.WebSockets.IsWebSocketRequest)
return Results.BadRequest();
if (!playerService.TryGet(playerId, out var player))
return Results.NotFound();
using var ws = await context.WebSockets.AcceptWebSocketAsync();
await controlsServer.HandleClient(ws, player);
return Results.Empty;
});
app.MapGet("/map", () => mapService.MapNames);
app.MapPost("/map", ([FromQuery] string name) =>
{
if (string.IsNullOrWhiteSpace(name))
return Results.BadRequest("invalid map name");
if (!mapService.TrySwitchTo(name))
return Results.NotFound("map with name not found");
return Results.Ok();
});
Endpoints.MapEndpoints(app);
app.Run();
}
@ -119,27 +58,28 @@ public static class Program
throw new InvalidOperationException("'Host' configuration missing");
builder.Services.AddSingleton<MapService>();
builder.Services.AddSingleton<BulletManager>();
builder.Services.AddSingleton<TankManager>();
builder.Services.AddSingleton<MapEntityManager>();
builder.Services.AddSingleton<ControlsServer>();
builder.Services.AddSingleton<PlayerServer>();
builder.Services.AddSingleton<ClientScreenServer>();
builder.Services.AddSingleton<SpawnQueue>();
builder.Services.AddSingleton<TankSpawnQueue>();
builder.Services.AddHostedService<GameTickWorker>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<ControlsServer>());
builder.Services.AddHostedService(sp => sp.GetRequiredService<ClientScreenServer>());
builder.Services.AddSingleton<ITickStep, MoveBullets>();
builder.Services.AddSingleton<ITickStep, CollideBulletsWithTanks>();
builder.Services.AddSingleton<ITickStep, CollideBulletsWithMap>();
builder.Services.AddSingleton<ITickStep, CollideBullets>();
builder.Services.AddSingleton<ITickStep, RotateTanks>();
builder.Services.AddSingleton<ITickStep, MoveTanks>();
builder.Services.AddSingleton<ITickStep, ShootFromTanks>();
builder.Services.AddSingleton<ITickStep, SpawnNewTanks>();
builder.Services.AddSingleton<ITickStep, CollectPowerUp>();
builder.Services.AddSingleton<ITickStep>(sp => sp.GetRequiredService<TankSpawnQueue>());
builder.Services.AddSingleton<ITickStep, SpawnPowerUp>();
builder.Services.AddSingleton<ITickStep, GeneratePixelsTickStep>();
builder.Services.AddSingleton<IDrawStep, DrawMapStep>();
builder.Services.AddSingleton<IDrawStep, DrawPowerUpsStep>();
builder.Services.AddSingleton<IDrawStep, DrawTanksStep>();
builder.Services.AddSingleton<IDrawStep, DrawBulletsStep>();
@ -150,7 +90,7 @@ public static class Program
builder.Configuration.GetSection("Tanks"));
builder.Services.Configure<PlayersConfiguration>(
builder.Configuration.GetSection("Players"));
builder.Services.Configure<GameRulesConfiguration>(builder.Configuration.GetSection("GameRules"));
builder.Services.Configure<GameRules>(builder.Configuration.GetSection("GameRules"));
if (hostConfiguration.EnableServicePointDisplay)
{

View file

@ -26,7 +26,8 @@
"BulletSpeed": 75
},
"GameRules": {
"DestructibleWalls": true
"DestructibleWalls": true,
"PowerUpSpawnChance": 0.1
},
"Players": {
"SpawnDelayMs": 3000,

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B