oakes / odoyle-rules

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

Retracting entity? #23

Closed nivekuil closed 1 year ago

nivekuil commented 1 year ago

When an entity dies, what's a good way to retract all facts related to it? My thought was to make a ::graveyard rule that queries for ::dead? entities then query that out-of-band to retract rules but it seems to leave behind some rules occasionally.

oakes commented 1 year ago

You mean retract facts right? Rules cannot be retracted.

To remove all the facts for a given entity you could do it out-of-band like you mention, or make a rule that triggers when the entities dies and manually retract all of the facts. It will throw an exception if the fact doesn't exist, but you can use o/contains? to check if it exists first.

The above approach requires you to know in advance all of the attributes you want to remove, though. Right now there isn't a way to query all facts with a given id. One possibility would be to allow attributes to have binding symbols, so you could write a rule like this:

::player-dies
[:what
 [id ::dead? true]
 [id attr value]
 :then
 (o/retract! id attr)]

Right now this will not work because the attribute isn't allowed to have a symbol there, but this would make it somewhat convenient to make catch-all rules that retract all the facts with a given id. Would that help in your use case?

nivekuil commented 1 year ago

You mean retract facts right? Rules cannot be retracted.

oops, yes

Would that help in your use case?

That looks helpful but I'm not sure I understand your example. Is [id ::dead? true] querying and checking at the same time? So ::player-dies would be triggered multiple times per fire-rules right, is that an inefficient way to modify the session? I was thinking garbage collection-like tasks should be fired separately from the game loop for better frame pacing, so I guess you could insert a ::gc? true fact somewhere but then how would you know when all the gc related rules are done?

oakes commented 1 year ago

I assumed that ::dead? was an attribute you give to entities. So, when you want to mark an entity as dead, you would do something like (o/insert! :my-entity-id ::dead? true). Is that the case?

If so, then the rule I wrote above would only trigger one time, when you set ::dead? to true. It would immediately remove all facts that have :my-entity-id as the id. If that is what you're looking for, I can try implementing it and see if it works for you.

nivekuil commented 1 year ago

ah, that makes sense. The way I have it right now is the entity has all possible attributes already added for denormalization/derived fact purposes, since those all have to exist for the derived fact rule to fire, so it starts with ::dead? false. It probably makes more sense to insert the ::dead? fact only when it matters. It sounds like it could work.

oakes commented 1 year ago

OK I pushed a quick change that allows binding symbols to be used on the attribute column. If you're using clojure deps, just change your deps.edn to bring odoyle in via git:

net.sekao/odoyle-rules {:git/url "https://github.com/oakes/odoyle-rules.git"
                        :git/sha "20788178fb9cfd7cc28742a58016898d133b6a61"}

With that, you should be able to write a rule like I made above. Try it out whenever and let me know how it goes. If it works well for you I can cut a release with it.

oakes commented 1 year ago

BTW you can initialize ::dead? to false if you want. The rule will still only be triggered once, because its what block says that it has to be true.

nivekuil commented 1 year ago

The rule will still only be triggered once, because its what block says that it has to be true.

oh, I did not realize that the right side could be a value acting as a match constraint, not just a symbol acting as a binding. Does the README mention this somewhere?

The new attribute matching feature does work, but I realized I had a further problem: in my ::attack rule, I'm modifying the targets ::hp directly, so depending on the execution order it could add some fact about a dead entity like [1 ::hp -5] that never gets cleaned up. I think I should break that logic out into a ::take-damage rule instead. The lesson is that rules should only modify entities they query for?

In the process of doing this I've noticed that RETE(/UL?) seems to scale pretty poorly with # of facts, even if nothing is explicitly happening with them, so in general something like odoyle would be better suited for complex interactions over a few entities (e.g. Path of Exile) and not something like Vampire Survivors? I wonder if you could spatially partition the set of facts and get things like efficient collision detection for free.

oakes commented 1 year ago

It looks like I didn't specifically have an example of a literal value, so I just added a bit to the end of the Conditions section to explain it.

Regarding "rules should only modify entities they query for", that sounds right, but what do you mean by query for? Are you calling o/query-all in your :then block, or are you talking about your :what block? I talked about the former a bit in the readme (search for "Important rule of thumb").

Yeah it's not going to scale like a typical ECS, I think. It is an open design question about how to get the benefits of both, and I don't know of anyone that has tried it. See #21.

I'll test the attribute matching a bit more and if all looks good, I will cut a release later today.

oakes commented 1 year ago

I released 1.1.0 with this feature so I'll close this.