相互依赖的信号

时间:2014-03-07 21:49:59

标签: elm

裸问:

有没有办法在Elm中定义一对相互依赖的信号?

序言:

我正在尝试编写一个小型Cookie-clicker风格的浏览器游戏,其中玩家正在收集资源,然后花费他们购买自动资源收集结构,这些结构在购买时会变得更加昂贵。这意味着三个相关信号:gathered(玩家收集了多少资源),spent(玩家已经花费了多少资源)和cost(升级费用多少)。

这是一个实现:

module Test where

import Mouse
import Time

port gather : Signal Bool
port build : Signal String

costIncrement = constant 50
cost = foldp (+) 0 <| keepWhen canAfford 0 <| sampleOn build costIncrement
nextCost = lift2 (+) cost costIncrement

spent = foldp (+) 0 <| merges [ sampleOn build cost ]

gathered = foldp (+) 0 <| merges [ sampleOn gather <| constant 1, sampleOn tick tickIncrement ]

balance = lift round <| lift2 (-) gathered spent

canAfford = lift2 (>) balance <| lift round nextCost

tickIncrement = foldp (+) 0 <| sampleOn cost <| constant 0.01
tick = sampleOn (every Time.millisecond) <| constant True

main = lift (flow down) <| combine [ lift asText balance, lift asText canAfford, lift asText spent, lift asText gathered, lift asText nextCost ]

这个编译很好,但是当我将它嵌入一个HTML文件中时,连接了相应的按钮以将消息发送到上面的相应端口,我收到错误

s2 is undefined
    Open the developer console for more details.

问题似乎是写的cost取决于canAfford,这取决于balance,这取决于spent,这取决于cost再次。

如果我修改成本行

...
cost = foldp (+) 0 <| sampleOn build costIncrement
...

它开始按预期工作(除了玩家被允许花费在负资源上,这是我想避免的。)

有什么想法吗?

1 个答案:

答案 0 :(得分:20)

回答你的问题

,Elm中没有通用的方法来定义相互递归的信号 问题在于Elm中的Signal必须始终具有值的约束。如果cost的定义需要canAffordcanAfford定义为cost,则问题在于从何处开始解析信号的初始值。当您考虑相互递归信号时,这是一个难以解决的问题。

相互递归信号与过去的信号值有关。 foldp构造允许您指定相对于一个点的相互递归信号的等价物。通过将foldp作为初始值的显式参数来解决初始值问题的解决方案。但约束是foldp只接受纯函数。

这个问题很难以一种不需要任何先验知识的方式清楚地解释。所以这是另一种解释,基于我对你的代码做的图表。

signal graph of the code given by the OP

花点时间找到代码和图表之间的联系(请注意,我遗漏了main以简化图表)。 foldp是一个回环的节点,sampleOn有一个闪电等等。(我将sampleOn重写为always的常量信号。有问题的部分是红线,在canAfford的定义中使用cost 如您所见,基本foldp具有带基值的简单循环。实现这个比你的任意循环更容易。

我希望你现在明白这个问题。限制在榆树,这不是你的错 我正在解决Elm中的这个限制,尽管这需要一些时间。

解决您的问题

虽然命名信号并与之合作可能会很好,但在Elm中实现游戏时,使用different programming style通常会有所帮助。链接文章中的想法归结为将代码拆分为:

  1. 输入:MouseTime和您的案例中的端口。
  2. 模型:游戏状态,在您的情况下为costbalancecanAffordspentgathered等。
  3. 更新:游戏的更新功能,你可以用较小的更新功能组成这些。这些应尽可能函数。
  4. 查看:查看模型的代码。
  5. 使用main = view <~ foldp update modelStartValues inputs之类的东西将它们组合在一起。

    特别是,我会这样写:

    import Mouse
    import Time
    
    -- Constants
    costInc      = 50
    tickIncStep  = 0.01
    gatherAmount = 1
    
    -- Inputs
    port gather : Signal Bool
    port build : Signal String
    
    tick = (always True) <~ (every Time.millisecond)
    
    data Input = Build String | Gather Bool | Tick Bool
    
    inputs = merges [ Build  <~ build
                    , Gather <~ gather
                    , Tick   <~ tick
                    ]
    
    -- Model
    
    type GameState = { cost          : Float
                     , spent         : Float
                     , gathered      : Float
                     , tickIncrement : Float
                     }
    
    gameState = GameState 0 0 0 0
    
    -- Update
    
    balance {gathered, spent} = round (gathered - spent)
    nextCost {cost} = cost + costInc
    canAfford gameSt = balance gameSt > round (nextCost gameSt)
    
    newCost input gameSt =
      case input of
        Build _ -> 
          if canAfford gameSt
            then gameSt.cost + costInc
            else gameSt.cost
        _ -> gameSt.cost
    
    newSpent input {spent, cost} = 
      case input of
        Build _ -> spent + cost
        _ -> spent
    
    newGathered input {gathered, tickIncrement} = 
      case input of
        Gather _ -> gathered + gatherAmount
        Tick   _ -> gathered + tickIncrement
        _ -> gathered
    
    newTickIncrement input {tickIncrement} =
      case input of
        Tick _ -> tickIncrement + tickIncStep
        _ -> tickIncrement
    
    update input gameSt = GameState (newCost          input gameSt)
                                    (newSpent         input gameSt)
                                    (newGathered      input gameSt)
                                    (newTickIncrement input gameSt)
    
    -- View
    view gameSt = 
      flow down <| 
        map ((|>) gameSt)
          [ asText . balance
          , asText . canAfford
          , asText . .spent
          , asText . .gathered
          , asText . nextCost ]
    
    -- Main
    
    main = view <~ foldp update gameState inputs