7

赤裸裸的问题:

有没有办法在 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,取决于balancespent取决于 ,cost再次取决于。

如果我修改成本线使得

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

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

有任何想法吗?

4

1 回答 1

20

回答你的赤裸裸的问题

,Elm 中没有通用的方法来定义相互递归的信号。
问题在于SignalElm 中的 a 必须始终具有值的约束。如果定义costrequires canAffordbutcanAfford是根据 定义的cost,那么问题是从哪里开始解析信号的初始值。当您根据相互递归的信号进行思考时,这是一个难以解决的问题。

相互递归的信号与信号的过去值有关。该foldp构造允许您指定相互递归信号的等价物直到一个点。初始值问题的解决方案是通过有一个明确的参数来解决的,foldp即初始值。但约束是foldp只需要纯函数。

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

OP给出的代码的信号图

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

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

解决您的问题

尽管命名信号并使用它们可能很好,但在 Elm 中实现游戏时,使用不同的编程风格通常会有所帮助。链接文章中的想法归结为将您的代码拆分为:

  1. 输入:MouseTime您的情况下的端口。
  2. 模型:游戏的状态,在你的情况下cost,,,,,等。balancecanAffordspentgathered
  3. 更新:游戏的更新功能,您可以将它们组合成更小的更新功能。这些应该尽可能是函数。
  4. 查看:查看模型的代码。

使用类似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
于 2014-03-09T15:56:19.960 回答