diff --git a/api/messages.proto b/api/messages.proto index b102835..e54e377 100644 --- a/api/messages.proto +++ b/api/messages.proto @@ -6,8 +6,23 @@ message PingResponse { bool IsSuccessful = 1; } +message CreateWorldRequest { + int32 SizeX = 1; + int32 SizeY = 2; + repeated Bot Bots = 3; +} + +message CreateWorldResponse { + string MapId = 1; +} + +message GetBotsRequest { + string MapId = 1; +} + message GetBotsResponse { - repeated Bot Bots = 1; + string MapId = 1; + repeated Bot Bots = 2; } message SelectBotRequest { diff --git a/api/pathfinder.proto b/api/pathfinder.proto index 1e41950..098639e 100644 --- a/api/pathfinder.proto +++ b/api/pathfinder.proto @@ -8,8 +8,9 @@ import "messages.proto"; import "google/protobuf/empty.proto"; service PathfinderService { + rpc CreateWorld(pathfinder.messages.CreateWorldRequest) returns(pathfinder.messages.CreateWorldResponse); rpc Ping(google.protobuf.Empty) returns(pathfinder.messages.PingResponse); - rpc GetBots(google.protobuf.Empty) returns(pathfinder.messages.GetBotsResponse); + rpc GetBots(pathfinder.messages.GetBotsRequest) returns(pathfinder.messages.GetBotsResponse); rpc SelectBot(pathfinder.messages.SelectBotRequest) returns(pathfinder.messages.SelectBotResponse); rpc ResetBot(pathfinder.messages.ResetBotRequest) returns(pathfinder.messages.ResetBotResponse); rpc SendMessage(stream pathfinder.messages.SendMessageRequest) returns (stream pathfinder.messages.SendMessageResponse); diff --git a/cd/stage/docker-compose.local.yaml b/cd/stage/docker-compose.local.yaml new file mode 100644 index 0000000..f3609bd --- /dev/null +++ b/cd/stage/docker-compose.local.yaml @@ -0,0 +1,6 @@ +services: + nasa-server: + image: ghcr.io/karmeev/mars-pathfinder:stage + container_name: nasa-server + ports: + - "5000:5000" \ No newline at end of file diff --git a/src/dashboard-tui/Nasa.Dashboard.Clients/Internal/Pathfinder/PathfinderClient.cs b/src/dashboard-tui/Nasa.Dashboard.Clients/Internal/Pathfinder/PathfinderClient.cs index 0494fd0..7b0ee8e 100644 --- a/src/dashboard-tui/Nasa.Dashboard.Clients/Internal/Pathfinder/PathfinderClient.cs +++ b/src/dashboard-tui/Nasa.Dashboard.Clients/Internal/Pathfinder/PathfinderClient.cs @@ -24,7 +24,7 @@ public async Task PingAsync() public async Task> GetBotsAsync() { - var response = await grpcService.GetBotsAsync(new Empty(), GetHeader()); + var response = await grpcService.GetBotsAsync(new GetBotsRequest{MapId = "1"}, GetHeader()); return response.Bots.Select(bot => new Bot { diff --git a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotConsumer.cs b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotConsumer.cs index b4bff92..a375b8f 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotConsumer.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotConsumer.cs @@ -12,24 +12,25 @@ internal class BotConsumer( { public async Task Consume(MoveCommand command, CancellationToken ct = default) { - var project = await worldMap.TryReachPosition(command.Bot.MapId, command.DesiredPosition, ct); + var project = await worldMap.ReachPosition(command.Bot.MapId, command.Bot.Position, command.OperatorCommands , ct); if (project is PositionNotChanged) { - var stand = new StandCommand(command.ClientId, command.Bot.Id, command.Bot.Position, command.CorrelationId); + var stand = new StandCommand(command.ClientId, command.Bot.Id, project.Position, + command.CorrelationId); processor.Publish(stand); return; } - if (project is PositionOutOfMap) + if (project is PositionOutOfMap outOfMap) { - var lost = new DeadCommand(command.ClientId, command.Bot.Id, command.DesiredPosition, command.Bot.MapId, - command.Bot.LastWords, command.CorrelationId); + var lost = new DeadCommand(command.ClientId, command.Bot.Id, command.Bot.MapId, + outOfMap.Previous, command.CorrelationId); processor.Publish(lost); return; } - var walk = new WalkCommand(command.ClientId, command.Bot.Id, command.DesiredPosition, + var walk = new WalkCommand(command.ClientId, command.Bot.Id, project.Position, command.CorrelationId); processor.Publish(walk); } diff --git a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotDeadWalkerConsumer.cs b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotDeadWalkerConsumer.cs index 65dca39..68ddec1 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotDeadWalkerConsumer.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotDeadWalkerConsumer.cs @@ -18,19 +18,19 @@ internal class BotDeadWalkerConsumer( public async Task Consume(DeadCommand command, CancellationToken ct = default) { await botRepository.ChangeBotStatusAsync(command.BotId, BotStatus.Dead, ct); - await botRepository.ChangeBotPositionAsync(command.BotId, command.DesiredPosition, ct); + await botRepository.ChangeBotPositionAsync(command.BotId, command.CurrentPosition, ct); var funeral = new Funeral { Id = Guid.CreateVersion7().ToString(), ETag = Guid.NewGuid(), - Value = command.DesiredPosition, + Value = command.CurrentPosition, MapId = command.MapId, }; await funeralRepository.AddNewFuneral(funeral, ct); - var notificationText = messageDecoder.EncodeBotMessage(command.DesiredPosition, true); + var notificationText = messageDecoder.EncodeBotMessage(command.CurrentPosition, true); var request = new SendMessageRequest(command.ClientId, command.BotId, notificationText, true, - false, command.LastWords); + false); processor.SendMessage(request); } } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotInvalidCommandConsumer.cs b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotInvalidCommandConsumer.cs index 34fe692..244fc16 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotInvalidCommandConsumer.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotInvalidCommandConsumer.cs @@ -10,7 +10,7 @@ internal class BotInvalidCommandConsumer(IOperatorProcessor processor) : IBotCon public Task Consume(InvalidCommand command, CancellationToken ct = default) { var request = new SendMessageRequest(command.ClientId, command.BotId, command.Message, true, - true, string.Empty); + true); processor.SendMessage(request); diff --git a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotStandConsumer.cs b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotStandConsumer.cs index 56b673e..6284bfc 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotStandConsumer.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotStandConsumer.cs @@ -14,7 +14,7 @@ public async Task Consume(StandCommand command, CancellationToken ct = default) { var notificationText = messageDecoder.EncodeBotMessage(command.CurrentPosition, false); var request = new SendMessageRequest(command.ClientId, command.BotId, notificationText, false, - false, string.Empty); + false); processor.SendMessage(request); } } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotWalkerConsumer.cs b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotWalkerConsumer.cs index d21668c..a7b45ff 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotWalkerConsumer.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotWalkerConsumer.cs @@ -14,10 +14,10 @@ internal class BotWalkerConsumer( { public async Task Consume(WalkCommand command, CancellationToken ct = default) { - await repository.ChangeBotPositionAsync(command.BotId, command.DesiredPosition, ct); - var notificationText = messageDecoder.EncodeBotMessage(command.DesiredPosition, false); + await repository.ChangeBotPositionAsync(command.BotId, command.CurrentPosition, ct); + var notificationText = messageDecoder.EncodeBotMessage(command.CurrentPosition, false); var request = new SendMessageRequest(command.ClientId, command.BotId, notificationText, false, - false, string.Empty); + false); processor.SendMessage(request); } } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Data.Contracts/Repositories/IBotRepository.cs b/src/nasa-server/src/Nasa.Pathfinder.Data.Contracts/Repositories/IBotRepository.cs index 5ca7615..5240fd6 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Data.Contracts/Repositories/IBotRepository.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Data.Contracts/Repositories/IBotRepository.cs @@ -7,8 +7,9 @@ namespace Nasa.Pathfinder.Data.Contracts.Repositories; public interface IBotRepository { + Task AddRangeAsync(IEnumerable bots, CancellationToken ct = default); Task TryGetAsync(string botId, CancellationToken ct = default); - Task> GetBotsAsync(CancellationToken ct = default); + Task> GetBotsAsync(string mapId, CancellationToken ct = default); Task ChangeBotStatusAsync(string botId, BotStatus status, CancellationToken ct = default); Task ChangeBotPositionAsync(string botId, Position position, CancellationToken ct = default); } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Data.Contracts/Repositories/IMapRepository.cs b/src/nasa-server/src/Nasa.Pathfinder.Data.Contracts/Repositories/IMapRepository.cs index dcc3eb8..95170cc 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Data.Contracts/Repositories/IMapRepository.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Data.Contracts/Repositories/IMapRepository.cs @@ -5,4 +5,5 @@ namespace Nasa.Pathfinder.Data.Contracts.Repositories; public interface IMapRepository { Task TryGetAsync(string id, CancellationToken ct = default); + Task AddAsync(MapInfo map, CancellationToken ct = default); } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Data/Internal/BotRepository.cs b/src/nasa-server/src/Nasa.Pathfinder.Data/Internal/BotRepository.cs index 5bc6a7a..462964f 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Data/Internal/BotRepository.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Data/Internal/BotRepository.cs @@ -10,15 +10,33 @@ namespace Nasa.Pathfinder.Data.Internal; internal class BotRepository(IDataContext dataContext) : IBotRepository { + public async Task AddRangeAsync(IEnumerable bots, CancellationToken ct = default) + { + await dataContext.AcquireAsync("all", ct); + try + { + foreach (var bot in bots) + { + bot.ETag = Guid.NewGuid(); + } + + await dataContext.PushManyAsync(bots, ct); + } + finally + { + await dataContext.ReleaseAsync("all", CancellationToken.None); + } + } + public async Task TryGetAsync(string botId, CancellationToken ct = default) { return await dataContext.GetAsync(botId, ct); } - public async Task> GetBotsAsync(CancellationToken ct = default) + public async Task> GetBotsAsync(string mapId, CancellationToken ct = default) { var bots = await dataContext.GetAllAsync(ct); - return bots; + return bots.Where(x => x.MapId == mapId); } public async Task ChangeBotStatusAsync(string botId, BotStatus status, CancellationToken ct = default) diff --git a/src/nasa-server/src/Nasa.Pathfinder.Data/Internal/MapRepository.cs b/src/nasa-server/src/Nasa.Pathfinder.Data/Internal/MapRepository.cs index a750ffe..fef2c9b 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Data/Internal/MapRepository.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Data/Internal/MapRepository.cs @@ -10,4 +10,13 @@ internal class MapRepository(IDataContext context) : IMapRepository { return await context.GetAsync(id, ct); } + + public async Task AddAsync(MapInfo map, CancellationToken ct = default) + { + map.Id = Guid.NewGuid().ToString(); + map.ETag = Guid.NewGuid(); + + await context.PushAsync(map, ct); + return map; + } } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Domain/Entities/Bots/Bot.cs b/src/nasa-server/src/Nasa.Pathfinder.Domain/Entities/Bots/Bot.cs index ed0e159..ba1d847 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Domain/Entities/Bots/Bot.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Domain/Entities/Bots/Bot.cs @@ -12,5 +12,4 @@ public class Bot : IEntity public BotStatus Status { get; set; } public Position Position { get; set; } public string MapId { get; set; } - public string LastWords { get; set; } } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Domain/Interactions/BotCommands.cs b/src/nasa-server/src/Nasa.Pathfinder.Domain/Interactions/BotCommands.cs index e6d0827..97ffd9c 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Domain/Interactions/BotCommands.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Domain/Interactions/BotCommands.cs @@ -1,4 +1,3 @@ -using Nasa.Pathfinder.Domain.Bots; using Nasa.Pathfinder.Domain.Entities.Bots; using Nasa.Pathfinder.Domain.World; @@ -12,13 +11,13 @@ public interface IBotCommand public record MoveCommand( string ClientId, Bot Bot, - Position DesiredPosition, + Stack OperatorCommands, Guid CorrelationId = default) : IBotCommand; public record WalkCommand( string ClientId, string BotId, - Position DesiredPosition, + Position CurrentPosition, Guid CorrelationId = default) : IBotCommand; public record StandCommand( @@ -30,9 +29,8 @@ public record StandCommand( public record DeadCommand( string ClientId, string BotId, - Position DesiredPosition, string MapId, - string LastWords, + Position CurrentPosition, Guid CorrelationId = default) : IBotCommand; public record InvalidCommand( diff --git a/src/nasa-server/src/Nasa.Pathfinder.Domain/World/PositionProject.cs b/src/nasa-server/src/Nasa.Pathfinder.Domain/World/PositionProject.cs index 24d35ff..37c4e18 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Domain/World/PositionProject.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Domain/World/PositionProject.cs @@ -1,26 +1,28 @@ namespace Nasa.Pathfinder.Domain.World; public interface IPositionProject -{ } +{ + public Position Position { get; } +} -public record PositionChanged : IPositionProject; -public record PositionNotChanged : IPositionProject; -public record PositionOutOfMap : IPositionProject; +public record PositionChanged(Position Position) : IPositionProject; +public record PositionNotChanged(Position Position) : IPositionProject; +public record PositionOutOfMap(Position Position, Position Previous) : IPositionProject; public static class PositionProject { - public static IPositionProject Changed() + public static IPositionProject Changed(Position position) { - return new PositionChanged(); + return new PositionChanged(position); } - public static IPositionProject NotChanged() + public static IPositionProject NotChanged(Position position) { - return new PositionNotChanged(); + return new PositionNotChanged(position); } - public static IPositionProject OutOfMap() + public static IPositionProject OutOfMap(Position position, Position previous) { - return new PositionOutOfMap(); + return new PositionOutOfMap(position, previous); } } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Facades.Contracts/IBotFacade.cs b/src/nasa-server/src/Nasa.Pathfinder.Facades.Contracts/IBotFacade.cs index 6d7fb96..29d511d 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Facades.Contracts/IBotFacade.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Facades.Contracts/IBotFacade.cs @@ -1,11 +1,12 @@ -using Nasa.Pathfinder.Domain.Bots; +using System.Numerics; using Nasa.Pathfinder.Domain.Entities.Bots; namespace Nasa.Pathfinder.Facades.Contracts; public interface IBotFacade { - Task> GetBotsAsync(CancellationToken ct = default); + Task CreateWorldAsync(Vector2 coordinates, IEnumerable bots, CancellationToken ct = default); + Task> GetBotsAsync(string mapId, CancellationToken ct = default); Task SelectBotAsync(string botId, CancellationToken ct = default); Task ResetBotAsync(string botId, CancellationToken ct = default); } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Facades/Internal/BotFacade.cs b/src/nasa-server/src/Nasa.Pathfinder.Facades/Internal/BotFacade.cs index e228411..b3e019c 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Facades/Internal/BotFacade.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Facades/Internal/BotFacade.cs @@ -1,19 +1,42 @@ +using System.Numerics; using Nasa.Pathfinder.Data.Contracts.Exceptions; using Nasa.Pathfinder.Data.Contracts.Repositories; using Nasa.Pathfinder.Domain.Bots; using Nasa.Pathfinder.Domain.Entities.Bots; +using Nasa.Pathfinder.Domain.Entities.World; using Nasa.Pathfinder.Facades.Contracts; using Nasa.Pathfinder.Facades.Contracts.Exceptions; namespace Nasa.Pathfinder.Facades.Internal; -internal class BotFacade(IBotRepository repository) : IBotFacade +internal class BotFacade( + IBotRepository repository, + IMapRepository mapRepository) : IBotFacade { - public async Task> GetBotsAsync(CancellationToken ct = default) + public async Task CreateWorldAsync(Vector2 coordinates, IEnumerable bots, CancellationToken ct = default) + { + var map = new MapInfo + { + SizeX = (int)coordinates.X, + SizeY = (int)coordinates.Y, + }; + var createdMap = await mapRepository.AddAsync(map, ct); + + foreach (var bot in bots) + { + bot.MapId = createdMap.Id; + } + + await repository.AddRangeAsync(bots.ToList(), ct); + + return createdMap.Id; + } + + public async Task> GetBotsAsync(string mapId, CancellationToken ct = default) { try { - var bots = await repository.GetBotsAsync(ct); + var bots = await repository.GetBotsAsync(mapId, ct); return bots; } catch (Exception ex) when (ex is OperationCanceledException) diff --git a/src/nasa-server/src/Nasa.Pathfinder.Facades/Internal/MessageFacade.cs b/src/nasa-server/src/Nasa.Pathfinder.Facades/Internal/MessageFacade.cs index aff7774..d0b2f28 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Facades/Internal/MessageFacade.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Facades/Internal/MessageFacade.cs @@ -11,7 +11,6 @@ namespace Nasa.Pathfinder.Facades.Internal; internal class MessageFacade( IBotRepository repository, IMessageDecoderService messageDecoder, - IWorldMapService worldMap, IBotProcessor processor) : IMessageFacade { public async Task ReceiveMessageAsync(IMessage message, CancellationToken ct = default) @@ -37,9 +36,7 @@ public async Task ReceiveMessageAsync(IMessage message, CancellationToken ct = d } var bot = await repository.TryGetAsync(operatorMessage.BotId, ct); - - var desiredPosition = worldMap.CalculateDesiredPosition(bot.Position, commands); - - processor.Publish(new MoveCommand(operatorMessage.ClientId, bot, desiredPosition, correlationId)); + + processor.Publish(new MoveCommand(operatorMessage.ClientId, bot, commands, correlationId)); } } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Infrastructure.Contracts/DataContexts/IDataContext.cs b/src/nasa-server/src/Nasa.Pathfinder.Infrastructure.Contracts/DataContexts/IDataContext.cs index f2e5ec7..d874bea 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Infrastructure.Contracts/DataContexts/IDataContext.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Infrastructure.Contracts/DataContexts/IDataContext.cs @@ -6,6 +6,7 @@ namespace Nasa.Pathfinder.Infrastructure.Contracts.DataContexts; public interface IDataContext { Task PushAsync(T entry, CancellationToken ct = default) where T : class, IEntity; + Task PushManyAsync(IEnumerable entry, CancellationToken ct = default) where T : class, IEntity; Task> UpdateAsync(T entry, CancellationToken ct = default) where T : class, IEntity; Task GetAsync(string id, CancellationToken ct = default) where T : class, IEntity; Task> GetAllAsync(CancellationToken ct = default); diff --git a/src/nasa-server/src/Nasa.Pathfinder.Infrastructure.Contracts/Grpc/Requests/SendMessageRequest.cs b/src/nasa-server/src/Nasa.Pathfinder.Infrastructure.Contracts/Grpc/Requests/SendMessageRequest.cs index 6138cf8..8ddbea1 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Infrastructure.Contracts/Grpc/Requests/SendMessageRequest.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Infrastructure.Contracts/Grpc/Requests/SendMessageRequest.cs @@ -5,5 +5,4 @@ public record SendMessageRequest( string BotId, string Message, bool IsLost, - bool IsInvalidCommand, - string LastWords); \ No newline at end of file + bool IsInvalidCommand); \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Infrastructure/Internal/Memory/MemoryDataContext.cs b/src/nasa-server/src/Nasa.Pathfinder.Infrastructure/Internal/Memory/MemoryDataContext.cs index 86719b8..be6fce5 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Infrastructure/Internal/Memory/MemoryDataContext.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Infrastructure/Internal/Memory/MemoryDataContext.cs @@ -29,6 +29,21 @@ public Task PushAsync(T entry, CancellationToken ct = default) where T : clas return Task.CompletedTask; } + + public Task PushManyAsync(IEnumerable entry, CancellationToken ct = default) where T : class, IEntity + { + if (ct.IsCancellationRequested) + return Task.CompletedTask; + + foreach (var entity in entry) + { + var id = GetInternalId(entity.Id); + cache.Set(id, entity); + _types.Add(id, entity.GetType()); + } + + return Task.CompletedTask; + } public Task> UpdateAsync(T entry, CancellationToken ct = default) where T : class, IEntity { @@ -40,14 +55,20 @@ public Task> UpdateAsync(T entry, CancellationToken ct = default) } } - public Task GetAsync(string id, CancellationToken ct = default) where T : class, IEntity + public async Task GetAsync(string id, CancellationToken ct = default) where T : class, IEntity { + await Task.CompletedTask; + var lockObj = GetEntityLock(id); lock (lockObj) { var result = Get(id); - if (result.IsError) return null; - return Task.FromResult(result.Value); + if (result.IsError) + return null; + + var entity = result.Value; + + return entity; } } diff --git a/src/nasa-server/src/Nasa.Pathfinder.Services.Contracts/IWorldMapService.cs b/src/nasa-server/src/Nasa.Pathfinder.Services.Contracts/IWorldMapService.cs index b383288..58e767c 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Services.Contracts/IWorldMapService.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Services.Contracts/IWorldMapService.cs @@ -5,7 +5,6 @@ namespace Nasa.Pathfinder.Services.Contracts; public interface IWorldMapService { - Position CalculateDesiredPosition(Position currentPosition, Stack commands); - Task TryReachPosition(string mapId, Position position, - CancellationToken ct = default); + Task ReachPosition(string mapId, Position currentPosition, + Stack commands, CancellationToken ct = default); } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs b/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs index 84d3b65..f7912aa 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs @@ -10,85 +10,141 @@ internal class WorldMapService( IMapRepository mapRepository, IFuneralRepository funeralRepository) : IWorldMapService { - public Position CalculateDesiredPosition(Position currentPosition, Stack commands) + public async Task ReachPosition(string mapId, Position currentPosition, + Stack commands, CancellationToken ct = default) { - foreach (var command in commands) currentPosition = Move(currentPosition, command); + var start = new Position + { + X = currentPosition.X, + Y = currentPosition.Y, + Direction = Direction.N + }; + + try + { + var funerals = await funeralRepository.GetFuneralsAsync(mapId, ct); - return currentPosition; + IPositionProject position = new PositionChanged(currentPosition); - static Position Move(Position position, IOperatorCommand command) - { - if (command is MoveRight or MoveLeft) position.Direction = Rotate(command, position.Direction); + foreach (var command in commands) + { + Position current; + switch (command) + { + case MoveRight or MoveLeft: + current = position.Position; + + current.Direction = Rotate(command, current.Direction); + position = new PositionChanged(current); + break; + case MoveFront moveFrontCommand: + current = position.Position; + + var newPosition = await CalculateMoveFront(current, moveFrontCommand, + funerals, mapId, ct); + + if (newPosition is PositionNotChanged) + { + continue; + } - switch (position.Direction) + if (newPosition is PositionOutOfMap @out) + { + return @out; + } + + position = newPosition; + break; + default: + continue; + } + } + + if (position.Position.X == start.X && position.Position.Y == start.Y && + position.Position.Direction == start.Direction) { - case Direction.N: - position.Y += command.Steps; - break; - case Direction.E: - position.X += command.Steps; - break; - case Direction.S: - position.Y -= command.Steps; - break; - case Direction.W: - position.X -= command.Steps; - break; - default: - throw new ArgumentOutOfRangeException(); + return new PositionNotChanged(currentPosition); } return position; } - - static Direction Rotate(IOperatorCommand command, Direction direction) + catch (Exception e) { - return command switch - { - MoveLeft _ => direction switch - { - Direction.N => Direction.W, - Direction.W => Direction.S, - Direction.S => Direction.E, - Direction.E => Direction.N, - _ => throw new ArgumentOutOfRangeException() - }, - MoveRight _ => direction switch - { - Direction.N => Direction.E, - Direction.E => Direction.S, - Direction.S => Direction.W, - Direction.W => Direction.N, - _ => throw new ArgumentOutOfRangeException() - } - }; + return new PositionNotChanged(start); } } - public async Task TryReachPosition(string mapId, Position position, - CancellationToken ct = default) + private async Task CalculateMoveFront(Position position, MoveFront command, + IReadOnlyCollection funerals, string mapId, CancellationToken ct = default) { - var funerals = await funeralRepository.GetFuneralsAsync(mapId, ct); - var isTrap = funerals.Any(x => x.Value == position); + var previous = new Position + { + X = position.X, + Y = position.Y, + Direction = position.Direction + }; + + var isTrap = funerals.Any(x => x.Value.X == position.X + && x.Value.Y == position.Y + && x.Value.Direction == position.Direction); + if (isTrap) + { + return PositionProject.NotChanged(position); + } + + switch (position.Direction) + { + case Direction.N: + position.Y += command.Steps; + break; + case Direction.E: + position.X += command.Steps; + break; + case Direction.S: + position.Y -= command.Steps; + break; + case Direction.W: + position.X -= command.Steps; + break; + default: + throw new ArgumentOutOfRangeException(); + } var isOutOfMap = false; var mapInfo = await mapRepository.TryGetAsync(mapId, ct); - if (position.X > mapInfo.SizeX || position.Y > mapInfo.SizeY) { isOutOfMap = true; } - - if (isTrap && !isOutOfMap) - { - return PositionProject.NotChanged(); - } if (isOutOfMap) { - return PositionProject.OutOfMap(); + return PositionProject.OutOfMap(position, previous); } - return PositionProject.Changed(); + return PositionProject.Changed(position); + } + + private static Direction Rotate(IOperatorCommand command, Direction direction) + { + return command switch + { + MoveLeft _ => direction switch + { + Direction.N => Direction.W, + Direction.W => Direction.S, + Direction.S => Direction.E, + Direction.E => Direction.N, + _ => throw new ArgumentOutOfRangeException() + }, + MoveRight _ => direction switch + { + Direction.N => Direction.E, + Direction.E => Direction.S, + Direction.S => Direction.W, + Direction.W => Direction.N, + _ => throw new ArgumentOutOfRangeException() + } + }; } } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder/Background/MigrationBackgroundTask.cs b/src/nasa-server/src/Nasa.Pathfinder/Background/MigrationBackgroundTask.cs index cd2e128..4b946dc 100644 --- a/src/nasa-server/src/Nasa.Pathfinder/Background/MigrationBackgroundTask.cs +++ b/src/nasa-server/src/Nasa.Pathfinder/Background/MigrationBackgroundTask.cs @@ -1,3 +1,4 @@ +using Nasa.Pathfinder.Data.Contracts.Repositories; using Nasa.Pathfinder.Domain.Bots; using Nasa.Pathfinder.Domain.Entities.Bots; using Nasa.Pathfinder.Domain.Entities.World; @@ -8,68 +9,67 @@ namespace Nasa.Pathfinder.Background; public class MigrationBackgroundTask( + IBotRepository botRepository, IMemoryDataContext context) : IHostedService { public async Task StartAsync(CancellationToken cancellationToken) { - var mapId = Guid.CreateVersion7().ToString(); + var mapId = "1"; var mapInfo = new MapInfo { Id = mapId, ETag = Guid.NewGuid(), - SizeX = 50, - SizeY = 50, + SizeX = 5, + SizeY = 3, }; await context.PushAsync(mapInfo, cancellationToken); - await context.PushAsync(new Bot + var bots = new List(); + bots.Add(new Bot { Id = Guid.NewGuid().ToString(), ETag = Guid.NewGuid(), Name = "bot-1", Status = BotStatus.Available, MapId = mapId, - LastWords = "beep-bep-bep-beee.", Position = new Position { X = 1, Y = 1, - Direction = Direction.N + Direction = Direction.E } - }, cancellationToken); - - await context.PushAsync(new Bot + }); + bots.Add(new Bot { Id = Guid.NewGuid().ToString(), ETag = Guid.NewGuid(), Name = "bot-2", Status = BotStatus.Available, MapId = mapId, - LastWords = "There is only the Emperor, and he is our shield and protector.", Position = new Position { - X = 10, - Y = 6, + X = 3, + Y = 2, Direction = Direction.N } - }, cancellationToken); - - await context.PushAsync(new Bot + }); + bots.Add(new Bot { Id = Guid.NewGuid().ToString(), ETag = Guid.NewGuid(), Name = "bot-3", Status = BotStatus.Available, MapId = mapId, - LastWords = "Blessed is the mind too small for doubt.", Position = new Position { - X = 5, - Y = 5, - Direction = Direction.N + X = 0, + Y = 3, + Direction = Direction.W } - }, cancellationToken); + }); + + await botRepository.AddRangeAsync(bots, cancellationToken); } public Task StopAsync(CancellationToken cancellationToken) diff --git a/src/nasa-server/src/Nasa.Pathfinder/Hubs/MessageHub.cs b/src/nasa-server/src/Nasa.Pathfinder/Hubs/MessageHub.cs index dc7ef70..aa19501 100644 --- a/src/nasa-server/src/Nasa.Pathfinder/Hubs/MessageHub.cs +++ b/src/nasa-server/src/Nasa.Pathfinder/Hubs/MessageHub.cs @@ -48,11 +48,12 @@ private Task StartConsumer(ConcurrentQueue queue, Cancellati { if (queue.IsEmpty) { - await Task.Delay(20, token); + await Task.Delay(5, token); continue; } - if (queue.TryDequeue(out var message)) await NotifyClient(message); + if (queue.TryDequeue(out var message)) + await NotifyClient(message); } }, token); } @@ -66,8 +67,7 @@ private async Task NotifyClient(SendMessageRequest message) BotId = message.BotId, Message = message.Message, IsLost = message.IsLost, - IsInvalidCommand = message.IsInvalidCommand, - LastWords = message.LastWords + IsInvalidCommand = message.IsInvalidCommand }; await stream.WriteAsync(reply); diff --git a/src/nasa-server/src/Nasa.Pathfinder/Services/PathfinderGrpcService.cs b/src/nasa-server/src/Nasa.Pathfinder/Services/PathfinderGrpcService.cs index 6aeb9bb..51385f1 100644 --- a/src/nasa-server/src/Nasa.Pathfinder/Services/PathfinderGrpcService.cs +++ b/src/nasa-server/src/Nasa.Pathfinder/Services/PathfinderGrpcService.cs @@ -1,3 +1,4 @@ +using System.Numerics; using Google.Protobuf.WellKnownTypes; using Grpc.Core; using Nasa.Pathfinder.Domain.Messages; @@ -25,9 +26,17 @@ public override async Task Ping(Empty request, ServerCallContext c }; } - public override async Task GetBots(Empty request, ServerCallContext context) + public override async Task CreateWorld(CreateWorldRequest request, ServerCallContext context) { - var result = await botFacade.GetBotsAsync(context.CancellationToken); + var vector = new Vector2(request.SizeX, request.SizeY); + var bots = Request.MapFromCreateWorldRequest(request); + var result = await botFacade.CreateWorldAsync(vector, bots, context.CancellationToken); + return Response.MapToCreateWorldResponse(result); + } + + public override async Task GetBots(GetBotsRequest request, ServerCallContext context) + { + var result = await botFacade.GetBotsAsync(request.MapId, context.CancellationToken); var response = Response.MapToGetBotsResponse(result); return response; diff --git a/src/nasa-server/src/Nasa.Pathfinder/Types/Request.cs b/src/nasa-server/src/Nasa.Pathfinder/Types/Request.cs new file mode 100644 index 0000000..abc33ad --- /dev/null +++ b/src/nasa-server/src/Nasa.Pathfinder/Types/Request.cs @@ -0,0 +1,28 @@ +using Nasa.Pathfinder.Domain.Bots; +using Nasa.Pathfinder.Domain.World; +using Pathfinder.Messages; +using Bot = Nasa.Pathfinder.Domain.Entities.Bots.Bot; +using Position = Nasa.Pathfinder.Domain.World.Position; + + +namespace Nasa.Pathfinder.Types; + +public static class Request +{ + public static IEnumerable MapFromCreateWorldRequest(CreateWorldRequest request) + { + return request.Bots.Select(bot => new Bot + { + Id = Guid.NewGuid().ToString(), + ETag = Guid.NewGuid(), + Name = bot.Name, + Status = BotStatus.Available, + Position = new Position + { + X = bot.Position.X, + Y = bot.Position.Y, + Direction = Enum.Parse(bot.Position.Direction), + } + }).ToList(); + } +} \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder/Types/Response.cs b/src/nasa-server/src/Nasa.Pathfinder/Types/Response.cs index 055fa0e..89b7ac5 100644 --- a/src/nasa-server/src/Nasa.Pathfinder/Types/Response.cs +++ b/src/nasa-server/src/Nasa.Pathfinder/Types/Response.cs @@ -68,4 +68,12 @@ public static ResetBotResponse MapToResetBotResponse(Domain.Entities.Bots.Bot bo }; return response; } + + public static CreateWorldResponse MapToCreateWorldResponse(string mapId) + { + return new CreateWorldResponse + { + MapId = mapId + }; + } } \ No newline at end of file diff --git a/src/nasa-server/tests/Nasa.Pathfinder.Facades.Tests/BotFacadeTests.cs b/src/nasa-server/tests/Nasa.Pathfinder.Facades.Tests/BotFacadeTests.cs index b91baf4..0eb9cc7 100644 --- a/src/nasa-server/tests/Nasa.Pathfinder.Facades.Tests/BotFacadeTests.cs +++ b/src/nasa-server/tests/Nasa.Pathfinder.Facades.Tests/BotFacadeTests.cs @@ -20,6 +20,7 @@ public void Setup() } private Mock _repositoryMock; + private Mock _mapRepositoryMock; [Test] public async Task GetBotsAsync_RepoReturnsNothing_ShouldReturnEmptyList() @@ -27,11 +28,11 @@ public async Task GetBotsAsync_RepoReturnsNothing_ShouldReturnEmptyList() await TestRunner> .Arrange(() => { - _repositoryMock.Setup(x => x.GetBotsAsync(It.IsAny())) + _repositoryMock.Setup(x => x.GetBotsAsync( It.IsAny(), It.IsAny())) .ReturnsAsync([]); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) - .ActAsync(sut => sut.GetBotsAsync(CancellationToken.None)) + .ActAsync(sut => sut.GetBotsAsync(It.IsAny(), CancellationToken.None)) .ThenAssertAsync(result => result.Should().NotBeNull()); } @@ -41,11 +42,11 @@ public async Task GetBotsAsync_RequestAborted_ShouldReturnEmptyList() await TestRunner> .Arrange(() => { - _repositoryMock.Setup(x => x.GetBotsAsync(It.IsAny())) + _repositoryMock.Setup(x => x.GetBotsAsync(It.IsAny(),It.IsAny())) .Callback(() => throw new OperationCanceledException()); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) - .ActAsync(sut => sut.GetBotsAsync(CancellationToken.None)) + .ActAsync(sut => sut.GetBotsAsync(It.IsAny(), CancellationToken.None)) .ThenAssertAsync(result => result.Should().BeEmpty()); } @@ -58,7 +59,7 @@ await TestRunner _repositoryMock.Setup(x => x.ChangeBotStatusAsync(It.IsAny(), BotStatus.Acquired, It.IsAny())) .ReturnsAsync(new Bot()); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) .ActAsync(sut => sut.SelectBotAsync(Guid.NewGuid().ToString(), CancellationToken.None)) .ThenAssertAsync(result => result.Should().NotBeNull()); @@ -73,7 +74,7 @@ await TestRunner _repositoryMock.Setup(x => x.ChangeBotStatusAsync(It.IsAny(), BotStatus.Acquired, It.IsAny())) .Callback(() => throw new ConcurrencyException(string.Empty)); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) .ActAsync(sut => sut.SelectBotAsync(Guid.NewGuid().ToString(), CancellationToken.None)) .ThenAssertThrowsAsync(); @@ -88,7 +89,7 @@ await TestRunner _repositoryMock.Setup(x => x.ChangeBotStatusAsync(It.IsAny(), BotStatus.Acquired, It.IsAny())) .Callback(() => throw new DataException(string.Empty)); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) .ActAsync(sut => sut.SelectBotAsync(Guid.NewGuid().ToString(), CancellationToken.None)) .ThenAssertThrowsAsync(); @@ -103,7 +104,7 @@ await TestRunner _repositoryMock.Setup(x => x.ChangeBotStatusAsync(It.IsAny(), BotStatus.Acquired, It.IsAny())) .Callback(() => throw new OperationCanceledException()); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) .ActAsync(sut => sut.SelectBotAsync(It.IsAny(), It.IsAny())) .ThenAssertAsync(result => result.Should().NotBeNull()); @@ -118,7 +119,7 @@ await TestRunner _repositoryMock.Setup(x => x.ChangeBotStatusAsync(It.IsAny(), BotStatus.Available, It.IsAny())) .ReturnsAsync(new Bot()); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) .ActAsync(sut => sut.ResetBotAsync(Guid.NewGuid().ToString(), CancellationToken.None)) .ThenAssertAsync(result => result.Should().NotBeNull()); @@ -133,7 +134,7 @@ await TestRunner _repositoryMock.Setup(x => x.ChangeBotStatusAsync(It.IsAny(), BotStatus.Available, It.IsAny())) .Callback(() => throw new ConcurrencyException(string.Empty)); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) .ActAsync(sut => sut.ResetBotAsync(Guid.NewGuid().ToString(), CancellationToken.None)) .ThenAssertThrowsAsync(); @@ -148,7 +149,7 @@ await TestRunner _repositoryMock.Setup(x => x.ChangeBotStatusAsync(It.IsAny(), BotStatus.Available, It.IsAny())) .Callback(() => throw new DataException(string.Empty)); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) .ActAsync(sut => sut.ResetBotAsync(Guid.NewGuid().ToString(), CancellationToken.None)) .ThenAssertThrowsAsync(); @@ -163,7 +164,7 @@ await TestRunner _repositoryMock.Setup(x => x.ChangeBotStatusAsync(It.IsAny(), BotStatus.Available, It.IsAny())) .Callback(() => throw new OperationCanceledException()); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) .ActAsync(sut => sut.ResetBotAsync(It.IsAny(), It.IsAny())) .ThenAssertAsync(result => result.Should().NotBeNull()); diff --git a/src/nasa-server/tests/Nasa.Pathfinder.Facades.Tests/MessageFacadeTests.cs b/src/nasa-server/tests/Nasa.Pathfinder.Facades.Tests/MessageFacadeTests.cs index 1ac99bf..c17538d 100644 --- a/src/nasa-server/tests/Nasa.Pathfinder.Facades.Tests/MessageFacadeTests.cs +++ b/src/nasa-server/tests/Nasa.Pathfinder.Facades.Tests/MessageFacadeTests.cs @@ -34,8 +34,7 @@ public void Setup() public async Task ReceiveMessageAsync_UnsupportedMessage_ShouldThrowsException() { await TestRunner - .Arrange(() => new MessageFacade(_repositoryMock.Object, _decoderServiceMock.Object, - _worldMapServiceMock.Object, _processorServiceMock.Object)) + .Arrange(() => new MessageFacade(_repositoryMock.Object, _decoderServiceMock.Object, _processorServiceMock.Object)) .ActAsync(sut => sut.ReceiveMessageAsync(new BotMessage())) .ThenAssertThrowsAsync(); } @@ -75,17 +74,7 @@ await TestRunner } }); - _worldMapServiceMock.Setup(x => x.CalculateDesiredPosition(It.IsAny(), - It.IsAny>())) - .Returns(new Position - { - X = 4, - Y = 3, - Direction = Direction.N - }); - - return new MessageFacade(_repositoryMock.Object, _decoderServiceMock.Object, - _worldMapServiceMock.Object, _processorServiceMock.Object); + return new MessageFacade(_repositoryMock.Object, _decoderServiceMock.Object, _processorServiceMock.Object); }) .ActAsync(sut => sut.ReceiveMessageAsync(new OperatorMessage { @@ -100,156 +89,4 @@ await TestRunner Times.Once); }); } - - // [Test] - // public async Task ReceiveMessageAsync_HasFuneral_ShouldStayAtPlace() - // { - // const string input = "FRFFL"; - // var botId = new Faker().Random.Hash(); - // var clientId = new Faker().Random.Hash(); - // - // var desiredPosition = new Position - // { - // X = 4, - // Y = 3, - // Direction = Direction.N - // }; - // - // await TestRunner - // .Arrange(() => - // { - // _decoderServiceMock.Setup(x => x.DecodeOperatorMessage( - // It.Is(i => i.Equals(input)))) - // .Returns(new List - // { - // new MoveFront(), - // new MoveRight(), - // new MoveFront(), - // new MoveFront(), - // new MoveLeft() - // }); - // - // _repositoryMock.Setup(x => x.GetAsync(It.IsAny(), It.IsAny())) - // .ReturnsAsync(new Bot - // { - // Id = botId, - // Name = new Faker().Hacker.Noun(), - // Status = BotStatus.Acquired, - // Position = new Position - // { - // X = 1, - // Y = 1, - // Direction = Direction.N - // } - // }); - // - // _worldMapServiceMock.Setup(x => x.CalculateDesiredPosition(It.IsAny(), - // It.IsAny>())) - // .Returns(desiredPosition); - // - // _worldMapServiceMock.Setup(x => x.GetFuneralsAsync(It.IsAny())) - // .ReturnsAsync([desiredPosition]); - // - // _worldMapServiceMock.Setup(x => - // x.TryReachPosition(It.IsAny(), It.IsAny())) - // .ReturnsAsync(true); - // - // _decoderServiceMock.Setup(x => - // x.EncodeBotMessage( - // It.IsAny(), - // It.Is(b => b == false), - // It.Is(b => b == false))) - // .Returns("4 3 N"); - // - // return new MessageFacade(_repositoryMock.Object, _decoderServiceMock.Object, - // _worldMapServiceMock.Object); - // }) - // .ActAsync(sut => sut.ReceiveMessageAsync(new OperatorMessage - // { - // BotId = botId, - // ClientId = clientId, - // Text = input - // })) - // .ThenAssertAsync(() => - // { - // _worldMapServiceMock.Verify(x => - // x.ChangeBotPositionAsync(It.IsAny(), It.IsAny()), - // Times.Never); - // - // _streamMock.Verify(x => x.SendMessage(It.IsAny()), Times.Once); - // }); - // } - // - // [Test] - // public async Task ReceiveMessageAsync_CantReachPosition_ShouldLost() - // { - // const string input = "FRFFL"; - // var botId = new Faker().Random.Hash(); - // var clientId = new Faker().Random.Hash(); - // - // var desiredPosition = new Position - // { - // X = 199, - // Y = 3, - // Direction = Direction.N - // }; - // - // await TestRunner - // .Arrange(() => - // { - // _decoderServiceMock.Setup(x => x.DecodeOperatorMessage( - // It.Is(i => i.Equals(input)))) - // .Returns([]); - // - // _repositoryMock.Setup(x => x.GetAsync(It.IsAny(), It.IsAny())) - // .ReturnsAsync(new Bot - // { - // Id = botId, - // Name = new Faker().Hacker.Noun(), - // Status = BotStatus.Acquired, - // Position = new Position - // { - // X = 1, - // Y = 1, - // Direction = Direction.N - // } - // }); - // - // _worldMapServiceMock.Setup(x => x.CalculateDesiredPosition(It.IsAny(), - // It.IsAny>())) - // .Returns(desiredPosition); - // - // _worldMapServiceMock.Setup(x => x.GetFuneralsAsync(It.IsAny())) - // .ReturnsAsync([]); - // - // _worldMapServiceMock.Setup(x => - // x.TryReachPosition(It.IsAny(), It.IsAny())) - // .ReturnsAsync(false); - // - // _decoderServiceMock.Setup(x => - // x.EncodeBotMessage( - // It.IsAny(), - // It.Is(b => b == false), - // It.Is(b => b == false))) - // .Returns("199 3 N"); - // - // return new MessageFacade(_repositoryMock.Object, _decoderServiceMock.Object, - // _worldMapServiceMock.Object); - // }) - // .ActAsync(sut => sut.ReceiveMessageAsync(new OperatorMessage - // { - // BotId = botId, - // ClientId = clientId, - // Text = input - // })) - // .ThenAssertAsync(() => - // { - // _worldMapServiceMock.Verify(x => - // x.ChangeBotPositionAsync(It.IsAny(), It.IsAny()), - // Times.Once); - // - // _streamMock.Verify(x => - // x.SendMessage(It.Is(r => r.IsLost)), Times.Once); - // }); - // } } \ No newline at end of file diff --git a/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/Fakes/FakeFuneral.cs b/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/Fakes/FakeFuneral.cs new file mode 100644 index 0000000..1655a45 --- /dev/null +++ b/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/Fakes/FakeFuneral.cs @@ -0,0 +1,21 @@ +using Bogus; +using Nasa.Pathfinder.Domain.Entities.World; +using Nasa.Pathfinder.Domain.World; + +namespace Nasa.Pathfinder.Services.Tests.Fakes; + +public static class FakeFuneral +{ + public static Funeral Make(string mapId) + { + return new Faker() + .RuleFor(x => x.Id, faker => faker.Database.Random.Uuid().ToString()) + .RuleFor(x => x.ETag, faker => faker.Random.Guid()) + .RuleFor(x => x.MapId, mapId) + .RuleFor(x => x.Value, _ => + new Faker() + .RuleFor(x => x.X, faker => faker.Random.Int()) + .RuleFor(x => x.Y, faker => faker.Random.Int()) + .RuleFor(x => x.Direction, faker => faker.Random.Enum())); + } +} \ No newline at end of file diff --git a/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs b/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs index 8f8e7de..52c4729 100644 --- a/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs +++ b/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs @@ -1,5 +1,7 @@ +using Bogus; using Moq; using Nasa.Pathfinder.Data.Contracts.Repositories; +using Nasa.Pathfinder.Domain.Entities.World; using Nasa.Pathfinder.Domain.Interactions; using Nasa.Pathfinder.Domain.World; using Nasa.Pathfinder.Services.Internal; @@ -20,81 +22,211 @@ public void Setup() private Mock _mockMapRepository; private Mock _mockFuneralRepository; - + [Test] - public void CalculateDesiredPosition_WhenWeGotListOfCommands_ShouldReturnsDestination() + public async Task ReachPosition_WhenFuneralWithDesiredPositionFindAndNotOutOfMap_ShouldReturnNotChanged() { - TestRunner - .Arrange(() => new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object)) - .Act(sut => + var mapId = $"map_{new Faker().Database.Random.Uuid()}"; + + var currentPosition = new Position + { + X = 0, + Y = 3, + Direction = Direction.W + }; + + var commands = new Stack(); + commands.Push(new MoveLeft()); + + commands.Push(new MoveFront()); + commands.Push(new MoveLeft()); + + commands.Push(new MoveFront()); + commands.Push(new MoveLeft()); + + commands.Push(new MoveFront()); + commands.Push(new MoveFront()); + commands.Push(new MoveFront()); + + commands.Push(new MoveLeft()); + commands.Push(new MoveLeft()); + + await TestRunner + .Arrange(() => { - var currentPosition = new Position - { - X = 1, - Y = 1, - Direction = Direction.E - }; + _mockMapRepository.Setup(x => x.TryGetAsync(mapId, It.IsAny())) + .ReturnsAsync(new MapInfo + { + Id = mapId, + ETag = new Faker().Random.Guid(), + SizeX = 5, + SizeY = 3, + }); + + _mockFuneralRepository.Setup(x => + x.GetFuneralsAsync(mapId, It.IsAny())) + .ReturnsAsync(new List + { + new() + { + Id = new Faker().Random.Guid().ToString(), + ETag = new Faker().Random.Guid(), + MapId = mapId, + Value = new Position + { + X = 3, + Y = 3, + Direction = Direction.N + } + } + }); - var commands = new Stack(); - commands.Push(new MoveFront()); - commands.Push(new MoveRight()); - commands.Push(new MoveFront()); - commands.Push(new MoveRight()); - commands.Push(new MoveFront()); - commands.Push(new MoveRight()); - commands.Push(new MoveFront()); - commands.Push(new MoveRight()); - - return sut.CalculateDesiredPosition(currentPosition, commands); + return new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object); }) - .Assert(position => + .ActAsync(async sut => await sut.ReachPosition(mapId, currentPosition, commands)) + .ThenAssertAsync(positionProject => { + var desiredPosition = new Position + { + X = 2, + Y = 3, + Direction = Direction.S + }; + Assert.Multiple(() => { - Assert.That(position.X, Is.EqualTo(1)); - Assert.That(position.Y, Is.EqualTo(1)); - Assert.That(position.Direction, Is.EqualTo(Direction.E)); + Assert.That(positionProject, Is.TypeOf()); + Assert.That(positionProject.Position.X, Is.EqualTo(desiredPosition.X)); + Assert.That(positionProject.Position.Y, Is.EqualTo(desiredPosition.Y)); + Assert.That(positionProject.Position.Direction, Is.EqualTo(desiredPosition.Direction)); }); + }); } - + [Test] - public void CalculateDesiredPosition_WhenDesiredPositionOutOfGrid_ShouldReturnsDestination() + public async Task ReachPosition_WhenBotCanMove_ShouldReturnChanged() { - TestRunner - .Arrange(() => new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object)) - .Act(sut => + var mapId = $"map_{new Faker().Database.Random.Uuid()}"; + + var currentPosition = new Position + { + X = 1, + Y = 1, + Direction = Direction.E + }; + + var commands = new Stack(); + commands.Push(new MoveFront()); + commands.Push(new MoveRight()); + commands.Push(new MoveFront()); + commands.Push(new MoveRight()); + commands.Push(new MoveFront()); + commands.Push(new MoveRight()); + commands.Push(new MoveFront()); + commands.Push(new MoveRight()); + + await TestRunner + .Arrange(() => + { + _mockMapRepository.Setup(x => x.TryGetAsync(mapId, It.IsAny())) + .ReturnsAsync(new MapInfo + { + Id = mapId, + ETag = new Faker().Random.Guid(), + SizeX = 5, + SizeY = 3, + }); + + _mockFuneralRepository.Setup(x => + x.GetFuneralsAsync(mapId, It.IsAny())) + .ReturnsAsync(new List()); + + return new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object); + }) + .ActAsync(async sut => await sut.ReachPosition(mapId, currentPosition, commands)) + .ThenAssertAsync(positionProject => { - var currentPosition = new Position + var desiredPosition = new Position { - X = 0, - Y = 3, - Direction = Direction.W + X = currentPosition.X, + Y = currentPosition.Y, + Direction = currentPosition.Direction }; + + Assert.Multiple(() => + { + Assert.That(positionProject, Is.TypeOf()); + Assert.That(positionProject.Position.X, Is.EqualTo(desiredPosition.X)); + Assert.That(positionProject.Position.Y, Is.EqualTo(desiredPosition.Y)); + Assert.That(positionProject.Position.Direction, Is.EqualTo(desiredPosition.Direction)); + }); + + }); + } + + [Test] + public async Task ReachPosition_WhenDesiredPositionOutOfGrid_ShouldReturnsDestination() + { + var mapId = $"map_{new Faker().Database.Random.Uuid()}"; + + var currentPosition = new Position + { + X = 3, + Y = 2, + Direction = Direction.N + }; + + var commands = new Stack(); + commands.Push(new MoveLeft()); + commands.Push(new MoveLeft()); + commands.Push(new MoveFront()); + commands.Push(new MoveRight()); + commands.Push(new MoveRight()); + commands.Push(new MoveFront()); + commands.Push(new MoveFront()); + commands.Push(new MoveLeft()); + commands.Push(new MoveLeft()); + commands.Push(new MoveFront()); + commands.Push(new MoveRight()); + commands.Push(new MoveRight()); + commands.Push(new MoveFront()); - var commands = new Stack(); - commands.Push(new MoveLeft()); - commands.Push(new MoveFront()); - commands.Push(new MoveLeft()); - commands.Push(new MoveFront()); - commands.Push(new MoveLeft()); - commands.Push(new MoveFront()); - commands.Push(new MoveFront()); - commands.Push(new MoveFront()); - commands.Push(new MoveLeft()); - commands.Push(new MoveLeft()); + await TestRunner + .Arrange(() => + { + _mockMapRepository.Setup(x => x.TryGetAsync(mapId, It.IsAny())) + .ReturnsAsync(new MapInfo + { + Id = mapId, + ETag = new Faker().Random.Guid(), + SizeX = 5, + SizeY = 3, + }); + _mockFuneralRepository.Setup(x => + x.GetFuneralsAsync(mapId, It.IsAny())) + .ReturnsAsync(new List()); - return sut.CalculateDesiredPosition(currentPosition, commands); + return new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object); }) - .Assert(position => + .ActAsync(async sut => await sut.ReachPosition(mapId, currentPosition, commands)) + .ThenAssertAsync(positionProject => { Assert.Multiple(() => { - Assert.That(position.X, Is.EqualTo(2)); - Assert.That(position.Y, Is.EqualTo(4)); - Assert.That(position.Direction, Is.EqualTo(Direction.S)); + Assert.That(positionProject, Is.TypeOf()); + + Assert.That(positionProject.Position.X, Is.EqualTo(3)); + Assert.That(positionProject.Position.Y, Is.EqualTo(4)); + Assert.That(positionProject.Position.Direction, Is.EqualTo(Direction.N)); + + var @out = positionProject as PositionOutOfMap; + Assert.That(@out.Previous.X, Is.EqualTo(3)); + Assert.That(@out.Previous.Y, Is.EqualTo(3)); + Assert.That(@out.Previous.Direction, Is.EqualTo(Direction.N)); }); + }); } } \ No newline at end of file diff --git a/tests/integration/Nasa.IntegrationTests.Pathfinder/Nasa.IntegrationTests.Pathfinder.csproj b/tests/integration/Nasa.IntegrationTests.Pathfinder/Nasa.IntegrationTests.Pathfinder.csproj index 62aae58..ffe9576 100644 --- a/tests/integration/Nasa.IntegrationTests.Pathfinder/Nasa.IntegrationTests.Pathfinder.csproj +++ b/tests/integration/Nasa.IntegrationTests.Pathfinder/Nasa.IntegrationTests.Pathfinder.csproj @@ -11,6 +11,7 @@ + diff --git a/tests/integration/Nasa.IntegrationTests.Pathfinder/PathfinderServiceTests.cs b/tests/integration/Nasa.IntegrationTests.Pathfinder/PathfinderServiceTests.cs index 1cba1c1..3501212 100644 --- a/tests/integration/Nasa.IntegrationTests.Pathfinder/PathfinderServiceTests.cs +++ b/tests/integration/Nasa.IntegrationTests.Pathfinder/PathfinderServiceTests.cs @@ -1,4 +1,6 @@ using System.Diagnostics; +using System.Numerics; +using ErrorOr; using Google.Protobuf.WellKnownTypes; using Grpc.Core; using Grpc.Net.Client; @@ -28,13 +30,16 @@ public void Ping_ReturnsOk_HappyPath() .Act(sut => sut.Ping(new Empty())) .Assert(response => Assert.That(response.IsSuccessful, Is.True)); } - + [Test] public void GetBots_ReturnsBotList_ShouldReturn3Bots() { TestRunner .Arrange(() => new Client(_channel)) - .Act(sut => sut.GetBots(new Empty())) + .Act(sut => sut.GetBots(new GetBotsRequest + { + MapId = "1" + })) .Assert(response => { Assert.Multiple(() => @@ -45,7 +50,7 @@ public void GetBots_ReturnsBotList_ShouldReturn3Bots() }); }); } - + [Test] public void SelectBot_ReturnsBot_ShouldReturnBot() { @@ -53,22 +58,19 @@ public void SelectBot_ReturnsBot_ShouldReturnBot() .Arrange(() => new Client(_channel)) .Act(sut => { - var bot = sut.GetBots(new Empty()).Bots.First(); + var bot = sut.GetBots(new GetBotsRequest + { + MapId = "1" + }).Bots.First(); var result = sut.SelectBot(new SelectBotRequest { BotId = bot.Id }); return result; }) - .Assert(response => - { - Assert.Multiple(() => - { - Assert.That(response.Bot, Is.Not.Null); - }); - }); + .Assert(response => { Assert.Multiple(() => { Assert.That(response.Bot, Is.Not.Null); }); }); } - + [Test] public void SelectBot_BotAlreadyReleased_ShouldFail() { @@ -76,14 +78,17 @@ public void SelectBot_BotAlreadyReleased_ShouldFail() .Arrange(() => new Client(_channel)) .Act(sut => { - var bot = sut.GetBots(new Empty()).Bots.First(); + var bot = sut.GetBots(new GetBotsRequest + { + MapId = "1" + }).Bots.First(); try { sut.SelectBot(new SelectBotRequest { BotId = bot.Id }); - + sut.SelectBot(new SelectBotRequest { BotId = bot.Id @@ -103,7 +108,7 @@ public void SelectBot_BotAlreadyReleased_ShouldFail() Assert.That(response.StatusCode, Is.EqualTo(StatusCode.InvalidArgument)); }); } - + [Test] public void ResetBot_ReturnsBot_ShouldInvalidArgument() { @@ -111,7 +116,10 @@ public void ResetBot_ReturnsBot_ShouldInvalidArgument() .Arrange(() => new Client(_channel)) .Act(sut => { - var bot = sut.GetBots(new Empty()).Bots.First(); + var bot = sut.GetBots(new GetBotsRequest + { + MapId = "1" + }).Bots.First(); bot = sut.SelectBot(new SelectBotRequest { BotId = bot.Id @@ -121,18 +129,12 @@ public void ResetBot_ReturnsBot_ShouldInvalidArgument() { BotId = bot.Id }); - + return response; }) - .Assert(response => - { - Assert.Multiple(() => - { - Assert.That(response.Bot, Is.Not.Null); - }); - }); + .Assert(response => { Assert.Multiple(() => { Assert.That(response.Bot, Is.Not.Null); }); }); } - + [Test] public void ResetBot_BotIsFree_ShouldInvalidArgument() { @@ -140,7 +142,10 @@ public void ResetBot_BotIsFree_ShouldInvalidArgument() .Arrange(() => new Client(_channel)) .Act(sut => { - var bot = sut.GetBots(new Empty()).Bots.First(); + var bot = sut.GetBots(new GetBotsRequest + { + MapId = "1" + }).Bots.First(); bot = sut.SelectBot(new SelectBotRequest { BotId = bot.Id @@ -157,7 +162,7 @@ public void ResetBot_BotIsFree_ShouldInvalidArgument() { BotId = bot.Id }); - + return null; } catch (RpcException ex) @@ -177,48 +182,211 @@ public void ResetBot_BotIsFree_ShouldInvalidArgument() } [Test] - public async Task SendMessage_SendCommandAndWaitResult_ShouldReturnMessageWithBotStatus() + //[Ignore("Right now, it's only for manual starting")] + public async Task SendMessage_ApplyTestTaskBehavior_ShouldReturn3DifferentMessages() { - await TestRunner - .Arrange(() => new Client(_channel)) - .ActAsync(async sut => + const string message1 = "RFRFRFRF"; + const string message2 = "FRRFLLFFRRFLL"; + const string message3 = "LLFFFLFLFL"; + var botsMessage = new Dictionary(); + var botsPositions = new Dictionary + { + { message1, new Position { X = 1, Y = 1, Direction = "E" } }, + { message2, new Position { X = 3, Y = 3, Direction = "N" } }, + { message3, new Position { X = 2, Y = 4, Direction = "S" } }, + }; + + var mapId = string.Empty; + + string[] botNames = ["bot-12", "bot-14", "bot-15"]; + + await TestRunner>> + .ArrangeAsync(async () => { - var bot = sut.GetBots(new Empty()).Bots.First(); - bot = sut.SelectBot(new SelectBotRequest + var client = new Client(_channel); + + var createWorldResponse = await client.CreateWorldAsync(new CreateWorldRequest { - BotId = bot.Id - }).Bot; + SizeX = 5, + SizeY = 3, + Bots = + { + new Bot() { Name = botNames[0], Position = new Position { X = 1, Y = 1, Direction = "E"} }, + new Bot() { Name = botNames[1], Position = new Position { X = 3, Y = 2, Direction = "N"} }, + new Bot() { Name = botNames[2], Position = new Position { X = 0, Y = 3, Direction = "W"} } + } + }); + + mapId = createWorldResponse.MapId; + + return client; + }) + .ActAsync(async sut => + { + var responses = new List>(); - var stream = sut.SendMessage(new Metadata + try { - { "TraceId", Activity.Current?.TraceId.ToString() ?? Guid.NewGuid().ToString() }, - { "clientId", Guid.NewGuid().ToString() } - }); + //<--- First bot ---> - var cts = new CancellationTokenSource(); + var first = sut.GetBots(new GetBotsRequest + { + MapId = mapId + }).Bots.First(x => x.Name == botNames[0]); + var bot = sut.SelectBot(new SelectBotRequest + { + BotId = first.Id + }).Bot; + + await StartWorkflowAsync(bot, message1); - await stream.RequestStream.WriteAsync(new SendMessageRequest - { - BotId = bot.Id, - Message = "RFRFRFRF" - }, cts.Token); - - await stream.RequestStream.CompleteAsync(); + //<--- Second bot ---> + + var second = sut.GetBots(new GetBotsRequest + { + MapId = mapId + }).Bots.First(x => x.Name == botNames[1]); + bot = sut.SelectBot(new SelectBotRequest + { + BotId = second.Id + }).Bot; + + await StartWorkflowAsync(bot, message2); - while (await stream.ResponseStream.MoveNext(cts.Token)) + //<--- Third bot ---> + + var third = sut.GetBots(new GetBotsRequest + { + MapId = mapId + }).Bots.First(x => x.Name == botNames[2]); + bot = sut.SelectBot(new SelectBotRequest + { + BotId = third.Id + }).Bot; + + await StartWorkflowAsync(bot, message3); + } + catch (Exception ex) { - var response = stream.ResponseStream.Current; - return response; + responses.Add(Error.Failure(ex.Message)); } - throw new Exception("No response received from gRPC stream."); + return responses; + + + async Task StartWorkflowAsync(Bot bot, string message) + { + var stream = sut.SendMessage(new Metadata + { + { "TraceId", Activity.Current?.TraceId.ToString() ?? Guid.NewGuid().ToString() }, + { "clientId", Guid.NewGuid().ToString() } + }); + + Thread.Sleep(1000); + + botsMessage.Add(bot.Id, message); + await stream.RequestStream.WriteAsync(new SendMessageRequest + { + BotId = bot.Id, + Message = message + }, CancellationToken.None); + + Thread.Sleep(2000); + + var cts = new CancellationTokenSource(); + var token = cts.Token; + + try + { + while (await stream.ResponseStream.MoveNext(token)) + { + var response = stream.ResponseStream.Current; + responses.Add(response); + break; + } + } + catch (Exception ex) + { + responses.Add(Error.Failure(ex.Message)); + } + + await stream.RequestStream.CompleteAsync(); + } }) - .ThenAssertAsync(_ => + .ThenAssertAsync(responses => { - Assert.Pass(); + var errors = new List(); + + foreach (var response in responses) + { + if (response.IsError) + { + errors.Add(response.FirstError); + } + } + + try + { + var client = new Client(_channel); + var bots = client.GetBots(new GetBotsRequest + { + MapId = mapId + }).Bots; + + foreach (var bot in bots) + { + if (bot.Status == "Available") + errors.Add(Error.Unexpected("Bot is available", + $"Bot ({bot.Name}) is still available instead of be acquired")); + + if (bot.Name == botNames[0]) + { + CheckLocation(bot, new Vector2(1, 1), "E"); + } + + if (bot.Name == botNames[1]) + { + CheckLocation(bot, new Vector2(3, 3), "N"); + + if (bot.Status != "Dead") + errors.Add(Error.Unexpected("Bot is available", + $"Bot ({bot.Name}) is still available instead of be acquired")); + } + + if (bot.Name == botNames[2]) + { + CheckLocation(bot, new Vector2(2, 3), "S"); + } + } + + void CheckLocation(Bot bot, Vector2 coordinates, string direction) + { + if (bot.Position.X != (int)coordinates.X + && bot.Position.Y != (int)coordinates.Y + && bot.Position.Direction != direction) + { + errors.Add(Error.Unexpected("Incorrect position", + $"Bot ({bot.Name}) has incorrect position")); + } + } + } + finally + { + if (!errors.Any()) Assert.Pass(); + + if (errors.Count > 0) + { + foreach (var error in errors) + { + Console.WriteLine("{0}: {1}", error.Code, error.Description); + } + + Assert.Fail(); + } + } }); } - + [TearDown] public void TearDown() { diff --git a/tests/integration/Nasa.IntegrationTests/TestRunner.cs b/tests/integration/Nasa.IntegrationTests/TestRunner.cs index 02ff66b..4392c4b 100644 --- a/tests/integration/Nasa.IntegrationTests/TestRunner.cs +++ b/tests/integration/Nasa.IntegrationTests/TestRunner.cs @@ -3,18 +3,31 @@ namespace Nasa.IntegrationTests; public class TestRunner { private readonly Func _arrange; + private readonly Func> _arrangeAsync; private TResult? _result; private TSut? _sut; private TestRunner(Func arrange) { _arrange = arrange; + _arrangeAsync = null; + } + + private TestRunner(Func> arrange) + { + _arrange = null; + _arrangeAsync = arrange; } public static TestRunner Arrange(Func arrange) { return new TestRunner(arrange); } + + public static TestRunner ArrangeAsync(Func> arrange) + { + return new TestRunner(arrange); + } public TestRunner Act(Func act) { @@ -25,7 +38,7 @@ public TestRunner Act(Func act) public async Task> ActAsync(Func> act) { - _sut = _arrange(); + _sut = _arrangeAsync is null ? _arrange() : await _arrangeAsync(); _result = await act(_sut); return this; } diff --git a/tests/integration/integration-tests.sln b/tests/integration/integration-tests.sln index bdc92b6..42be69e 100644 --- a/tests/integration/integration-tests.sln +++ b/tests/integration/integration-tests.sln @@ -4,6 +4,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nasa.IntegrationTests", "Na EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nasa.IntegrationTests.Pathfinder", "Nasa.IntegrationTests.Pathfinder\Nasa.IntegrationTests.Pathfinder.csproj", "{4879B4E7-BC2B-43DB-8CC5-5B26272939F3}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{697A6CE2-07F5-4DA4-A385-F2608C30E561}" + ProjectSection(SolutionItems) = preProject + ..\..\cd\stage\docker-compose.local.yaml = ..\..\cd\stage\docker-compose.local.yaml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU