game-developmentclips

CLIPS rules system behavior


I am learning CLIPS to evaluate logic in a video game. My CLIPS code is below. In this example, there is physical damage and physical resistances, which should reduce the physical damage by a factor. The current code works for this toy example in which there is one damage fact and one resistance fact. However, it fails when there are multiple damage facts, since the resist fact is retracted by the rule. However, if I do not retract the resist rule, then it re-triggers creating an infinite loop.

I'm wondering what the best practice is for the following: There may be several resistance facts and several damage facts. Where applicable, a resistance fact should be applied to each damage fact once and only once. In other words, a single damage fact might have multiple resistance facts applied to it, or a single resistance fact might apply to multiple damage facts.

(deftemplate damage
  (slot source (type STRING))
  (slot target (type STRING))
  (slot amount (type FLOAT))
  (slot type (type STRING)))

(deftemplate resist
  (slot type (type STRING))
  (slot factor (type FLOAT))
  (slot player_id (type STRING)))

(defrule damage-resistance
   ?d <- (damage (source ?source) (target ?target) (amount ?amount) (type ?type))
   ?r <- (resist (type ?type) (factor ?factor) (player_id ?target))
   =>
   (bind ?reduced-amount (* ?amount ?factor))
   (retract ?r)
   (modify ?d (amount ?reduced-amount))
)

(assert (damage (source "1") (target "2") (amount 10.0) (type "physical")))
(assert (resist (type "physical") (factor 0.9) (player_id "2")))

What is the best practice here? It seems like I can add multislots to the templates to track rules that have been applied, or I can modify the rules in some way...

I've tried modifying the templates but not sure I have an elegant solution

EDIT: I have decided to use a solution like this, where each damage fact has a unique_id and each resistance fact tracks which damage facts it has applied to already. Not sure if this is the most elegant, but it works... at least for my current needs.

(deftemplate damage
  (slot id (type INTEGER))
  (slot source (type STRING))
  (slot target (type STRING))
  (slot amount (type FLOAT))
  (slot type (type STRING)))

(deftemplate resist
  (slot type (type STRING))
  (slot factor (type FLOAT))
  (slot player_id (type STRING))
  (multislot applied_to)) ; Add a multislot to track to which damage facts this resist has been applied

(defrule damage-resistance
   ?d <- (damage (source ?source) (target ?target) (amount ?amount) (type ?type) (id ?id))
   ?r <- (resist (type ?type) (factor ?factor) (player_id ?target) (applied_to $?applied))
   (test (not (member$ ?id ?applied)))
   =>
   (bind ?reduced-amount (* ?amount ?factor))
   (modify ?r (applied_to $?applied ?id)) ; Mark this resist as applied to this damage source
   (modify ?d (amount ?reduced-amount))
)

(assert (damage (source "1") (target "2") (amount 10.0) (type "physical") (id 0)))
(assert (damage (source "1") (target "2") (amount 5.0) (type "physical") (id 1)))
(assert (resist (type "physical") (factor 0.9) (player_id "2")))

Solution

  • Using facts, having a slot that tracks what's already been applied is probably the best solution.

    Object patterns are only triggered when a change is made to a slot that is explicitly matched, so if you use instances and the amount slot is not included in the pattern, you can modify it in the actions of the rule without creating an infinite loop:

             CLIPS (6.4.1 4/8/23)
    CLIPS> 
    (defclass DAMAGE
      (is-a USER)
      (slot source (type STRING))
      (slot target (type STRING))
      (slot amount (type FLOAT))
      (slot type (type STRING)))
    CLIPS> 
    (defclass RESIST
      (is-a USER)
      (slot type (type STRING))
      (slot factor (type FLOAT))
      (slot player_id (type STRING)))
    CLIPS> 
    (defrule damage-resistance
       ?d <- (object (is-a DAMAGE) (source ?source) (target ?target) (type ?type))
       (object (is-a RESIST) (type ?type) (factor ?factor) (player_id ?target))
       =>
       (bind ?reduced-amount (* (send ?d get-amount) ?factor))
       (modify-instance ?d (amount ?reduced-amount)))
    CLIPS> 
    (definstances initial
      (d1 of DAMAGE (source "1") (target "2") (amount 10.0) (type "physical"))
      (d2 of DAMAGE (source "2") (target "2") (amount 18.0) (type "physical"))
      (r1 of RESIST (type "physical") (factor 0.9) (player_id "2"))
      (r2 of RESIST (type "physical") (factor 0.8) (player_id "2")))
    CLIPS> (reset)
    CLIPS> (watch rules)
    CLIPS> (run)
    FIRE    1 damage-resistance: [d1],[r2]
    FIRE    2 damage-resistance: [d2],[r2]
    FIRE    3 damage-resistance: [d1],[r1]
    FIRE    4 damage-resistance: [d2],[r1]
    CLIPS> (send [d1] print)
    [d1] of DAMAGE
    (source "1")
    (target "2")
    (amount 7.2)
    (type "physical")
    CLIPS> (send [d2] print)
    [d2] of DAMAGE
    (source "2")
    (target "2")
    (amount 12.96)
    (type "physical")
    CLIPS>