ScoreX

player

Итоговый проект по курсу Основы Проектирования на C#

Об игре

Тема

  • Казуальная карточная(визуализация)игра
  • Антураж - футбольные команды и игроки
  • вдохновление - HeartStone

Суть

Играют 2 игрока
Сначала договариваются о правилах
Поочередно ходят в соответствии с правилами
Побеждает тот, у кого больше очков

Кто и что делал



Максим

  1. Actions
  2. Внутренняя логика
  3. Тесты. DI-контейнер

Елена

  1. Actions
  2. Консольный контроллер
  3. База Данных

Данные о футболистах


Name Rating Preferred pos Skill
Cristiano Ronaldo 94 LW/ST 5
David Silva 87 CM 4
Fedor Smolov 79 ST 2

Точки расширения проекта

Actions

Хранилище

Механика

Actions


player player

схема основных Action-ов

action

Action - основа гибкости игры

						public interface IAction
    {
        string Explanation { get; }
        int Value { get; }
        bool IsAvailable { get; }
        bool Execute(IParameters parameters);
        void SetUp(Game game);
        void Accept(ISuccess success);
    }
					

Удобно хранить действия в одной структуре данных

Разные действия принимают разное количество параметров разных типов

						
public abstract class Action<T> : IAction
        where T : class, IParameters
    {
        public abstract bool IsAvailable { get; }
        public abstract string Explanation { get; }
        public abstract int Value { get; }

        public abstract bool AreSuitable(T parameters);
        public abstract bool Execute(T parameters);
        public abstract void Accept(ISuccess success);

        protected bool wasSuccessfullyExecuted;
        protected Game game;

        // More code ...    
    }

Вызывается Action с конкретными параметрами

			
	public abstract bool Execute(T parameters);

	public bool Execute(IParameters parameters)
        {
            wasSuccessfullyExecuted = false;
            var converted = CheckParameters(parameters);
            return Execute(converted);
        }
			
		

public class GetFromDeckAction : Action<GetFromDeckParameters>
    {
        public override string Explanation => "Get card from deck and put it in hand";

        public override int Value => 5;

        public override bool IsAvailable => true;

        public override bool AreSuitable(GetFromDeckParameters parameters) => true;

        public override bool Execute(GetFromDeckParameters parameters) {
            var deck = parameters.Deck;
            if (!deck.Any)
                return false;
            game.CurrentPlayer.Team.Hand.InsertCard(deck.GetCard());
            wasSuccessfullyExecuted = true;
            return wasSuccessfullyExecuted;
        }

        public override void Accept(ISuccess success) => 
            success.Apply(this, wasSuccessfullyExecuted);
    }

ISuccess

Меняет глобальное состояние игры в зависимости от результата действия

public interface ISuccess
    {
        string Message { get; }
        void Apply(PassAction action, bool successful);
        // More code...
    }
public void Apply(PassAction action, bool successful)
    {
        var successPass = SuccessAction(action, 
                          g => $"ball moves to {game.BallPlace}, Nice!");
            
        var failurePass = FailureAction(action, 
                          g => $"ball was intercepted by {game.BallOwner}");
        Apply(successPass, failurePass, successful);
    }

Хранилище

Разные способы загрузки данных для игры

		
public interface IDatabase
    { 
        PlayerInfo GetPlayerInfo(string name);
        PlayerInfo GetPlayerOfType(params string[] types);
        IEnumerable<PlayerInfo> GetPlayers(int count);
    }
		
	

Обертка для IDatabase для создания объектов игры

		
public interface IFootballDatabase
    {
        FootballCard GetCardOfType(ZoneType zone);
        IEnumerable<FootballCard>GetCards(int count);
    }
		
	

Механика

  • Легко добавлять способы расчета характеристик Action-а
  • На уровне кода настраивать баланс в игре
  • Большое количество показателей для игровых объектов

Пример рассчета характеристики успешности паса

			
public static double PassPower(this Zone zone)
{
    var totalAvgDef = zone.CardsAttributes(f => f.Defend).Average();
    var totalAvgMid = zone.CardsAttributes(f => f.Midfield).Average();
    var totalAvgAtt = zone.CardsAttributes(f => f.Attack).Average();
    return (totalAvgDef + totalAvgMid + totalAvgAtt) / 3;
}
private static IEnumerable<double> CardsAttributes(this Zone zone, Func<FootballCard, 
            double> attributeSelector)
        {
            var attributes = zone.GetCards().Select(f => attributeSelector(f));
            if (!attributes.Any())
                return new List<double>() { 0 };
            return attributes;
        }

Общая структура решения

game screen

Game

			
public class Game
{        
    public readonly Deck Deck;
    public int MovesLeft { get; private set; }
    public Player CurrentPlayer { get; }
    public string Message { get; }
    public IEnumerable<Player> GetOpponents { get; }
    private void Next();
    public void Turn(Tuple<IAction, IParameters> executionPair);
    public bool IsEnd { get; }
    public void AddPlayer(Player player);
}
			
		

Player

			
	public class Player
	{
	    public readonly string Name;
	    public Team Team { get; set; }
	    public int Score { get; private set; }

	    public void IncreaseScore(int points);
	}
			
		

Team

			
    public class Team
    {
        public readonly Squad Squad;
        public readonly Hand Hand;
        public IBall Ball { get; private set; }
        public bool HasBall { get; }
        public void Update(IBall ball);

        public void SubstitutionFromHandToSquad(ZoneType type, 
                                                int cardPosition,
                                                int newCardPosition);
        
        public void SwapInSquad(ZoneType first, int firstPos, 
                                ZoneType second, int secondPos;    
    }

Hand

			
    public class Hand
    {
        public int HandSize { get; }
        public bool Any { get; }
        public FootballCard Peek { get; }

        public bool Remove(FootballCard card);
        public void InsertCard(FootballCard card);
        public IEnumerable<FootballCard> GetCardsByRank(int count);
        public bool Contains(FootballCard card);
    }
			
		

Squad

			
    public class Squad
    {
        private Dictionary<ZoneType, Zone> squad;

        public readonly string Formation;
        public string Name { get; private set; }
        public bool Any { get; }

        public bool IsActive(ZoneType zone, int position);
        
        public FootballCard Remove(ZoneType type, int cardIndex);

        public void Insert(ZoneType type, FootballCard card, 
        				   int position);
        
        public double GetZonePower(ZoneType zoneType,
                                   Func<Zone, double> calculate);
    }
			
		

Zone

			
    public class Zone : BaseZone
    {
        public bool Any;
        public int Count;

        public IEnumerable<FootballCard> GetCards();

        public FootballCard RemoveCard(int cardIndex);

        public void RemoveDeadCards();

        public void InsertCard(FootballCard card, int position);
    }
			
		

Zone Extensions

			
    static class ZoneExtensions
    {
        public static double PassPower(this Zone zone);
        public static double DefendPower(this Zone zone);
        public static double WithAdditionalPower(this Zone zone, 
                                                 FootballCard card);
        public static double InterceptPower(this Zone zone);
        public static double ShootPower(this Zone zone);
        public static double PressurePower(this Zone zone);

        public static void DecreaseRandomCardRank(this Zone zone, 
                                                  int percent);
    }
		

Ball

			
    public class Ball: IBall
    {
        private List<Team> observers;
        private Team owner;
        public ZoneType Place { get; private set; }

        public void AddObserver(Team observer);
        public void Move();
        public void InterceptedBy(Team newOwner);
        public void Restart(Team newOwner);
        private void UpdateObservers(Team newOwner);
    }
			
		

Текущее состояние игры


Консольный режим

console console

Взаимодействие с пользователем с консоли

parser

Перспективы

unity

Планируется полностью перенести игру на Юнити

Перспективы

  • Нужно создавать графические карточки (почти сделано)
  • придумать интерфейс взаимодействия пользователя с объектами визуализации
  • подставлять выбор пользователя в game.Turn()
  • добавить эффекты

DI-контейнер

		
    // ...
    var container = new StandardKernel();
    container.Bind<IDatabase>().To<MongoDatabase>();
    container.Bind<IFootballDatabase>().To<FootballDatabase>();
    container.Bind<IBall>().To<Ball>();
    container.Bind<IAction>().To<GetFromDeckAction>();
    container.Bind<IAction>().To<InterceptionAction>();
    container.Bind<IAction>().To<PassAction>();
    container.Bind<IAction>().To<PressureAction>();
    container.Bind<IAction>().To<ShootAction>();
    container.Bind<IAction>().To<SwapAction>();
    container.Bind<ISuccess>().To<Success>();
    var controller = container.Get<ConsoleController>();
    controller.Loop();
		
	

Тестирование


Объекты тестирования

Вся игра построена на Action-ах. Суть тестов - корректное изменение состояния игры после применения Action-а

Метод тестирования:

  1. Создаем несколько юнит-тестов на каждое действие
  2. Исполняем действие на заданных параметрах
  3. Проверяем, что состояние корректно изменилось

Пример теста

			
    [Test]
    public void CheckPassTransition()
    {
        var ball = Parameters.Container.Get<Ball>();
        var players = Parameters.GetPlayers(ball);
        var first = players.Item1;
        var second = players.Item2;
        IAction action = Parameters.Container.Get<PassAction>();
        action.SetUp(Parameters.Container.Get<Game>()
        		    			.AddPlayer(first)
        		    			.AddPlayer(second));

        Assert.True(first.Team.HasBall);
        Assert.AreEqual(first.Team.Ball.Place, ZoneType.MID);

        var parameters = new EnemyParameters(second.Team);

        if (action.Execute(parameters))
            Assert.AreEqual(first.Team.Ball.Place, ZoneType.ATT);
        else
        {
            Assert.True(second.Team.HasBall);
            Assert.AreEqual(second.Team.Ball.Place, ZoneType.MID);
        }
		

Общий подход к написанию тестов в итоге такой:

  1. Проинициализировать игру
  2. Взять интересующее действие
  3. Задать параметры
  4. Проверять то, что захочется: результат исполнения, счет, success

Устойчивость к ошибкам пользовательского ввода

Невозможно привести игру в некорректное состояние - у Action есть две проверки:

  • Доступно ли действие

    public abstract bool IsAvailable { get; }
    
  • Верные ли параметры

    public abstract bool AreSuitable(T parameters);
    

Некорректный ввод обрабатывается и производится новая попытка ввода