Bare question:
Is there a way of defining a pair of signals that depend on each other in Elm?
Preamble:
I'm trying to write a tiny Cookie-clicker-style browser game in which the player is gathering resources, then spending them to purchase autonomous resource-gathering constructs which get more expensive as they're purchased. That implies three relevant signals: gathered
(how much resources the player has gathered), spent
(how much resource the player has already spent) and cost
(how much an upgrade costs).
Here's an implementation:
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 ]
This compiles fine, but when I embed it in an HTML file with the appropriate buttons hooked up to send messages to the appropriate ports above, I get the error
s2 is undefined
Open the developer console for more details.
The problem seems to be that as written, cost
depends on canAfford
, which depends on balance
, which depends on spent
, which depends on cost
again.
If I modify the cost line such that
...
cost = foldp (+) 0 <| sampleOn build costIncrement
...
it starts working as expected (except that the player is allowed to spend into negative resources, which is what I'd like to avoid).
Any ideas?
No, there is no general way in Elm to define mutually recursive signals.
The problem lies in the constraint that a Signal
in Elm must always have a value. If the definition of cost
requires canAfford
but canAfford
is defined in terms of cost
, the problem is where to start with resolving the initial value of the signal. This is a tough problem to solve when you think in terms of mutually recursive signals.
Mutually recursive signals have everything to do with past values of signals. The foldp
construct allows you to specify the equivalent of mutually recursive signals up to a point. The solution to the initial value problem is solved by having an explicit argument to foldp
that is the initial value. But the constraint is that foldp
only takes pure functions.
This problem is hard to clearly explain in a way that doesn't require any prior knowledge. So here's another explanation, based on a diagram I made of your code.
Take your time to find the connections between the code and the diagram (note that I left out main
to simplify the graph). A foldp
is a node with a loop back, sampleOn
has a lightning bolt etc. (I rewrote sampleOn
on a constant signal to always
). The problematic part is the red line going up, using canAfford
in the definition of cost
.
As you can see, a basic foldp
has a simple loop with a base value. Implementing this is easier than arbitrary loop-back like yours.
I hope you understand the problem now. The limitation is in Elm, it's not your fault.
I'm resolving this limitation in Elm although it will take some time to do so.
Although it can be nice to name signals and work with those, when implementing games in Elm it usually helps to use a different programming style. The idea in the linked article comes down to splitting your code up in:
Mouse
, Time
and ports in your case.cost
, balance
, canAfford
, spent
, gathered
etc.Tie it all together by using something like main = view <~ foldp update modelStartValues inputs
.
In particular, I would write it like:
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With