Welcome to Prolog! I'm glad you're giving it a shot, and I hope you find it serves your imagination well, since you seem to have a lot!
You're using a lot of strange terminology here for Prolog. I'm not clear on what you mean by "frame" or "node" or "extract" and I'm pretty sure you're using "consult" in an off way. I don't know if this is because the tutorial was written for SICStus or if it's just the usual confusion. I also wouldn't normally think of doing inheritance in Prolog, but that part of this I think I actually do understand. So I hope you'll play along and we'll see if I manage to answer your question anyway.
First things first. It's not a big deal but average score should be implemented like this:
avg_score(Z) :-
student(jack, X),
student(jill, Y),
Z is (X + Y) / 2. % <-- note the use of 'is' instead of '='
This is a common beginner mistake. Prolog doesn't evaluate algebra by default, so when you say something like (X + Y) / 2
all you're really doing is building an expression, no different than div(plus(X, Y), 2)
. The one difference, really, is that is
knows how to evaluate algebraic expressions like (X + Y) / 2
but not div(plus(X, Y), 2)
. (I'll pre-warn you that you'll find Prolog makes a lot more sense if you start using clpfd
from the start, and use #=
instead of is
.)
No, what's unhelpful about avg_score
, of course, is that jack
and jill
are embedded within it. Someday you'll probably want to calculate the average scores of all your students. First let's calculate the average:
average(L, Average) :-
sum(L, Sum),
length(L, Length),
Average is Sum / Length.
% this is overly complex because I wanted to show you
% the tail recursive version
sum(L, Sum) :- sum(L, 0, Sum).
sum([], Sum, Sum).
sum([X|Xs], Acc, Sum) :- Acc1 is X + Acc, sum(Xs, Acc1, Sum).
Now we can get the average of all the students like this:
avg_score(Z) :-
findall(X, student(_, X), Scores),
average(Scores, Z).
I don't happen to know if this relates to what you're asking or not, but it seems like it might be helpful so there it is.
Of course, another possibility is to parameterize things. We could take the original code and parameterize it by student:
avg_score(Student1, Student2, Z) :-
student(Student1, X),
student(Student2, Y),
Z is (X + Y) / 2.
This seems like it might be more pertinent to querying the bestiary. Rather than asking attack(X)
you would ask attack(fire_warrior, X)
.
I hate to send you off to Logtalk when you're so new to Prolog, but I suspect it might hold some of the answers you're looking for. It's particularly good at handling inheritance in a way that vanilla Prolog probably isn't. But it can be a big diversion. For instance, you could handle the inheritance chain for querying the attack stat like this:
% this is the inheritance tree
parent_class(character, base).
parent_class(npc, character).
parent_class(enemy, npc).
parent_class(red_faction, enemy).
parent_class(blue_faction, enemy).
parent_class(fire_warrior, red_faction).
parent_class(water_consort, blue_faction).
% these are the attack bonuses
attack_bonus(base, 0).
attack_bonus(red_faction, 1.5).
attack_bonus(blue_faction, 1).
calc_attack_bonus(X, Bonus) :-
attack_bonus(X, Bonus), !.
calc_attack_bonus(X, Bonus) :-
\+ attack_bonus(X, Bonus),
parent_class(X, Parent),
calc_attack_bonus(Parent, Bonus).
This appears to work for some basic queries:
?- calc_attack_bonus(fire_warrior, Bonus).
Bonus = 1.5.
?- calc_attack_bonus(character, Bonus).
Bonus = 0.
I don't know if you want this behavior or not:
?- calc_attack_bonus(tree, Bonus).
false.
It's easy to fix if not (if parent_class
fails, unify Bonus with 0, otherwise recur).
This is not, however, all that extensible. A more extensible approach might be this:
% these are the attack bonuses
attribute(attack_bonus, base, 0).
attribute(attack_bonus, red_faction, 1.5).
attribute(attack_bonus, blue_faction, 1).
% base stats
attribute(base_stat, character, 8.0).
attribute(level, fire_warrior, 1.0).
Now we can write the rules we need without a lot of pain:
calc_attribute(X, Attribute, Value) :-
attribute(X, Attribute, Value), !.
calc_attribute(X, Attribute, Value) :-
\+ attribute(X, Attribute, Value),
parent_class(X, Parent),
calc_attribute(Parent, Attribute, Value).
And now attack becomes easy:
attack(X, Value) :-
calc_attribute(X, attack_bonus, Bonus),
calc_attribute(X, base_stat, Base),
calc_attribute(X, level, Level),
Value is (Level + Base) * Bonus.
We need to generalize a bit more to get to where we can write compute
though, because ideally, we want to say something like compute(fire_warrior, attack, Value)
. For that to happen, we need to know that attack_bonus
is related to attack
and we don't know that. Let's restructure attribute
a little:
% these are the attack bonuses
attribute(base, bonus(attack), 0).
attribute(red_faction, bonus(attack), 1.5).
attribute(blue_faction, bonus(attack), 1).
% base stats
attribute(character, base_stat, 8.0).
attribute(fire_warrior, level, 1.0).
Now we're cooking:
compute(X, Attribute, Value) :-
calc_attribute(X, bonus(Attribute), Bonus),
calc_attribute(X, base_stat, Base),
calc_attribute(X, level, Level),
Value is (Level + Base) * Bonus.
And behold:
?- compute(fire_warrior, attack, Value).
Value = 13.5.
I hope that's what you wanted. :)
Big Edit
For fun I thought I'd see what this would be like with Logtalk, an object-oriented extension language for Prolog. I'm very new to Logtalk, so this may or may not be a good approach, but it did "do the trick" so let's see if it's more along the lines of what you're wanting. First, the base object:
:- object(base).
:- public(base_stat/1).
:- public(attack/1).
:- public(bonus/2).
:- public(level/1).
:- public(compute/2).
bonus(attack, 0).
base_stat(0).
level(0).
compute(Attribute, Value) :-
::bonus(Attribute, Bonus),
::base_stat(Base),
::level(Level),
Value is (Level + Base) * Bonus.
:- end_object.
This is basically defining the facts we're going to store about each thing, and how to compute the property you're interested in. Next we establish the object hierarchy:
:- object(character, extends(base)).
base_stat(8.0).
:- end_object.
:- object(npc, extends(character)).
:- end_object.
:- object(enemy, extends(npc)).
:- end_object.
:- object(red_faction, extends(enemy)).
bonus(attack, 1.5).
bonus(speed, 1.25).
bonus(defense, 0.25).
:- end_object.
:- object(blue_faction, extends(enemy)).
bonus(attack, 1).
bonus(speed, 1).
bonus(defense, 1).
:- end_object.
:- object(fire_warrior, extends(red_faction)).
level(1.0).
holding(nothing).
:- end_object.
:- object(water_consort, extends(blue_faction)).
level(1.0).
holding(nothing).
:- end_object.
Now using this is pretty much a snap:
?- fire_warrior::compute(attack, X).
X = 13.5.
?- water_consort::compute(attack, X).
X = 9.0.
I hope this helps!