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

时间:2011-03-14 00:16:06

标签: haskell state

我正在为在线游戏编写客户端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

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

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

data SessionState = SSLoggedOut LoggedOut
                  | SSReady Ready
                  | SSNotReady NotReady

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

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

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

1 个答案:

答案 0 :(得分:3)

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

  1. 是的,我认为这是一个合理的模式。
  2. 是的,TVar是实现此目标的最佳方式。我假设您了解组合器atomically
  3. 是的,afaik。见下一段。
  4. 如果您希望类型检查同步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}
    

    以下是一些可能用于从一种状态移动到另一种状态的简单函数。我试图给出使用警卫和使用模式匹配的例子;你可以选择你喜欢哪种风格或者像我一样混音:

    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"
    

    使用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)
    

    这种方法通过编译器具有较少的类型安全性,但仍然可以非常易读,并且作为额外的好处,灵活。这是我在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"]}