Functional thinking, managing state, boardgame

Hi Clojureverse.

I am three months in on a learning-path with Clojure. Some fundamentals in place, still a long way to go.

I have a simple model of a boardgame, currently stateless. A map holding a current representation, and some functions to analyze, make decision, make a move etc. The analyze-decide-move step produce a new map with a the new board representation - a new state. Currently this is still in my REPL and the new map evaporates beyond proof-of-concept.

I have not yet decided how to implement this into a sequence, or a game-engine.

Any pointers to resources which give me a good theoretical understanding of how to solve this properly in idiomatic functional, Clojure-ish way is highly appreciated.

My goal is NOT to solve this one problem, but learn this properly :slight_smile:

./henningzen

1 Like

not quite sure if i understand the question correctly… but… anyway… here are two books i can recommend to you… both are teaching how to program ( in lisp )… by discussing possible ways for building various kinds of games…

https://htdp.org/2018-01-06/Book/

http://landoflisp.com/ ( this one really is super fun to read!.. very well written! )

does that help?

3 Likes

Hey @henningzen,

It’s not entirely clear to me what you’re asking about.

You’re describing modeling the game state with pure data and having “state transformers”. Given that, I think you’re on the right track.

Perhaps it would help if you provided a bit more context. How do you intend your game to be run? Should the player run functions in the REPL? Or do you want to write a terminal interface?

Teodor

Your transformer from one game state to the next: splendid! It’s done! Don’t overdress it.

One fun way to proceed is to employ that functional element in not one, but a whole series of interactive experiences. Website, single-page web app, Node app, web service… telephone answering machine? robo-caller dialing random numbers and asking whoever answers for the next move?.. Anyway, it’s a pleasant learning technique because you can hold constant all the delicate logic (already debugged) and focus, laser-like, on the side effects, the different project/compiler practices, etc.

For example,

  • Make a stdin/stdout text app.

  • Shift the game core to a library that you can use in numerous apps, including the above and all the following.

  • Make a stateless website (e.g., with Ring Jetty) in the 1993 style that receives both the entire old game state and new user input on the URL and returns a new page.

  • (If the game core is “pure Clojure” without Java entanglements) rename the files to cljc and use the exact same code in a cljs app - either a web page or a Node app according to your taste.

  • (If the strategy is naturally server-side) make it into a web service and use that web service from a web page or Node.

  • If you did your first cljs app with JQuery, then redo it with React+Reagent for greater smoothness and simplicity.

  • If you’re an old hand with Java, then make a desktop app with JavaFX or Swing app.

I think this is a dynamite way to get used to the language and tools.

The way you handle state functionally is by continuing to transform the old state into the new, repeatedly. This is often done through the use of recursion, for something like a game, where you don’t know when you’ll be done.

So in your case, you have a board, which you said is modeled as a immutable map. That’s a great start.

Now, you would want to do:

  1. Create initial board
  2. Take input for player 1 move
  3. Perform the transformation from initial board to board+player1move = next-board-state
  4. Take input for player 2 (could be the computer)
  5. Perform the transformation from next-board-state+player2move = next-next-board-state
  6. Repeat until someone wins

Here’s an example in code:

(loop [board init-board next-player first-player]
    (print-board board)
    (cond
      (win? board)
      (println "You win!")
      (lose? board)
      (println "You lose!")
      (= :player next-player)
      (recur (next-board board (player-move board))
             :computer)
      (= :computer next-player)
      (recur (next-board board (computer-move board))
             :player)))

So what is happening here is that your long living game state is maintained inside the recursive game loop. You walk through the game state with the cond block, perform the current state actions, apply the transformations from it to the board, and then recur where the board is now bound to the transformed board.

The player-move fn takes a board, and returns the player1move. You can imagine in that method that some IO will be performed, waiting for the player to enter his move, and the IO will be parsed into the move, and the board will be accordingly transformed. This is handled here by the next-board fn, which takes the board and a move and return the board with the move applied to it. This whole thing could itself be a loop, since you might need to validate the move is legal, and if not, return the player an error and recur until the player provides you a valid move.

So player-move takes a board and ask the user for their move, returning the move only once it is valid. After which, next-board takes the board and returns a new copy with the result of applying the given move to it.

The computer-move fn is similar, but you would take the board, and use AI to decide what the computer move should be.

Now this might look very similar to an OOP/imperative approach, and it does look very similar. The difference is that nothing is being mutated. You pass in old-board, and get the new board as a copy. The old board is left intact.

In this way, you can think of it as the state is being carried over from the last iteration to the next.

This is the way to do it functionally. Almost all of your functions will be taking board as the first argument, and return a new board. And the “latest” board will be maintained in the loop binding.

Now, you can have more state if needed. You could have players, and rooms, and what not. Though a common pattern in Clojure is to take the whole set of the world and put it into one big map. So if you had more then just a board to keep track of, you could either add more bindings to loop, or put all of them under one big map called world. The two approaches are somewhat similar, so its a bit of a matter of preference.

Now, if you wanted to be real fancy, you could tap> the board and the move at every recursion, and have it sent to REBL, or pretty-printed, or logged to a file, etc. So now you could easily inspect each change to the board and it would be a nice way to debug and inspect what is happening inside your game loop at every move.

With that, you could even imagine being able to save and reload a game. You’d just save the board and the next-player, and then to restore it, you’d call into the loop but initialize the board and next player to the last saved ones. Similarly, you could recreate the game state, say you had a bug, you could go to the tapped log file, grab the state before the bug, and initialize a loop from it, and debug what is happening. Sky is the limit here!

4 Likes

Check out Solving Problems the Clojure Way by Rafal Dittwald. That talk is such a gem.

Hi @Phill and thanx for reply. To the point, your proposal is what I hope to achieve. A repl-driven approach initially to carry the engine forward, migrating to a proper CLI application, and integrating into a ClojureScript environment for a full web application down the road.

regards, Henning

Hi @didibus

and thank you a very good explanation. Your outline is exactly was I was vaguely thinking but did not know how to express. It bumped my thinking a solid step forward, simplified some frustration I had regarding the state-progression and a number of ways to over-complicate it. Isn’t that the beginners paradox, getting just enough tools under the belt to more harm than good? :wink:

Regards, Henning

@jan,

Will do! A card-game, same concepts for state, and a progression of steps moving forward to n’. Excellent!

Regards, Henning

I’ve always thought this video by Mark Bastian was a good explanation of how to approach designing software functionally, especially coming from OO. It is ~5 years old, and it still holds up. His specific example is a board game, so should be particularly relevant.

3 Likes

I’m chiming in to endorse this talk. I find it to be a fantastic resource for getting into the right mindset, with a practical approach. It answers so many questions most Clojure beginners have, without getting lost in abstract arguments.

Also, welcome to Clojureverse, @d-el! Great first post :slight_smile:

I guess it’s common to put the state of the whole program as a map. What if the app is complex and the state map gets too big? What techniques can we use to help with this?

You mean for performance ? Or in the sense of making sense of it and manipulating it ?

@didibus, the above post by @markx has a valid and interesting point. However, in my case, the whole game-map is nor complex or growing; the structure is static, the change ratio for a game’s lifetime is linear. The potential complexity of a map is maybe the rootcause for choosing something that is immutable and encourage the use of pure functions? That will not reduce the overall complexity, but for sure will make complexity manageable and testable.

regards, Henningzen

Depending on how you have organized your map, you can basically create namespaces inside it, and group related things as needed. If you have many relations (high coupling) between different parts of your data model, or your map is getting too deep, you might want to consider using Datascript instead, and basically putting your app state into a database.

For deeply nested data (which while normally undesirable, sometimes is a good way to represent a given domain), you can also use lens-like libraries (I’ve had good experiences with Specter), which simplify manipulating those structures.

The latter.

Yeah I’m mostly talking about how to organize it.

When the state gets too big, I image a lot of the functions will take a state parameter, and follow a long chain of nested maps to read some value, to update it or just pass it to other functions.

In my past React using experience, this is a similar problem when your components chain is too long, and you need something like Redux.

However, Redux’s combineReducers can be helpful because, each reducer only cares about part of the state tree, and it’s automatically passed that part, instead of the whole state.

I’m just wondering if there’s already some lib there to help, or we need to manually wire these together.

And how would database help here, unless it uses too much ram?

Just some random thoughts. I could be totally wrong.

Hum…

Well, I’d say if you have a lot of complex state, but that’s just the nature of the app, then its just a more complex app, and there’s little you can do to get away from that.

But, if you feel like its the way you model state that makes it feel more complex then it is. I think there are certain tools. Like other have said, there’s a few good lens-like libraries like Specter and Meander which help. Or like I said previously, you don’t need to put everything in one big map, that’s a pattern some people follow because they find it makes their life easier. If you find that makes yours harder, then just have multiple root maps instead.

Using an in-memory DB (or even a real DB), can help for really complex cases, because it gives you a powerful query engine. And most DB already try to solve the data model issue, making it easier to organize large amount of data in a performant, searchable and queryable way already.

Another thing is, don’t pass your map down the chain. You should still have components with proper boundaries, and have the caller find and prepare the state your component needs.

Lastly I’d say… how would you do it in OOP? In most OOP lang, you similarly just end up with a nested set of key/value objects. And the only thing you can do on them is get-in and assoc-in. Its even worse then in Clojure, where you can easily loop over elements in the map, search over, them, etc. In OOP you can’t normally do that. So at the very least, it shouldn’t be any worse, and in most cases it should be easier to manage these complex states in Clojure.

1 Like

Thank you @d-el - that is a superb talk :slight_smile:

I also really liked Mark Bastian’s similarly brilliant talk on data modelling which again starts with the dominant approach that’s been used for the last 20 years - relational schemas and SQL - and directly contrasts that with the newer but simpler way that many in the Clojure community get to know about (but only after a while) - entity attribute value triples with attribute schemas and Datalog. It’s a game changing way of describing the world, particularly in complex domains - far more open and flexible. And it’s all done with superheroes :slight_smile:

Capture.PNG

1 Like

Hi! This thread is a gem!
Another great resource is http://www.parens-of-the-dead.com/ by Magnar Sveen. It’s a screencast series tackling the development of a board game using Clojure and ClojureScript.

There is also (more or less) the same series in Norwegian here, where he pairs with @cjohansen : http://www.zombieclj.no/

1 Like