Дизайн интерфейса абстракция - PullRequest
7 голосов
/ 24 апреля 2011

В настоящее время я пытаюсь написать небольшую игровую программу (Skat) в качестве хобби-проекта.Скат - это игра, в которой два игрока играют против одного игрока.Поскольку есть разные виды игроков (локальный проигрыватель, сетевой игрок, компьютер и т. Д.), Я хотел бы абстрагировать интерфейс от проигрывателя.

Моя основная идея - использовать класс типов Player, который определяет все виды вещей, которые игрок должен делать и знать (разыгрывая карту, получая уведомление о том, кто выиграл трюк и т. Д.).Затем вся игра выполняется функцией playSkat :: (Player a, Player b, Player c) => a -> b -> c -> IO (), где a, b и c могут быть разными игроками.Затем игрок может реагировать определенным способом реализации.Локальный игрок может получить какое-то сообщение на своем терминале, сетевой игрок может отправить некоторую информацию по сети, а компьютерный игрок может рассчитать новую стратегию.

Поскольку игрок может захотеть сделать какой-нибудь ввод-вывод и определенно захочетиметь какое-то государство для отслеживания личных вещей, оно должно жить в какой-то монаде.Поэтому я подумал об определении класса Player следующим образом:

class Player p where
  playCard :: [Card] -> p -> IO (Card,p)
  notifyFoo :: Event -> p -> IO p
  ...

Этот шаблон, похоже, очень похож на преобразователь состояния, но я не уверен, как с ним справиться.Если бы я написал его как дополнительный монад-трансформер поверх IO, у меня было три разных монады в конце дня.Как я могу написать эту абстракцию хорошим способом?

Чтобы уточнить, что мне нужно, вот как должен выглядеть обычный поток управления:
При игре трюк первый игрок играет карту,затем второй и, наконец, третий.Для этого логике необходимо выполнить функцию playCard трижды для каждого игрока.После этого логика решает, какой игрок выиграл трюк, и отправляет информацию о том, кто выиграл, всем игрокам.

Ответы [ 4 ]

6 голосов
/ 24 апреля 2011

Прежде всего, имейте в виду, что основная цель классов типов состоит в том, чтобы разрешить перегрузку функций, то есть, что вы можете использовать одну функцию для разных типов.Вы на самом деле этого не требуете, так что вам лучше иметь тип записи в соответствии с

data Player = Player { playCard :: [Card] -> IO (Card, Player), ... }


Во-вторых, проблема некоторых игроков, нуждающихся в IO, а некоторых не может быть решена с помощьюобычай монады.Я написал соответствующий пример кода для игры TicTacToe, которая является частью моего операционного пакета.

4 голосов
/ 24 апреля 2011

Гораздо лучшим дизайном было бы отсутствие ввода-вывода как части любого типа проигрывателя.Почему игрок должен сделать IO?Игроку, вероятно, необходимо получить информацию и отправить информацию.Сделайте интерфейс, который отражает это.Если / когда необходим ввод-вывод, он будет выполнен playSkat.

Если вы сделаете это, у вас могут быть другие версии playSkat, которые не выполняют ввод-вывод, и вы также можете гораздо проще тестировать своих игроков,они взаимодействуют только через методы класса, а не через IO.

1 голос
/ 09 июля 2011

Вот так я наконец и разработал абстракцию:

Все, что движок может пожелать от одного из игроков, закодировано в большой GADT под названием Message, потому что мне не всегда нужен ответ. Параметром GADT является запрошенное возвращаемое значение:

data Message answer where
  ReceiveHand :: [Card] -> Message ()
  RequestBid  :: Message (Maybe Int)
  HoldsBid    :: Int -> Message Bool
  ...

Различные типы игроков абстрагируются по классу типов с одной единственной функцией playerMessage, которая позволяет движку отправлять сообщение игроку и запрашивать ответ. Ответ заключен в Either, поэтому игрок может вернуть соответствующую ошибку, если невозможно вернуть ответ (например, если функция не реализована или сеть находится в забастовке и т. Д.). Параметр p представляет собой запись состояния, в которой проигрыватель может хранить личные данные и конфигурацию. Игрок абстрагируется от монады m, чтобы позволить некоторым игрокам использовать IO, в то время как другим это не нужно:

class Monad m => Player p m | p -> m where
  playerMessage :: Message answer -> p -> m (Either String answer,p)

Редактировать

Я задал другой вопрос , потому что мне не нравилось вводить контексты снова и снова, поэтому я, наконец, изменил код для уточнения класса типов Player. Игроки сами не имеют состояния, но они могут использовать частично прикладные функции для имитации этого. Подробности смотрите в другом вопросе.

0 голосов
/ 24 апреля 2011

Даже не подумал об этом, но, возможно, все же стоит подумать.Здесь я заметил, что у вас есть и p вход, и p выход в функциях класса типов, я догадался, что это означает "обновление" p.Какая-то государственная монада.

class (MonadIO m, MonadState p m) => Player p where
  playCard :: [Card] -> m Card
  notifyFoo :: Event -> m ()

Опять же, это просто стихийная мысль.Я не гарантирую, что он будет мудрым (или даже компилируемым).

...