接口抽象设计

发布于 2024-11-03 15:35:02 字数 877 浏览 4 评论 0原文

目前,我尝试编写一个小游戏程序(Skat)作为业余爱好项目。 Skat 是一种夺牌游戏,由两名玩家对抗一名玩家。由于有不同类型的播放器(本地播放器、网络播放器、计算机等),我想抽象播放器的接口。

我的基本想法是使用类型类Player,它定义了玩家必须做和知道的所有事情(打牌、获得有关谁赢得了技巧的通知等)。然后,整个游戏就由函数 playSkat :: (Player a, Player b, Player c) => 完成。一个-> b-> c-> IO () 其中 abc 可能是不同类型的玩家。然后,玩家可能会以实现定义的方式做出反应。本地玩家会在他的终端上收到一些消息,网络玩家可能会通过网络发送一些信息,而计算机玩家可能会计算出新的策略。

因为玩家可能想要做一些 IO 并且肯定想要有某种状态来跟踪私人事物,所以它必须生活在某种 Monad 中。所以我考虑像这样定义 Player 类:

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

这种模式似乎与状态转换器非常相似,但我不确定如何处理它。如果我将其写为 IO 之上的额外 monad-transformer,那么最终我会得到三个不同的 monad。我怎样才能很好地写出这个抽象呢?

为了澄清我需要什么,通常的控制流应该是这样的:
玩戏时,第一个玩家打出一张牌,然后是第二个,最后是第三个。为此,逻辑需要为每个玩家执行三次 playCard 函数。然后,逻辑决定哪位玩家赢得该墩并将获胜的信息发送给所有玩家。

Currently, I try to write a small game program (Skat) as a hobby project. Skat is a trick-taking game were two players play against a single player. As there are different kinds of players (lokal player, network player, computer, etc.), I wanted to abstract the interface to the player.

My basic idea is to use a typeclass Player, that defines all kind of things, a player have to do and to know (playing a card, get notified about who won the trick, etc). Then, the whole game is just done by a function playSkat :: (Player a, Player b, Player c) => a -> b -> c -> IO () where a, b and c might be different kinds of players. A player might then react in a implementation defined way. A lokal player would get some message on his terminal, a network player might send some information over the network and a computer player might calculate a new strategy.

Because the player might want to do some IO and definitly want to have some kind of state to track private things, it has to live in some kind of Monad. So I thought about defining the Player class like this:

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

This pattern seems to be quite similar to a state transformer, but I am not shure how to handle it. If I would write it as an extra monad-transformer on top of IO, I had three different monads at the end of the day. How can I write this abstraction in a good way?

To clarify, what I need, here is how a usual control flow should look like:
When playing a trick, the first player plays a card, then the second, and finally the third. To do this, the logic needs to execute the function playCard trice for each player. Afterwards, the logic decides, which player wins the trick and sends the information who won to all players.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

夕色琉璃 2024-11-10 15:35:02

首先,请记住类型类的主要目的是允许函数重载,即您可以在不同类型上使用单个函数。您实际上并不需要这样做,因此您最好使用类似于

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

的记录类型
其次,有的玩家需要IO,有的玩家不需要IO的问题可以通过自定义monad来解决。我已经为 TicTacToe 游戏编写了相应的示例代码,这是我的操作包。

First of all, keep in mind that the main purpose of type classes is to permit overloading of functions, i.e. that you can use a single function at different types. You don't really require that, so you are better off with a record type along the lines of

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

Second, the problem of some players needing IO and some not can be solved with a custom monad. I have written corresponding example code for a game of TicTacToe, which is part of my operational package.

一花一树开 2024-11-10 15:35:02

更好的设计是不将 IO 作为任何 Player 类型的一部分。
为什么玩家需要做IO?玩家可能需要获取信息并发送信息。制作一个反映这一点的界面。如果/当需要 IO 时,它将由 playSkat 执行。

如果你这样做,你就可以拥有不执行任何 IO 的其他版本的 playSkat,并且你还可以更轻松地测试你的玩家,因为他们只通过类方法而不是通过 IO 进行交互。

A much better design would be not to have IO as part of any Player type.
Why does the player need to do IO? The player probably needs to get information and send information. Make an interface that reflects that. If/when IO is needed it will be performed by playSkat.

If you do it this what you can have other versions of playSkat that don't do any IO and you can also test your players much more easily since they only interact via the class methods and not through IO.

别闹i 2024-11-10 15:35:02

这就是我最终设计抽象的方式:

引擎可能想要从某个玩家那里得到的所有东西都被编码在一个名为 Message 的大 GADT 中,因为我并不总是需要答案。 GADT 的参数是请求的返回值:

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

不同类型的玩家通过一个类型类进行抽象,该类型类具有一个函数 playerMessage,该函数允许引擎向玩家发送消息并请求答案。答案包含在 Either 中,因此如果无法返回答案(例如,如果功能未实现或网络罢工,则玩家可以返回适当的错误, ETC)。参数p是播放器的状态记录,用于存储私有数据和配置。玩家通过 monad m 进行抽象,以允许某些玩家使用 IO,而其他玩家不需要它:

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

编辑

我问 另一个问题,因为我对一次又一次地输入上下文不满意,所以我最终改变了代码具体化类型类Player。玩家自己没有状态,但他们可以使用部分应用函数来模拟这种状态。详情请参阅另一个问题。

That's how I finally designed the abstraction:

All things the engine may want from one of the players are encoded in a big GADT called Message, because I do not always need an answer. The parameter of the GADT is the requested return value:

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

The different kinds of players are abstracted over a type class with one single function playerMessage that allows the engine to send a message to a player and requests for an answer. The answer is wrapped up in an Either, so the player can return an appropriate error if it is not possible to return an answer (for instance, if the function is not implemented or the network is on strike, etc). The parameter p is a state record for the player to store private data and configuration. The player is abstracted over a monad m to allow some players to use IO while others don't need it:

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

Edit

I asked another Question, because I was not happy with typing the contexts again and again, so I finally changed the code to reify the typeclass Player. The players have no state by them self, but they can use partial applied functions to simulate this. See the other question for details.

姜生凉生 2024-11-10 15:35:02

根本没有考虑过这一点,但也许仍然值得考虑。在这里,我注意到类型类函数中有 p 输入和 p 输出,我猜这意味着那些“更新”p。某种意义上的状态单子。

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

再说一次,这只是一个自发的想法。我不保证它是明智的(甚至是可编译的)。

Haven't at all thought this through, but maybe still worth considering. Here I noticed that you have both p in and p out in the type class functions, I guessed that means those "update" p. A state monad somehow.

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

Again, this is just a spontaneous thought. I don't guarantee it to be wise (or even compilable).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文