This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2020-12-01
Channels
- # adventofcode (66)
- # announcements (12)
- # aws (8)
- # babashka (28)
- # beginners (160)
- # cider (28)
- # clara (22)
- # clj-kondo (5)
- # cljdoc (40)
- # clojure (129)
- # clojure-australia (2)
- # clojure-europe (24)
- # clojure-gamedev (19)
- # clojure-nl (5)
- # clojure-norway (15)
- # clojure-sanfrancisco (25)
- # clojure-seattle (2)
- # clojure-spec (13)
- # clojure-uk (29)
- # clojurescript (14)
- # cryogen (5)
- # cursive (7)
- # data-science (1)
- # datascript (5)
- # datomic (8)
- # deps-new (5)
- # emacs (19)
- # events (8)
- # fulcro (32)
- # graalvm (7)
- # helix (9)
- # kaocha (3)
- # lambdaisland (1)
- # london-clojurians (4)
- # malli (5)
- # meander (32)
- # off-topic (143)
- # pathom (4)
- # portal (32)
- # re-frame (7)
- # reagent (33)
- # reitit (2)
- # shadow-cljs (5)
- # spacemacs (4)
- # tools-deps (30)
- # vim (1)
Hey all, I’m experimenting with rewriting my game to be “pure”/stateless: pass in the current game state and player action to a function, get the next state back. Are there any resources out there for structuring the code so the number of helper functions doesn’t explode?
Right now the game is built on passing a state atom around and doing a fair amount of swap!
ing inside the functions
It depends a lot on how your game is architected. One technique I’ve seen is to skip atoms entirely, and make a single recursive “run game loop” function that takes the entire state as an argument.
https://prog21.dadgum.com/23.html Article on “purely functional retrogames,” by an 80s console game programmer
For my roguelike I went passing in gamestate and producing a new gamestate as output. It works fairly well, but I can see how some may not be interested in adding an additional parameter to many functions.
well, we already pass around the game state atom because it's a web game so there's a map of game-id to game atom, and when a player makes a move, we grab their atom and pass it into the corresponding action function (along with any other relevant stuff), so i don't mind passing around state
as the first variable in all functions lol, and because it's a turn-based card game, we don't need the "run game loop"
i'm more asking about, like, how to handle when a bunch of things are changed in a single place: "draw 2 cards" means we have to update both card objects with the new locations, trigger "player 1 drew 2 cards" event handlers, move the card objects from the deck to the hand, write the message "player 1 drew 2 cards" to the game log, etc
this is pretty easy when using an atom, because like a classic variable, i can just pass it around and not have to nest let
bindings, or even just call swap!
manually to change a flag
It sounds like the overhead of an atom is maybe not worth it, if it’s all single threaded and you don’t mind passing it around everywhere?
@paul.legato i'll read that article, thanks for the link
yeah, it's all single-threaded, and because it's a web game, rendering is handled by clojurescript/reagent client-side
One way to make it pure functional involves discarding the notion of “objects.” It’s all just one big map, with deeper maps nested in it. Every bit of state is somewhere in that map.
The game loop is (from that article):
repeat forever {
get user input
process one frame
draw everything on the screen
wait until a frame's worth of time has elapsed
}
get user input
returns “player pressed the draw 2 cards button”. process one frame
takes that + the current huge state map as its arguments, and returns a new state (with 2 cards now drawn). Draw updates the screen to show two cards drawn, etc.
This should work well with reagent-type reactive UI rendering. It’s a very similar structure.
The map that tells us the state of the cards doesn’t have to know where they are drawn on the screen. That’s the UI renderer’s job. Likewise, the UI renderer doesn’t need to know how two more cards got added to the state map.
Similarly, there are no event handlers in this model. That is all driven reactively, changing the state map by returning a new state map from some “process one frame” function.
e.g. initial state:
{:deck [ {:10 :hearts} {:J :clubs} ... ]
:player_hand [{:k :hearts} ... ]}
Call (process-frame current-state user-input)
, get a new state back like:
{:deck [ {:10 :hearts} ... ]
:player_hand [{:k :hearts} {:J :clubs} ... ]}
yeah, that sounds about right