Hi Oskar! Excellent questions. I totally agree with you that writing a simulation in a purely functional style is not the (only) answer in Clojure. After all the primary goal of the language is to deal with (necessary) state well, not to get rid of it or hide it.
Oskar <oskar.kv...@gmail.com> writes: > For the monsters' AI to work, the AI needs to know the state of the > world, i.e. the characters. My current solution is to (declare > characters) and then have a > (defn init-ai [characters] (def characters characters)) > function in ai.clj that I call from the main file at the start of the > game. It works, but it doesn't feel quite right. I don't want to have > to pass the characters as an argument all the time, that seems a > little bit too verbose to me. It seems somehow unnecessary to have to > just pass the same atom around to all the functions. I agree. How about putting the game state in another file which everything that needs access to can refer to? ;; state.clj (ns game.state) (def app (atom nil)) (def characters (atom nil)) ;; ai.clj (ns game.ai (:require [game.state :as state])) (defn do-some-ai-thing [...] (swap! state/characters ...)) ;; gfx.clj (ns game.gfx (:require [game.state :as state])) (defn paint [...] (let [window (.getWindow @app)] (doseq [char @state/characters] ...))) ;; main.clj (ns game.main (:require [game.state :as state])) (defn main [] (reset! state/characters (make-characters)) (reset! state/app (make-app))) > I have a function called physical-attack (the one I mentioned above) > that takes the id of the attacker and the id of the target. Then it > calculates how much damage the attack is supposed to do, and updates > the target's health in the 'characters' atom. Sure, it could return > the amount of damage instead, that would be one step towards purity. > But to be really pure it would have to take the actual maps as > arguments too. It may work in this case, but it's not always so easy > to make the functions pure. The function that calls physical-attack, > for example, also updates the attacker (he now has to wait before he > can attack again), and returning both of the updated characters is not > as nice as returning one. And the update still has to happen > somewhere, because attacking *has* a side effect (in the game), so > maybe it's ok. It's just that now the updates are spread out all over, > and I thought that maybe it was better to have them in a special place > in the code, for sturcture and sanity. ;; state.clj (def characters (atom {:alice {:health 100 :tiredness 0} :bob {:health 100 :tiredness 0}})) ;; main.clj (defn physical-attack [characters attacker-id victim-id] (let [cost 5 damage 20] (-> characters (update-in [victim-id :health] - damage) (update-in [attacker-id :tiredness] + cost)))) (swap! state/characters physical-attack :alice :bob) Okay but perhaps things are getting more complex. We have more pieces of state in play, the "current turn" of the game. This might be an indication that we really need coordination, which would mean a switch from atoms to refs: ;; state.clj (def characters (ref {:alice (ref {:health 100 :next-turn 0}) :bob (ref {:health 100 :next-turn 0})})) (def current-turn (ref 0)) ;; combat.clj (defn physical-attack [attacker victim] (let [delay 3 damage 20] (dosync (alter victim update-in [:health] - damage) (alter attacker assoc :next-turn (+ (ensure current-turn) delay))))) (physical-attack (@state/characters :alice) (@state/characters :bob)) So our physical-attack now contains a transaction (dosync) which causes both the victom and attacker to be updated atomically. This way is more sustainable as the simulation becomes more complicated. For example a weapon's wear level might need to be updated as well, perhaps also the room's "rowdyness level", nearby civilian NPCs could panic and run away from the fighting while Bob's allies jump to his aid and so on. Note that nested transactions are OK. Basically everything just commits at the end of the outermost dosync. So if you want your AI code toperform an attack AND update it's own state in the same transaction you can do something like: ;; ai.clj (defn ai-act [ai] (dosync (physical-attack (:character @ai) (:target @ai)) (alter ai assoc :target (choose-a-target)))) I hope this gives you some ideas! :-) Alex -- You received this message because you are subscribed to the Google Groups "Clojure" group. To post to this group, send email to clojure@googlegroups.com Note that posts from new members are moderated - please be patient with your first post. To unsubscribe from this group, send email to clojure+unsubscr...@googlegroups.com For more options, visit this group at http://groups.google.com/group/clojure?hl=en