Open askvortsov1 opened 1 year ago
I don't have a good intuition for what the best way to solve your problem would be, but these functions might be useful:
wrap
Wrap allows you to build a "coordinating" state machine that has access to the result of the computation that its coordinating. The interesting thing is that the coordinated components can communicate with - and see the model of - the coordinator.actor*
Actors can respond to actions by returning values, allowing them to communicate more naturally with other components.with_inject_fixed_point
This is actually just a small wrapper around wrap
, but makes it easy to use an effect that is computed somewhere else.I think I've found the solution I was looking for (at least on v0.15, but I don't see why it shouldn't carry over)!
To simplify my excessively long initial post, I wanted to:
Player_state
and Apple_state
computationsSnake.t
s and Apple.t
s from thatSnake.t
s and Apple.t
s available to every Player_state
and Apple_state
' when they:
In particular, as each element spawns / ticks and changes its state, the worldview provided to all further spawns / ticks should be updated.
I could probably implement this with wrap
. The coordinated components would be the individual Player/Apple computations, and the coordinator would be a Game_elements.t
. The downside is that coordinated components would need to explicitly tell the coordinator when their state updates, so recalculating Game_elements.t
would be an explicit update, instead of a declarative, equational derivation.
I don't think it's possible in Bonsai to sequentially execute a list of lazily evaluated unit Effect.t Value.t
s. However, this case is helped greatly by the fact that the actions in question are all Move of Game_elements.t
and Tick of Game_elements.t
, or Restart of Game_elements.t
and Spawn of Game_elements.t
. I could separate the "chain effects" part, and the "incremental dependency" part. Here's what I came up with:
let chain_scheduler
: type a. a Value.t -> ((a -> unit Ui_effect.t) list -> unit Ui_effect.t) Computation.t
=
fun input ->
let module Action = struct
type t = Run of (a -> unit Effect.t) list [@@deriving sexp]
end
in
let apply_action ~inject ~schedule_event input _model (Action.Run effect_fns) =
match effect_fns with
| effect_fn :: dependents ->
schedule_event (Effect.Many [ effect_fn input; inject (Action.Run dependents) ])
| [] -> ()
in
let open Bonsai.Let_syntax in
let%sub (), inject =
Bonsai.state_machine1
[%here]
(module Unit)
(module Action)
~default_model:()
~apply_action
input
in
let%arr inject = inject in
fun effects -> inject (Action.Run effects)
;;
Then, to use it:
let%sub player1, player1_inject = Player_state.computation ~rows ~cols ~color:"green" in
let%sub player2, player2_inject = Player_state.computation ~rows ~cols ~color:"blue" in
let%sub apple, apple_inject = Apple_state.computation ~rows ~cols in
let%sub apple2, apple2_inject = Apple_state.computation ~rows ~cols in
let%sub game_elements =
let%arr player1 = player1
and player2 = player2
and apple = apple
and apple2 = apple2 in
{ Game_elements.snakes = Player_state.Model.snakes [ player1; player2 ]
; apples = Apple_state.Model.apples [ apple; apple2 ]
}
in
let%sub scheduler = chain_scheduler game_elements in
(* Tick logic *)
let%sub () =
let%sub clock_effect =
let%arr player1_inject = player1_inject
and player2_inject = player2_inject
and apple_inject = apple_inject
and apple2_inject = apple2_inject
and scheduler = scheduler in
scheduler
[ (fun g -> player1_inject (Move g))
; (fun g -> player2_inject (Move g))
; (fun g -> apple_inject (Tick g))
; (fun g -> apple2_inject (Tick g))
]
in
Bonsai.Clock.every [%here] (Time_ns.Span.of_sec 2.) clock_effect
in
The reset effect is defined similarly.
And exactly as desired, every state machine gets an up-to-date view of the world when handling its actions. It's also really simple to understand and maintain: every state machine just needs to decide how it should act based on the world state, and doesn't need to be aware of / communicate with other state machines.
Even better, I think this might be a general solution for sequentially composing effects that depend on an incremental input, which might change as the effects are executed. I feel like this might be useful for enabling / simplifying other applications, although I haven't worked enough with production Bonsai code to have any particular ones in mind.
module Continuation = Effect.Define1 (struct
module Action = struct
type 'a t = ('a -> unit Ui_effect.t) -> unit Ui_effect.t
end
let handle action ~on_response =
on_response
|> Effect.of_sync_fun
|> action
|> Effect.Expert.handle_non_dom_event_exn
;;
end)
val with_yoink
: ('a Effect.t Value.t -> 'a Computation.t)
-> 'a Computation.t
let with_yoink f =
Bonsai.wrap
()
~default_model:()
~apply_action:(fun ctx result () cb ->
Bonsai.Apply_action_context.schedule_event ctx (cb result))
~f:(fun _model inject ->
let%sub result = Bonsai.pure Continuation.inject inject in
f result)
;;
Apparently this combinator already exists and is called Bonsai_extra.with_self_effect
...
Hi! I'm working on a
snake
tutorial, and ran into a design challenge. I was wondering if you had any suggestions for a clean way to implement this in Bonsai.The premise of my design is that I have 2 "lower-level" stateful computations:
Snake.t
(glorified deque of (row,col) coords), direction, score, and statusApple.t
(just a (row, col) record)On every tick:
Additionally, on game restart, every snake and apple is randomly placed on the game grid.
I've attempted several implementations, but none are as clean as I'd like.
Snakes Are in Charge
This is the current code at this commit.
Here, the
Player
'sMove
action takes the other elements on the board as parameters. For a game with 1 snake and 1 apple, heres the signature forPlayer
actions:and for
Apple
actions:On a player's tick, if the snake has eatten the apple, it will use
schedule_event
to dispatch aEatten
action to the apple, with its current position as an argument.This becomes messy fast when we add n snakes and m apples. Each player needs to receive all the snakes and apples (and their
inject
functions!). But because we need to useEffect.Many
to combine all these actions into one effect on tick, thei
th player could eat an apple causing it to respawn, but thei+1
th player would still see the old position of the apple, and of the snakes that have already moved:This means that the apple could spawn right under where the
i+1
th snake is about to move, but thei+1
th snake would never know. This also doesn't fix the issue of elements spawning on top of each other when the entire board is reset.Apples Take The Reins (of their fate)
I tried to fix the generalized snake game in this commit. Essentially:
Game_element.t
type represents allSnake.t
s andApple.t
s in the game, and comes with a helper to get all occupied positions on the board. I had to separateApple
andApple_state
into separate files to avoid dependency cycles.Game_element.t
is computed incrementally from allPlayer_state.computation
s andApple_state.computation
s.Apple_state
now has aTick
action, which takes thatGame_elements.t
as input. Apples are responsible for checking if they have been eatten, and for respawning themselves. Players will still separately grow/increase score when they eat an apple.Player_state
'sRestart
andMove
actions take thatGame_elements.t
as input. That way, snakes will not spawn on an existing game element, and will be aware of everything on the board.Apple_state
'sSpawn
andTick
actions do the sameMy hope was that each snake would move, check for eatting itself/OOB, and then grow if it ate an apple. Then, all apples would respawn if eatten. Because everything takes
Game_elements.t
as input, and thatGame_elements.t
is computed incrementally, theapply_action
function would always have an up-to-date view of the world.Sadly, I can't figure out a way to do this with Bonsai's current API. If I want to dispatch multiple effects at once, I need to combine them with
Effect.Many
. But that means that all effects are calculated incrementally fromGame_elements.t
pre-tick, and will not update in response to previous events being evaluated.I tried
Bonsai.lazy_
to compute the effects, but that didn't seem to help. I might be using it incorrectly though.Input It Out...
As an alternative to all this, we could instantiate computations of snakes and apples in a chain, with each element taking the positions of all previously instantiated elements as an input. That position input could then be used to control where apples/snakes couldn't spawn.
There's 2 big problems with this:
rows
andcols
, which could be controlled by a form. Having a 3-tuple of inputs would be messy.i
th apple could never respawn on top of thei-1
th apple. However, there's nothing stopping thei+1
th apple from respawning on top of thei
th apple. I suppose we could use this for full board resets and pass aroundGame_elements.t
through actions for apple respawns in-game, but that feels confusing and messy.Effect.many_value?
The cleanest solution that comes to my mind would be having some:
that isn't just
Value.all
, but rather lazily evaluates each effect when its time to call it. I'm not sure that this would be possible with howValue.t
is currently implemented though.I would also appreciate any suggestions if there's a cleaner / more idiomatic way of doing this. Thanks!