diff --git a/ConsoleApp/ConsoleApp.csproj b/ConsoleApp/ConsoleApp.csproj index 4afbb5b..6ebd867 100644 --- a/ConsoleApp/ConsoleApp.csproj +++ b/ConsoleApp/ConsoleApp.csproj @@ -2,12 +2,13 @@ Exe - net5.0 + net7.0 9 + diff --git a/ConsoleApp/Program.cs b/ConsoleApp/Program.cs index cf39300..bf70ea8 100644 --- a/ConsoleApp/Program.cs +++ b/ConsoleApp/Program.cs @@ -1,6 +1,9 @@ using System; +using System.Collections.Generic; using System.Linq; using Game.Domain; +using JetBrains.Annotations; +using Tests; namespace ConsoleApp { @@ -8,12 +11,15 @@ class Program { private readonly IUserRepository userRepo; private readonly IGameRepository gameRepo; + private readonly IGameTurnRepository gameTurnRepo; private readonly Random random = new Random(); private Program(string[] args) { - userRepo = new InMemoryUserRepository(); - gameRepo = new InMemoryGameRepository(); + var db = TestMongoDatabase.Create(); + userRepo = new MongoUserRepository(db, false); + gameRepo = new MongoGameRepository(db, false); + gameTurnRepo = new MongoGameTurnRepository(db, false); } public static void Main(string[] args) @@ -125,8 +131,8 @@ private bool HandleOneGameTurn(Guid humanUserId) if (game.HaveDecisionOfEveryPlayer) { - // TODO: Сохранить информацию о прошедшем туре в IGameTurnRepository. Сформировать информацию о закончившемся туре внутри FinishTurn и вернуть её сюда. - game.FinishTurn(); + var gameTurn = game.FinishTurn(); + gameTurnRepo.Insert(gameTurn); } ShowScore(game); @@ -180,8 +186,23 @@ private void UpdatePlayersWhenGameFinished(GameEntity game) private void ShowScore(GameEntity game) { var players = game.Players; - // TODO: Показать информацию про 5 последних туров: кто как ходил и кто в итоге выиграл. Прочитать эту информацию из IGameTurnRepository + var lastTurns = gameTurnRepo.FindLastTurns(game.Id); + foreach (var lastTurn in lastTurns) + { + var winner = GetWinner(players, lastTurn.WinnerId); + Console.WriteLine($"Turn {lastTurn.TurnIndex}: {players[0].Name} {lastTurn.FirstPlayerDecision} : {players[1].Name} {lastTurn.SecondPlayerDecision}"); + Console.WriteLine($"Turn result: {(winner is null ? "draw" : $"{winner.Name} win")}"); + } Console.WriteLine($"Score: {players[0].Name} {players[0].Score} : {players[1].Score} {players[1].Name}"); } + + [CanBeNull] + private Player GetWinner(IReadOnlyList players, Guid winnerId) + { + if (winnerId == Guid.Empty) + return null; + + return players[0].UserId == winnerId ? players[0] : players[1]; + } } } diff --git a/Game/Domain/GameEntity.cs b/Game/Domain/GameEntity.cs index ec7b5ec..815ec64 100644 --- a/Game/Domain/GameEntity.cs +++ b/Game/Domain/GameEntity.cs @@ -1,18 +1,21 @@ using System; using System.Collections.Generic; using System.Linq; +using MongoDB.Bson.Serialization.Attributes; namespace Game.Domain { public class GameEntity { + [BsonElement] private readonly List players; public GameEntity(int turnsCount) : this(Guid.Empty, GameStatus.WaitingToStart, turnsCount, 0, new List()) { } - + + [BsonConstructor] public GameEntity(Guid id, GameStatus status, int turnsCount, int currentTurnIndex, List players) { Id = id; @@ -31,6 +34,7 @@ public Guid Id public IReadOnlyList Players => players.AsReadOnly(); + [BsonElement] public int TurnsCount { get; } public int CurrentTurnIndex { get; private set; } @@ -88,9 +92,7 @@ public GameTurnEntity FinishTurn() winnerId = player.UserId; } } - //TODO Заполнить все внутри GameTurnEntity, в том числе winnerId - var result = new GameTurnEntity(); - // Это должно быть после создания GameTurnEntity + var result = new GameTurnEntity(Id, winnerId, Players[0].Decision!.Value, Players[1].Decision!.Value, CurrentTurnIndex); foreach (var player in Players) player.Decision = null; CurrentTurnIndex++; diff --git a/Game/Domain/GameTurnEntity.cs b/Game/Domain/GameTurnEntity.cs index 0c07495..38828d7 100644 --- a/Game/Domain/GameTurnEntity.cs +++ b/Game/Domain/GameTurnEntity.cs @@ -1,7 +1,35 @@ +using System; +using MongoDB.Bson.Serialization.Attributes; + namespace Game.Domain { public class GameTurnEntity { - //TODO: Придумать какие свойства должны быть в этом классе, чтобы сохранять всю информацию о закончившемся туре. + public Guid Id { get; set; } + + [BsonElement] + public Guid GameId { get; } + + [BsonElement] + public Guid WinnerId { get; } + + [BsonElement] + public PlayerDecision FirstPlayerDecision { get; } + + [BsonElement] + public PlayerDecision SecondPlayerDecision { get; } + + [BsonElement] + public int TurnIndex { get; } + + [BsonConstructor] + public GameTurnEntity(Guid gameId, Guid winnerId, PlayerDecision firstPlayerDecision, PlayerDecision secondPlayerDecision, int turnIndex) + { + GameId = gameId; + WinnerId = winnerId; + FirstPlayerDecision = firstPlayerDecision; + SecondPlayerDecision = secondPlayerDecision; + TurnIndex = turnIndex; + } } } \ No newline at end of file diff --git a/Game/Domain/IGameTurnRepository.cs b/Game/Domain/IGameTurnRepository.cs index f540e2c..f6490c0 100644 --- a/Game/Domain/IGameTurnRepository.cs +++ b/Game/Domain/IGameTurnRepository.cs @@ -1,7 +1,12 @@ +using System; +using System.Collections.Generic; + namespace Game.Domain { public interface IGameTurnRepository { - // TODO: Спроектировать интерфейс исходя из потребностей ConsoleApp + GameTurnEntity Insert(GameTurnEntity turn); + + IList FindLastTurns(Guid gameId, int turnsCount = 5); } } \ No newline at end of file diff --git a/Game/Domain/MongoGameRepository.cs b/Game/Domain/MongoGameRepository.cs index 86873d4..6e9b3bc 100644 --- a/Game/Domain/MongoGameRepository.cs +++ b/Game/Domain/MongoGameRepository.cs @@ -7,39 +7,51 @@ namespace Game.Domain // TODO Сделать по аналогии с MongoUserRepository public class MongoGameRepository : IGameRepository { + private readonly IMongoCollection gameCollection; public const string CollectionName = "games"; - public MongoGameRepository(IMongoDatabase db) + public MongoGameRepository(IMongoDatabase db, bool dropCollection = true) { + if (dropCollection) + db.DropCollection(CollectionName); + + gameCollection = db.GetCollection(CollectionName); } public GameEntity Insert(GameEntity game) { - throw new NotImplementedException(); + gameCollection.InsertOne(game); + return game; } public GameEntity FindById(Guid gameId) { - throw new NotImplementedException(); + var filter = Builders.Filter.Eq(g => g.Id, gameId); + return gameCollection.Find(filter).FirstOrDefault(); } public void Update(GameEntity game) { - throw new NotImplementedException(); + var filter = Builders.Filter.Eq(g => g.Id, game.Id); + gameCollection.ReplaceOne(filter, game); } // Возвращает не более чем limit игр со статусом GameStatus.WaitingToStart public IList FindWaitingToStart(int limit) { - //TODO: Используй Find и Limit - throw new NotImplementedException(); + var filter = Builders.Filter.Eq(g => g.Status, GameStatus.WaitingToStart); + return gameCollection.Find(filter) + .Limit(limit) + .ToList(); } // Обновляет игру, если она находится в статусе GameStatus.WaitingToStart public bool TryUpdateWaitingToStart(GameEntity game) { - //TODO: Для проверки успешности используй IsAcknowledged и ModifiedCount из результата - throw new NotImplementedException(); + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Eq(g => g.Status, GameStatus.WaitingToStart) & filterBuilder.Eq(g => g.Id, game.Id); + var result = gameCollection.ReplaceOne(filter, game); + return result.IsAcknowledged && result.ModifiedCount == 1; } } } \ No newline at end of file diff --git a/Game/Domain/MongoGameTurnRepository.cs b/Game/Domain/MongoGameTurnRepository.cs index 1fa10a5..4461e62 100644 --- a/Game/Domain/MongoGameTurnRepository.cs +++ b/Game/Domain/MongoGameTurnRepository.cs @@ -1,6 +1,43 @@ +using System; +using System.Collections.Generic; +using MongoDB.Driver; + namespace Game.Domain { public class MongoGameTurnRepository : IGameTurnRepository { + private IMongoCollection turnsCollection; + public const string CollectionName = "turns"; + + public MongoGameTurnRepository(IMongoDatabase db, bool dropCollection = true) + { + if (dropCollection) + db.DropCollection(CollectionName); + + turnsCollection = db.GetCollection(CollectionName); + + var indexBuilder = Builders.IndexKeys.Ascending(entity => entity.GameId); + var createIndexModel = new CreateIndexModel(indexBuilder); + turnsCollection.Indexes.CreateOne(createIndexModel); + indexBuilder = Builders.IndexKeys.Descending(entity => entity.TurnIndex); + createIndexModel = new CreateIndexModel(indexBuilder); + turnsCollection.Indexes.CreateOne(createIndexModel); + } + + + public GameTurnEntity Insert(GameTurnEntity turn) + { + turnsCollection.InsertOne(turn); + return turn; + } + + public IList FindLastTurns(Guid gameId, int turnsCount = 5) + { + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Eq(entity => entity.GameId, gameId); + var lastTurns = turnsCollection.Find(filter).SortByDescending(t => t.TurnIndex).Limit(turnsCount).ToList(); + lastTurns.Reverse(); + return lastTurns; + } } } \ No newline at end of file diff --git a/Game/Domain/MongoUserRepositoty.cs b/Game/Domain/MongoUserRepositoty.cs index 51aeba5..7ba6fd8 100644 --- a/Game/Domain/MongoUserRepositoty.cs +++ b/Game/Domain/MongoUserRepositoty.cs @@ -1,4 +1,5 @@ using System; +using MongoDB.Bson; using MongoDB.Driver; namespace Game.Domain @@ -8,46 +9,72 @@ public class MongoUserRepository : IUserRepository private readonly IMongoCollection userCollection; public const string CollectionName = "users"; - public MongoUserRepository(IMongoDatabase database) + public MongoUserRepository(IMongoDatabase database, bool dropCollection = true) { + if (dropCollection) + database.DropCollection(CollectionName); + userCollection = database.GetCollection(CollectionName); + userCollection.Indexes.CreateOne(Builders.IndexKeys.Ascending(e => e.Login), new CreateIndexOptions { Unique = true }); } public UserEntity Insert(UserEntity user) { - //TODO: Ищи в документации InsertXXX. - throw new NotImplementedException(); + userCollection.InsertOne(user); + return user; } public UserEntity FindById(Guid id) { - //TODO: Ищи в документации FindXXX - throw new NotImplementedException(); + var filter = new BsonDocument("_id", id); + return userCollection.Find(filter).FirstOrDefault(); } public UserEntity GetOrCreateByLogin(string login) { - //TODO: Это Find или Insert - throw new NotImplementedException(); + var filter = new BsonDocument("Login", login); + var user = userCollection.Find(filter).FirstOrDefault(); + if (user is null) + { + user = new UserEntity { Login = login }; + try + { + userCollection.InsertOne(user); + } + catch (MongoWriteException) + { + user = userCollection.Find(filter).FirstOrDefault(); + } + } + + return user; } public void Update(UserEntity user) { - //TODO: Ищи в документации ReplaceXXX - throw new NotImplementedException(); + var filter = new BsonDocument("_id", user.Id); + userCollection.ReplaceOne(filter, user); } public void Delete(Guid id) { - throw new NotImplementedException(); + var filter = new BsonDocument("_id", id); + userCollection.DeleteOne(filter); } // Для вывода списка всех пользователей (упорядоченных по логину) // страницы нумеруются с единицы public PageList GetPage(int pageNumber, int pageSize) { - //TODO: Тебе понадобятся SortBy, Skip и Limit - throw new NotImplementedException(); + var findResult = userCollection.Find(FilterDefinition.Empty) + .SortBy(entity => entity.Login) + .Skip((pageNumber - 1) * pageSize) + .Limit(pageSize); + return new PageList( + findResult.ToList(), + userCollection.CountDocuments(FilterDefinition.Empty), + pageNumber, + pageSize); } // Не нужно реализовывать этот метод diff --git a/Game/Domain/Player.cs b/Game/Domain/Player.cs index 9c4f838..6b9e6d2 100644 --- a/Game/Domain/Player.cs +++ b/Game/Domain/Player.cs @@ -1,4 +1,5 @@ using System; +using MongoDB.Bson.Serialization.Attributes; namespace Game.Domain { @@ -7,17 +8,21 @@ namespace Game.Domain /// public class Player { + [BsonConstructor] public Player(Guid userId, string name) { UserId = userId; Name = name; } + [BsonElement] public Guid UserId { get; } /// /// Снэпшот имени игрока на момент старта игры. Считайте, что это такое требование к игре. /// + + [BsonElement] public string Name { get; } /// diff --git a/Game/Game.csproj b/Game/Game.csproj index 33c4f28..b0a85a0 100644 --- a/Game/Game.csproj +++ b/Game/Game.csproj @@ -1,7 +1,7 @@  - net5.0 + net7.0 9 diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 6071f4a..2a6c9f5 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -1,7 +1,7 @@  - net5.0 + net7.0 9 diff --git a/global.json b/global.json new file mode 100644 index 0000000..7cd6a1f --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "7.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file