-
Notifications
You must be signed in to change notification settings - Fork 44
Description
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:
- Player, which packages a
Snake.t(glorified deque of (row,col) coords), direction, score, and status - Apple, which packages a
Apple.t(just a (row, col) record)
On every tick:
- The snake moves
- For every apple it eats, the score increases, and the snake's "left_to_grow" param is increased
- Every eatten apple is respawned
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's Move action takes the other elements on the board as parameters. For a game with 1 snake and 1 apple, heres the signature for Player actions:
module Action : sig
type t =
| Restart
| Move of (Apple.t * (Apple.Action.t -> unit Effect.t))
| Change_direction of Direction.t
endand for Apple actions:
module Action = struct
type t =
| Spawn of Position.t list
| Eatten of Position.t list
[@@deriving sexp]
endOn a player's tick, if the snake has eatten the apple, it will use schedule_event to dispatch a Eatten 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 use Effect.Many to combine all these actions into one effect on tick, the ith player could eat an apple causing it to respawn, but the i+1th player would still see the old position of the apple, and of the snakes that have already moved:
(* Tick logic *)
let%sub () =
let%sub clock_effect =
let%arr player1_inject = player1_inject
and player2_inject = player2_inject
and game_elements = game_elements in
Effect.Many
[ player1_inject (Move game_elements); player2_inject (Move game_elements) ]
in
Bonsai.Clock.every [%here] (Time_ns.Span.of_sec 0.25) clock_effect
inThis means that the apple could spawn right under where the i+1th snake is about to move, but the i+1th 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:
- A
Game_element.ttype represents allSnake.ts andApple.ts in the game, and comes with a helper to get all occupied positions on the board. I had to separateAppleandApple_stateinto separate files to avoid dependency cycles. - A
Game_element.tis computed incrementally from allPlayer_state.computations andApple_state.computations. Apple_statenow has aTickaction, which takes thatGame_elements.tas 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'sRestartandMoveactions take thatGame_elements.tas input. That way, snakes will not spawn on an existing game element, and will be aware of everything on the board.Apple_state'sSpawnandTickactions do the same
My 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 that Game_elements.t is computed incrementally, the apply_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 from Game_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:
- I want to leave state machine inputs open for
rowsandcols, which could be controlled by a form. Having a 3-tuple of inputs would be messy. - With this chain approach, the
ith apple could never respawn on top of thei-1th apple. However, there's nothing stopping thei+1th apple from respawning on top of theith apple. I suppose we could use this for full board resets and pass aroundGame_elements.tthrough 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:
val many_value : Effect.t Value.t list -> Effect.t Value.tthat 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 how Value.t is currently implemented though.
I would also appreciate any suggestions if there's a cleaner / more idiomatic way of doing this. Thanks!