裸问:
有没有办法在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
...
它开始按预期工作(除了玩家被允许花费在负资源上,这是我想避免的。)
有什么想法吗?
答案 0 :(得分:20)
否,Elm中没有通用的方法来定义相互递归的信号
问题在于Elm中的Signal
必须始终具有值的约束。如果cost
的定义需要canAfford
,canAfford
定义为cost
,则问题在于从何处开始解析信号的初始值。当您考虑相互递归信号时,这是一个难以解决的问题。
相互递归信号与过去的信号值有关。 foldp
构造允许您指定相对于一个点的相互递归信号的等价物。通过将foldp
作为初始值的显式参数来解决初始值问题的解决方案。但约束是foldp
只接受纯函数。
这个问题很难以一种不需要任何先验知识的方式清楚地解释。所以这是另一种解释,基于我对你的代码做的图表。
花点时间找到代码和图表之间的联系(请注意,我遗漏了main
以简化图表)。 foldp
是一个回环的节点,sampleOn
有一个闪电等等。(我将sampleOn
重写为always
的常量信号。有问题的部分是红线,在canAfford
的定义中使用cost
如您所见,基本foldp
具有带基值的简单循环。实现这个比你的任意循环更容易。
我希望你现在明白这个问题。限制在榆树,这不是你的错 我正在解决Elm中的这个限制,尽管这需要一些时间。
虽然命名信号并与之合作可能会很好,但在Elm中实现游戏时,使用different programming style通常会有所帮助。链接文章中的想法归结为将代码拆分为:
Mouse
,Time
和您的案例中的端口。cost
,balance
,canAfford
,spent
,gathered
等。使用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