oakes / odoyle-rules

A rules engine for Clojure(Script)
The Unlicense
530 stars 20 forks source link

Add priority map for function execution order #14

Closed ertugrulcetin closed 2 years ago

ertugrulcetin commented 2 years ago

First of all, thank you for creating this library! When building games there is render loop and sometimes we need to execute functions in some order so I needed it and created this PR.

o/->session takes an extra parameter which is called priority-map.

(def rules
  (o/ruleset
    {::player
     [:what
      [::player ::x x]
      [::player ::y y]]

     ::go
     [:what
      [::time ::delta dt]
      [::a ::b ss]
      :then
      (println "GO!")
      :then-finally
      (println "GO Then Finally")]

     ::move-player
     [:what
      [::time ::delta dt]
      [::player ::x x {:then false}]
      :then
      (do
        (println "Move player")
        (o/insert! ::player ::x (+ x dt)))
      :then-finally
      (println "Move player Then Finally")]}))

(def *session
  (atom (reduce o/add-rule
          (o/->session {::move-player 1
                        ::go 2})
          rules)))

(swap! *session
    (fn [session]
      (-> session
        (o/insert ::player {::x 20 ::y 15})
        o/fire-rules)))

(swap! *session
    (fn [session]
      (-> session
        (o/insert ::a {::b 4})
        o/fire-rules)))

Run this multiple times;

  (swap! *session
    (fn [session]
      (-> session
        (o/insert ::time {::total 100 ::delta 0.1})
        o/fire-rules)))
Move player
GO!
Move player Then Finally
GO Then Finally
oakes commented 2 years ago

This could be compared to the salience feature in clara. I've thought about it before but i couldn't think of a good use case. Are you doing rendering in your rules? I have't found any advantage to that; ideally rules only update the state, and then afterwards you could query the session for data and do side-effecting stuff like rendering. Ideally order should not matter unless there is a dependency in the data expressed in the :what block.

ertugrulcetin commented 2 years ago

Ideally order should not matter unless there is a dependency in the data expressed in the :what block.

@oakes Could you show me an example of this please? - how to construct dependency order in :what block

oakes commented 2 years ago

A dependency occurs when a rule receives a fact in its :what block that some other rule inserts. If you made a rule that received ::player ::x, then it would be "dependent" on ::move-player, and would always run after it.

So, if you need rules to run in a certain order even though they don't have any dependency between each other, then i assume you are trying to do side-effects like rendering in your rules. Is this true? Because i want to know the use case for something like this.

ertugrulcetin commented 2 years ago

you are trying to do side-effects like rendering in your rules. Is this true?

Yes, that's true. In the game loop, I need certain order.

oakes commented 2 years ago

Here's how i do it in an example game:

https://github.com/oakes/play-cljc-examples/blob/47f79b7fef8dd5b34baddecb392468e4fd6fe65e/basic-bird/src/basic_bird/core.cljc#L81-L93

Each tick of the game loop, i fire the rules and then query the data i want afterwards. Keep in mind that query-all is very fast because it's just returning the matches that were already computed; the real work is being done during fire-rules. I guess it can sometimes be convenient to do things inside of the rules but i think you'll find that keeping state changes separate from external side effects (like rendering, playing sounds, etc) will end up being cleaner in the long run.

ertugrulcetin commented 2 years ago

Thank you so much for the explanation!