Общий дизайн для консоли и графического интерфейса

Я разрабатываю небольшую игру для собственного удовольствия и тренировки. Реальная идентичность игры совершенно не имеет отношения к моему фактическому вопросу, предположим, что это Mastermind игра (на самом деле это:)

Моя реальная цель - иметь интерфейс IPlayer, который будет использоваться для любого игрока: компьютер или человек, консоль или gui, локальная или сетевая. Я также намерен иметь GameController, который будет иметь дело только с двумя IPlayer s.

интерфейс IPlayer будет выглядеть примерно так:

class IPlayer
{
public:
    //dtor
    virtual ~IPlayer()
    {
    }
    //call this function before the game starts. In subclasses,
    //the overriders can, for example, generate and store the combination.
    virtual void PrepareForNewGame() = 0;
    //make the current guess
    virtual Combination MakeAGuess() = 0;
    //return false if lie is detected.
    virtual bool ProcessResult(Combination const &, Result const &) = 0;
    //Answer to opponent guess
    virtual Result AnswerToOpponentsGuess(Combination const&) = 0;
};

Класс GameController сделает следующее:

IPlayer* pPlayer1 = PlayerFactory::CreateHumanPlayer();
IPlayer* pPlayer1 = PlayerFactory::CreateCPUPlayer();

pPlayer1->PrepareForNewGame();
pPlayer2->PrepareForNewGame();

while(no_winner)
{
   Guess g = pPlayer1->MakeAguess();
   Result r = pPlayer2->AnswerToOpponentsGuess(g);
   bool player2HasLied = ! pPlayer1->ProcessResult(g, r);
   etc. 
   etc.
}   

В этом дизайне я готов сделать класс GameController неизменным, то есть я нахожу в нем правильные правила игры, и ничего больше, поэтому, поскольку сама игра установлена, этот класс не должен меняться. Для консольной игры этот дизайн будет работать отлично. Я бы имел HumanPlayer, который в своем методе MakeAGuess читал бы Combination со стандартного ввода и CPUPlayer, который каким-то образом произвольно генерировал бы его и т.д.

Теперь вот моя проблема: интерфейс IPlayer, а также класс GameController являются синхронными по своей природе. Я не могу себе представить, как реализовать вариант GUI игры с тем же GameController, когда методу MakeAGuess GUIHumanPlayer придется ждать, например, некоторых движений мыши и кликов. Конечно, я мог бы запустить новый поток, который будет ждать ввода пользователя, в то время как основной поток будет блокироваться, чтобы имитировать синхронный IO, но почему-то эта идея меня отвратительна. Или, альтернативно, я мог бы спроектировать как контроллер, так и плейер для асинхронности. В этом случае для консольной игры мне придется подражать асинхронности, которая кажется более простой, чем первая версия.

Не могли бы вы прокомментировать мой дизайн и мою озабоченность по поводу выбора синхронного или асинхронного дизайна? Кроме того, я чувствую, что больше ответственности отношусь к классу игроков, чем к классу GameController. Etc и т.д.

Спасибо вам заблаговременно.

P.S. Мне не нравится название моего вопроса. Не стесняйтесь редактировать его.

Ответ 1

Вместо использования возвращаемых значений различных методов IPlayer рассмотрите вопрос о введении класса наблюдателя для объектов IPlayer, например:

class IPlayerObserver
{
public:
  virtual ~IPlayerObserver() { }
  virtual void guessMade( Combination c ) = 0;
  // ...
};

class IPlayer
{
public:
  virtual ~IPlayer() { }
  virtual void setObserver( IPlayerObserver *observer ) = 0;
  // ...
};

Методы IPlayer должны затем вызвать соответствующие методы установленного IPlayerObserver вместо того, чтобы возвращать значение, как в:

void HumanPlayer::makeAGuess() {
  // get input from human
  Combination c;
  c = ...;
  m_observer->guessMade( c );
}

Затем ваш класс GameController мог бы реализовать IPlayerObserver, чтобы он получал уведомление, когда игрок делал что-то интересное, например, делая предположение.

С этим дизайном он отлично подходит, если все методы IPlayer являются асинхронными. На самом деле, как и следовало ожидать, все они возвращаются void!. Ваш игровой контроллер вызывает makeAGuess на активном проигрывателе (это может немедленно вычислить результат, или он может сделать некоторый сетевой IO для многопользовательских игр, или он будет ждать, когда GUI что-то сделает), и всякий раз, когда игрок делал свой выбор, игровой контроллер может быть уверен, что будет вызван метод guessMade. Furthemore, объекты игрока все еще ничего не знают о игровом контроллере. Они просто имеют дело с непрозрачным интерфейсом "IPlayerObserver".

Ответ 2

Единственное, что делает этот интерфейс для графического интерфейса по сравнению с консолью, заключается в том, что ваш графический интерфейс управляется событиями. Эти события происходят в потоке графического интерфейса пользователя, и поэтому, если вы размещаете код игры в потоке графического интерфейса пользователя, у вас есть проблема: ваш звонок, чтобы игрок сделал шаг, блокирует поток графического интерфейса, и это означает, что вы не можете получить любые события до тех пор, пока этот вызов не вернется. [EDIT: Вставить следующее предложение.] Но вызов не может вернуться, пока не получит событие. Итак, вы зашли в тупик.

Эта проблема исчезнет, ​​если вы просто разместите код игры в другом потоке. Вам все равно нужно синхронизировать потоки, поэтому MakeAGuess() не возвращается до готовности, но это, безусловно, возможно.

Если вы хотите сохранить все однопоточность, вы можете рассмотреть другую модель. Игра может уведомить Игроков об их повороте с событием, но оставить его игрокам для начала операций в игре.