Haskell:为GUI建模在线游戏状态

nar*_*thi 5 state haskell

我正在为在线游戏编写客户端UI.它被构造为表示游戏状态的Model模块和View模块,它跟踪当前游戏状态并使用模型转换(即从一个状态到另一个状态的功能)更新它.为了利用静态类型检查,我将状态建模为具有表示共同特征的类型类的不同类型:

class Erring s where errors :: s -> [String]
class WithPlayers s where players :: s -> [String]
class Erring s => LoggedIn s

data LoggedOut = LoggedOut [String] deriving (Eq, Show)
instance Erring LoggedOut where errors (LoggedOut es) = es

data Ready = Ready [String] [String] deriving (Eq, Show)
instance Erring Ready where errors (Ready _ es) = es
instance LoggedIn Ready
instance WithPlayers Ready where players (Ready ps _) = ps

data NotReady = NotReady [String] [String] deriving (Eq, Show)
instance Erring NotReady where errors (NotReady _ es) = es
instance LoggedIn NotReady
instance WithPlayers NotReady where players (NotReady ps _) = ps

-- some transitions:

login :: String -> LoggedOut -> Either Ready LoggedOut
login pwd (LoggedOut es) = 
  if pwd == "password" then Left $ Ready [] es
  else Right $ LoggedOut (es ++ ["incorrect password"])

logout :: LoggedIn s => s -> LoggedOut
logout s = LoggedOut $ errors s
Run Code Online (Sandbox Code Playgroud)

当有许多状态和实例要定义但产生强大的API时,这可能有点单调乏味.

进入视图.为了存储我想要使用的状态TMVar,所以UI线程和来自服务器的线程处理消息都可以执行状态转换.由于每个状态是不同的类型,我创建了一个可以代表每个可能状态的新类型:

data SessionState = SSLoggedOut LoggedOut
                  | SSReady Ready
                  | SSNotReady NotReady
Run Code Online (Sandbox Code Playgroud)

现在可以定义类型的状态引用TMVar SessionState.

现在,这感觉不太对劲.我实际上必须将每个状态定义两次,一次作为类型,另一次作为包装此类型的类型构造函数.所以,问题:

  1. 以这种方式模拟游戏状态是否合理?
  2. 保持状态值是否合理是否需要TMVar不同线程的原子更新或是否有更好的方法来跟踪状态?
  3. 如果TMVar是正确的方法,是否有必要SessionState定义包装器?

Dan*_*ton 3

我花了一分钟才明白为什么数据LoggedIn是类LoggedOut,但是......

  1. 是的,我认为这是一个合理的模型。
  2. 是的,TVar据我所知这是最好的方法。我假设你知道组合器atomically
  3. 是的,据我所知。请参阅下一段。

如果您想要类型检查同步 TMVar,那么您需要定义类型(用于类型检查)和数据包装器(用于 TMVar)。我看不出有什么办法可以解决这个问题;据我所知,TMVar 必须保持相同的类型。(如我错了请纠正我!)

如果是我,我会放弃类型,而使用函数和防护。

data SessionState = Ready {errors :: [String], players :: [String]}
                  | NotReady {errors :: [String], players :: [String]}
                  | LoggedOut {errors :: [String]}
                  deriving (Eq, Show, Ord)

loggedIn :: SessionState -> Bool
loggedIn (LoggedOut _) = False
loggedIn _             = True

ready :: SessionState -> Bool
ready (Ready _ _) = True
ready _           = False

addError :: SessionState -> String -> SessionState
addError s e = s {errors = e:errors s}

addPlayer :: SessionState -> String -> SessionState
addPlayer s@(LoggedOut _) p = addError s $ "Can't add " ++ p ++ " when logged out"
addPlayer s p               = s {players = p:players s}
Run Code Online (Sandbox Code Playgroud)

您可以使用以下一些简单的函数从一种状态转移到另一种状态。我尝试给出使用防护和使用模式匹配的示例;你可以选择你更喜欢的风格,或者像我一样混合使用:

login :: SessionState -> SessionState
login (LoggedOut es) = NotReady es []
login s              = addError s "Can't log in when already logged in"

logout :: SessionState -> SessionState
logout s
    | loggedIn s = LoggedOut $ errors s
    | otherwise  = addError s "Can't log out when not logged in"

enable :: SessionState -> SessionState
enable (NotReady es ps) = Ready es ps
enable s@(LoggedOut _)  = addError s "Can't enable when logged out"
enable s@(Ready _ _ )   = addError s "Can't enable when already ready"

disable :: SessionState -> SessionState
disable s
    | ready s   = NotReady (errors s) (players s)
    | otherwise = addError s "Can't disable when not ready"
Run Code Online (Sandbox Code Playgroud)

还有一个使用该loggedIn函数的愚蠢示例函数:

countPlayers :: SessionState -> (SessionState, Maybe Int)
countPlayers s
    | loggedIn s = (s, Just . length $ players s)
    | otherwise  = (addError s "Can't count players whilst logged out", Nothing)
Run Code Online (Sandbox Code Playgroud)

这种方法通过编译器的类型安全性较低,但仍然具有很强的可读性,而且还有一个额外的好处,那就是灵活。这是我在 ghci 中摆弄的:

*Main> LoggedOut []
LoggedOut {errors = []}
*Main> login it
NotReady {errors = [], players = []}
*Main> enable it
Ready {errors = [], players = []}
*Main> addError it "Illegal somethingorother"
Ready {errors = ["Illegal somethingorother"], players = []}
*Main> logout it
LoggedOut {errors = ["Illegal somethingorother"]}
*Main> disable it
LoggedOut {errors = ["Can't disable when not ready","Illegal somethingorother"]}
Run Code Online (Sandbox Code Playgroud)