-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathGamygdala.js
971 lines (861 loc) · 42.8 KB
/
Gamygdala.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
////////////////////////////////////////////////////////////////////////
//GAMYGDALA EMOTION ENIGINE CODE. This is STANDALONE AND NOT DEPENDENT ON PHASER!
////////////////////////////////////////////////////////////////////////
TUDelft = function(){
//simply create the namespace
};
/**
* This is the main appraisal engine class taking care of interpreting a situation emotionally. Typically you create one instance of this class and then register all agents (emotional entities) to it, as well as all goals.
*
* @class TUDelft.Gamygdala
* @constructor
*/
TUDelft.Gamygdala = function () {
this.agents = [];
this.goals=[];
this.decayFunction=this.exponentialDecay;
this.decayFactor=0.8;
this.lastMillis=Date.now();
this.millisPassed;
this.debug;
};
/**
* A facilitator method that creates a new Agent and registers it for you
*
* @method TUDelft.Gamygdala.createAgent
* @param {String} agentName The agent with agentName is created
* @return {TUDelft.Gamygdala.Agent} An agent reference to the newly created agent
*/
TUDelft.Gamygdala.prototype.createAgent = function(agentName){
temp=new TUDelft.Gamygdala.Agent(agentName);
this.registerAgent(temp);
return temp;
}
/**
* A facilitator method to create a goal for a particular agent, that also registers the goal to the agent and gamygdala.
* This method is thus handy if you want to keep all gamygdala logic internal to Gamygdala.
* However, if you want to do more sophisticated stuff (e.g., goals for multiple agents, keep track of your own list of goals to also remove them, appraise events per agent without the need for gamygdala to keep track of goals, etc...) this method will probably be doing too much.
* @method TUDelft.Gamygdala.createGoalForAgent
* @param {String} agentName The agent's name to which the newly created goal has to be added.
* @param {String} goalName The goal's name.
* @param {double} goalUtility The goal's utility.
* @param {boolean} isMaintenanceGoal Defines if the goal is a maintenance goal or not [optional]. The default is that the goal is an achievement goal, i.e., a goal that once it's likelihood reaches true (1) or false (-1) stays that way.
* @return {TUDelft.Gamygdala.Goal} - a goal reference to the newly created goal.
*/
TUDelft.Gamygdala.prototype.createGoalForAgent = function(agentName, goalName, goalUtility, isMaintenanceGoal){
tempAgent=this.getAgentByName(agentName);
if (tempAgent){
tempGoal=this.getGoalByName(goalName);
if (tempGoal)
console.log("Warning: I cannot make a new goal with the same name "+goalName+" as one is registered already. I assume the goal is a common goal and will add the already known goal with that name to the agent "+agentName);
else {
tempGoal=new TUDelft.Gamygdala.Goal(goalName, goalUtility);
this.registerGoal(tempGoal);
}
tempAgent.addGoal(tempGoal);
if (isMaintenanceGoal)
tempGoal.isMaintenanceGoal=isMaintenanceGoal;
return tempGoal;
} else
{ console.log("Error: agent with name "+ agentName + " does not exist, so I cannot create a goal for it.");
return null;
}
}
/**
* A facilitator method to create a relation between two agents. Both source and target have to exist and be registered with this Gamygdala instance.
* This method is thus handy if you want to keep all gamygdala logic internal to Gamygdala.
* @method TUDelft.Gamygdala.createRelation
* @param {String} sourceName The agent who has the relation (the source)
* @param {String} targetName The agent who is the target of the relation (the target)
* @param {double} relation The relation (between -1 and 1).
*/
TUDelft.Gamygdala.prototype.createRelation = function(sourceName, targetName, relation){
source=this.getAgentByName(sourceName);
target=this.getAgentByName(targetName);
if (source && target && relation>=-1 && relation<=1){
source.updateRelation(targetName, relation);
} else
console.log('Error: cannot relate ' + source + ' to ' + target + ' with intensity '+relation);
}
/**
* A facilitator method to appraise an event. It takes in the same as what the new Belief(...) takes in, creates a belief and appraises it for all agents that are registered.
* This method is thus handy if you want to keep all gamygdala logic internal to Gamygdala.
* @method TUDelft.Gamygdala.appraiseBelief
* @param {double} likelihood The likelihood of this belief to be true.
* @param {String} causalAgentName The agent's name of the causal agent of this belief.
* @param {String[]} affectedGoalNames An array of affected goals' names.
* @param {double[]} goalCongruences An array of the affected goals' congruences (i.e., the extend to which this event is good or bad for a goal [-1,1]).
* @param {boolean} [isIncremental] Incremental evidence enforces gamygdala to see this event as incremental evidence for (or against) the list of goals provided, i.e, it will add or subtract this belief's likelihood*congruence from the goal likelihood instead of using the belief as "state" defining the absolute likelihood
*/
TUDelft.Gamygdala.prototype.appraiseBelief = function(likelihood, causalAgentName, affectedGoalNames, goalCongruences, isIncremental){
tempBelief=new TUDelft.Gamygdala.Belief(likelihood, causalAgentName, affectedGoalNames, goalCongruences, isIncremental);
this.appraise(tempBelief);
}
/**
* Facilitator method to print all emotional states to the console.
* @method TUDelft.Gamygdala.printAllEmotions
* @param {boolean} gain Whether you want to print the gained (true) emotional states or non-gained (false).
*/
TUDelft.Gamygdala.prototype.printAllEmotions = function(gain){
for (var i=0;i<this.agents.length;i++){
this.agents[i].printEmotionalState(gain);
this.agents[i].printRelations(null);
}
}
/**
* Facilitator to set the gain for the whole set of agents known to TUDelft.Gamygdala.
* For more realistic, complex games, you would typically set the gain for each agent type separately, to finetune the intensity of the response.
* @method TUDelft.Gamygdala.setGain
* @param {double} gain The gain value [0 and 20].
*/
TUDelft.Gamygdala.prototype.setGain =function(gain){
for (var i=0;i<this.agents.length;i++){
this.agents[i].setGain(gain);
}
}
/**
* Sets the decay factor and function for emotional decay.
* It sets the decay factor and type for emotional decay, so that an emotion will slowly get lower in intensity.
* Whenever decayAll is called, all emotions for all agents are decayed according to the factor and function set here.
* @method TUDelft.Gamygdala.setDecay
* @param {double} decayFactor The decayfactor used. A factor of 1 means no decay, a factor
* @param {function} decayFunction The decay function tobe used. choose between linearDecay or exponentialDecay (see the corresponding methods)
*/
TUDelft.Gamygdala.prototype.setDecay = function(decayFactor, decayFunction){
this.decayFunction=decayFunction;
this.decayFactor=decayFactor;
}
/**
* This starts the actual gamygdala decay process. It simply calls decayAll() at the specified interval.
* The timeMS only defines the interval at which to decay, not the rate over time, that is defined by the decayFactor and function.
* For more complex games (e.g., games where agents are not active when far away from the player, or games that do not need all agents to decay all the time) you should yourself choose when to decay agents individually.
* To do so you can simply call the agent.decay() method (see the agent class).
* @param {int} timeMS The "framerate" of the decay in milliseconds.
*/
TUDelft.Gamygdala.prototype.startDecay = function(timeMS){
setInterval(this.decayAll.bind(this), timeMS);
}
////////////////////////////////////////////////////////
//Below this is more detailed gamygdala stuff to use it more flexibly.
////////////////////////////////////////////////////////
/**
* For every entity in your game (usually NPC's, but can be the player character too) you have to first create an Agent object and then register it using this method.
* Registering the agent makes sure that Gamygdala will be able to emotionally interpret incoming Beliefs about the game state for that agent.
* @method TUDelft.Gamygdala.registerAgent
* @param {TUDelft.Gamygdala.Agent} agent The agent to be registered
*/
TUDelft.Gamygdala.prototype.registerAgent = function(agent){
this.agents.push(agent);
agent.gamygdalaInstance=this;
};
/**
* Simple agent getter by name.
* @method TUDelft.Gamygdala.getAgentByName
* @param {String} agentName The name of the agent to be found.
* @return {TUDelft.Gamygdala.Agent} null or an agent reference that has the name property equal to the agentName argument
*/
TUDelft.Gamygdala.prototype.getAgentByName = function(agentName){
for(var i = 0; i <this.agents.length; i++){
if(this.agents[i].name === agentName){
return this.agents[i];
}
}
console.log('Warning: agent '+agentName+' not found');
return null;
};
/**
* For every goal that NPC's or player characters can have you have to first create a Goal object and then register it using this method.
* Registering the goals makes sure that Gamygdala will be able to find the correct goal references when a Beliefs about the game state comes in.
* @method TUDelft.Gamygdala.registerGoal
* @param {TUDelft.Gamygdala.Goal} goal The goal to be registered.
*/
TUDelft.Gamygdala.prototype.registerGoal = function(goal){
if (this.getGoalByName(goal.name)==null)
this.goals.push(goal);
else{
console.log("Warning: failed adding a second goal with the same name: "+goal.name);
}
};
/**
* Simple goal getter by name.
* @method TUDelft.Gamygdala.getGoalByName
* @param {String} goalName The name of the goal to be found.
* @return {TUDelft.Gamygdala.Goal} null or a goal reference that has the name property equal to the goalName argument
*/
TUDelft.Gamygdala.prototype.getGoalByName = function(goalName){
for(var i = 0; i <this.goals.length; i++){
if(this.goals[i].name === goalName){
return this.goals[i];
}
}
return null;
};
/**
* This method is the main emotional interpretation logic entry point. It performs the complete appraisal of a single event (belief) for all agents (affectedAgent=null) or for only one agent (affectedAgent=true)
* if affectedAgent is set, then the complete appraisal logic is executed including the effect on relations (possibly influencing the emotional state of other agents),
* but only if the affected agent (the one owning the goal) == affectedAgent
* this is sometimes needed for efficiency, if you as a game developer know that particular agents can never appraise an event, then you can force Gamygdala to only look at a subset of agents.
* Gamygdala assumes that the affectedAgent is indeed the only goal owner affected, that the belief is well-formed, and will not perform any checks, nor use Gamygdala's list of known goals to find other agents that share this goal (!!!)
* @method TUDelft.Gamygdala.appraise
* @param {TUDelft.Gamygdala.Belief} belief The current event, in the form of a Belief object, to be appraised
* @param {TUDelft.Gamygdala.Agent} [affectedAgent] The reference to the agent who needs to appraise the event. If given, this is the appraisal perspective (see explanation above).
*/
TUDelft.Gamygdala.prototype.appraise = function(belief, affectedAgent){
if (affectedAgent==null){
//check all
if (this.debug)
console.log(belief);
if(belief.goalCongruences.length != belief.affectedGoalNames.length){
console.log("Error: the congruence list was not of the same length as the affected goal list");
return false; //The congruence list must be of the same length as the affected goals list.
}
if (this.goals.length==0){
console.log("Warning: no goals registered to Gamygdala, all goals to be considered in appraisal need to be registered.");
return false; //No goals registered to GAMYGDALA.
}
for (var i = 0; i < belief.affectedGoalNames.length; i++) {
//Loop through every goal in the list of affected goals by this event.
var currentGoal=this.getGoalByName(belief.affectedGoalNames[i]);
if (currentGoal!=null){
//the goal exists, appraise it
var utility = currentGoal.utility;
var deltaLikelihood = this.calculateDeltaLikelihood(currentGoal, belief.goalCongruences[i], belief.likelihood, belief.isIncremental);
//var desirability = belief.goalCongruences[i] * utility;
var desirability = deltaLikelihood * utility;
if (this.debug)
console.log('Evaluated goal: ' + currentGoal.name +'('+utility+', '+deltaLikelihood+')');
//now find the owners, and update their emotional states
for(var j = 0; j < this.agents.length; j++){
if(this.agents[j].hasGoal(currentGoal.name)){
var owner=this.agents[j];
if (this.debug)
console.log('....owned by '+owner.name);
this.evaluateInternalEmotion(utility, deltaLikelihood, currentGoal.likelihood, owner);
this.agentActions(owner.name, belief.causalAgentName, owner.name, desirability, utility, deltaLikelihood);
//now check if anyone has a relation to this goal owner, and update the social emotions accordingly.
for (var k=0;k<this.agents.length;k++){
var relation=this.agents[k].getRelation(owner.name);
if(relation!=null){
if (this.debug){
console.log(this.agents[k].name + ' has a relationship with '+owner.name);
console.log(relation);
}
//The agent has relationship with the goal owner which has nonzero utility, add relational effects to the relations for agent[k].
this.evaluateSocialEmotion(utility, desirability, deltaLikelihood, relation, this.agents[k]);
//also add remorse and gratification if conditions are met within (i.e., agent[k] did something bad/good for owner)
this.agentActions(owner.name, belief.causalAgentName, this.agents[k].name, desirability, utility, deltaLikelihood);
} else {
if (this.debug)
console.log(this.agents[k].name + ' has NO relationship with '+owner.name);
}
}
}
}
}
}
} else {
//check only affectedAgent (which can be much faster) and does not involve console output nor checks
for (var i = 0; i < belief.affectedGoalNames.length; i++) {
//Loop through every goal in the list of affected goals by this event.
var currentGoal=affectedAgent.getGoalByName(belief.affectedGoalNames[i]);
var utility = currentGoal.utility;
var deltaLikelihood = this.calculateDeltaLikelihood(currentGoal, belief.goalCongruences[i], belief.likelihood, belief.isIncremental);
//var desirability = belief.goalCongruences[i] * utility;
var desirability = deltaLikelihood * utility;
//assume affectedAgent is the only owner to be considered in this appraisal round.
var owner=affectedAgent;
this.evaluateInternalEmotion(utility, deltaLikelihood, currentGoal.likelihood, owner);
this.agentActions(owner.name, belief.causalAgentName, owner.name, desirability, utility, deltaLikelihood);
//now check if anyone has a relation to this goal owner, and update the social emotions accordingly.
for (var k=0;k<this.agents.length;k++){
var relation=this.agents[k].getRelation(owner.name);
if(relation!=null){
if (this.debug){
console.log(this.agents[k].name + ' has a relationship with '+owner.name);
console.log(relation);
}
//The agent has relationship with the goal owner which has nonzero utility, add relational effects to the relations for agent[k].
this.evaluateSocialEmotion(utility, desirability, deltaLikelihood, relation, this.agents[k]);
//also add remorse and gratification if conditions are met within (i.e., agent[k] did something bad/good for owner)
this.agentActions(owner.name, belief.causalAgentName, this.agents[k].name, desirability, utility, deltaLikelihood);
} else {
if (this.debug)
console.log(this.agents[k].name + ' has NO relationship with '+owner.name);
}
}
}
}
//print the emotions to the console for debugging
if (this.debug){
this.printAllEmotions(false);
//this.printAllEmotions(true);
}
}
/**
* This method decays for all registered agents the emotional state and relations. It performs the decay according to the time passed, so longer intervals between consecutive calls result in bigger clunky steps.
* Typically this is called automatically when you use startDecay(), but you can use it yourself if you want to manage the timing.
* This function is keeping track of the millis passed since the last call, and will (try to) keep the decay close to the desired decay factor, regardless the time passed
* So you can call this any time you want (or, e.g., have the game loop call it, or have e.g., Phaser call it in the plugin update, which is default now).
* Further, if you want to tweak the emotional intensity decay of individual agents, you should tweak the decayFactor per agent not the "frame rate" of the decay (as this doesn't change the rate).
* @method TUDelft.Gamygdala.decayAll
*/
TUDelft.Gamygdala.prototype.decayAll = function(){
this.millisPassed=Date.now()-this.lastMillis;
this.lastMillis=Date.now();
for (var i=0;i<this.agents.length;i++){
this.agents[i].decay(this);
}
}
////////////////////////////////////////////////////////
//Below this is internal gamygdala stuff not to be used publicly (i.e., never call these methods).
////////////////////////////////////////////////////////
TUDelft.Gamygdala.prototype.calculateDeltaLikelihood = function(goal, congruence, likelihood, isIncremental){
//Defines the change in a goal's likelihood due to the congruence and likelihood of a current event.
//We cope with two types of beliefs: incremental and absolute beliefs. Incrementals have their likelihood added to the goal, absolute define the current likelihood of the goal
//And two types of goals: maintenance and achievement. If an achievement goal (the default) is -1 or 1, we can't change it any more (unless externally and explicitly by changing the goal.likelihood).
var oldLikelihood = goal.likelihood;
var newLikelihood;
if (goal.isMaintenanceGoal==false && (oldLikelihood>=1 | oldLikelihood<=-1))
return 0;
if (goal.calculateLikelyhood){
//if the goal has an associated function to calculate the likelyhood that the goal is true, then use that function,
newLikelihood=goal.calculateLikelyhood();
} else {
//otherwise use the event encoded updates.
if (isIncremental){
newLikelihood = oldLikelihood + likelihood*congruence;
newLikelihood=Math.max(Math.min(newLikelihood,1), -1);
}
else
newLikelihood = (congruence * likelihood + 1.0)/2.0;
}
goal.likelihood=newLikelihood;
if(oldLikelihood != null){
return newLikelihood - oldLikelihood;
}else{
return newLikelihood;
}
}
TUDelft.Gamygdala.prototype.evaluateInternalEmotion = function(utility, deltaLikelihood, likelihood, agent){
//This method evaluates the event in terms of internal emotions that do not need relations to exist, such as hope, fear, etc..
var positive;
var intensity;
var emotion = [];
if( utility >= 0){
if ( deltaLikelihood >= 0){
positive = true;
}else {
positive = false;
}
} else if ( utility < 0){
if( deltaLikelihood >= 0){
positive = false;
} else {
positive = true;
}
}
if(likelihood > 0 && likelihood < 1){
if (positive === true){
emotion.push('hope');
}else {
emotion.push('fear');
}
} else if(likelihood === 1){
if (utility >= 0){
if(deltaLikelihood < 0.5){
emotion.push('satisfaction');
}
emotion.push('joy');
}
else {
if(deltaLikelihood <0.5){
emotion.push('fear-confirmed');
}
emotion.push('distress');
}
} else if(likelihood === 0){
if( utility >= 0){
if( deltaLikelihood > 0.5){
emotion.push('disappointment');
}
emotion.push('distress');
}else {
if( deltaLikelihood > 0.5){
emotion.push('relief');
}
emotion.push('joy');
}
}
intensity = Math.abs(utility * deltaLikelihood);
if(intensity != 0){
for(var i = 0; i < emotion.length; i++){
agent.updateEmotionalState(new TUDelft.Gamygdala.Emotion(emotion[i], intensity));
}
}
}
TUDelft.Gamygdala.prototype.evaluateSocialEmotion = function(utility, desirability, deltaLikelihood, relation, agent){
//This function is used to evaluate happy-for, pity, gloating or resentment.
//Emotions that arise when we evaluate events that affect goals of others.
//The desirability is the desirability from the goal owner's perspective.
//The agent is the agent getting evaluated (the agent that gets the social emotion added to his emotional state).
//The relation is a relation object between the agent being evaluated and the goal owner of the affected goal.
var emotion = new TUDelft.Gamygdala.Emotion(null,null);
if (desirability >= 0){
if(relation.like >= 0){
emotion.name = 'happy-for';
}
else {
emotion.name = 'resentment';
}
}
else {
if (relation.like >= 0){
emotion.name = 'pity';
}
else {
emotion.name = 'gloating';
}
}
emotion.intensity = Math.abs(utility * deltaLikelihood * relation.like);
if(emotion.intensity != 0){
relation.addEmotion(emotion);
agent.updateEmotionalState(emotion); //also add relation emotion the emotion to the emotional state
}
}
TUDelft.Gamygdala.prototype.agentActions = function(affectedName, causalName, selfName, desirability, utility, deltaLikelihood){
if (causalName!=null && causalName!=''){
//If the causal agent is null or empty, then we we assume the event was not caused by an agent.
//There are three cases here.
//The affected agent is SELF and causal agent is other.
//The affected agent is SELF and causal agent is SELF.
//The affected agent is OTHER and causal agent is SELF.
var emotion = new TUDelft.Gamygdala.Emotion(null,null);
var relation;
if(affectedName === selfName && selfName != causalName){
//Case one
if(desirability >= 0){
emotion.name = 'gratitude';
}
else {
emotion.name = 'anger';
}
emotion.intensity = Math.abs(utility * deltaLikelihood);
var self = this.getAgentByName(selfName);
if(self.hasRelationWith(causalName)){
relation = self.getRelation(causalName);
}else{
self.updateRelation(causalName, 0.0);
relation = self.getRelation(causalName);
}
relation.addEmotion(emotion);
self.updateEmotionalState(emotion); //also add relation emotion the emotion to the emotional state
}
if(affectedName === selfName && selfName === causalName){
//Case two
//This case is not included in TUDelft.Gamygdala.
//This should include pride and shame
}
if(affectedName != selfName && causalName === selfName){
//Case three
relation;
if( this.getAgentByName(causalName).hasRelationWith(affectedName)){
relation = this.getAgentByName(causalName).getRelation(affectedName);
if(desirability >= 0){
if(relation.like >= 0){
emotion.name = 'gratification';
emotion.intensity = Math.abs(utility * deltaLikelihood * relation.like);
relation.addEmotion(emotion);
this.getAgentByName(causalName).updateEmotionalState(emotion); //also add relation emotion the emotion to the emotional state
}
}
else {
if(relation.like >= 0){
emotion.name = 'remorse';
emotion.intensity = Math.abs(utility * deltaLikelihood * relation.like);
relation.addEmotion(emotion);
this.getAgentByName(causalName).updateEmotionalState(emotion); //also add relation emotion the emotion to the emotional state
}
}
}
}
}
}
/** A linear decay function that will decrease the emotion intensity of an emotion every tick by a constant defined by the decayFactor in the gamygdala instance.
* You can set Gamygdala to use this function for all emotion decay by calling setDecay() and passing this function as second parameter. This function is not to be called directly.
* @method TUDelft.Gamygdala.linearDecay
*/
TUDelft.Gamygdala.prototype.linearDecay = function(value){
//assumes the decay of the emotional state intensity is linear with a factor equal to decayFactor per second.
return value-this.decayFactor*(this.millisPassed/1000);
}
/** An exponential decay function that will decrease the emotion intensity of an emotion every tick by a factor defined by the decayFactor in the gamygdala instance.
* You can set Gamygdala to use this function for all emotion decay by calling setDecay() and passing this function as second parameter. This function is not to be called directly.
* @method TUDelft.Gamygdala.exponentialDecay
*/
TUDelft.Gamygdala.prototype.exponentialDecay = function(value){
//assumes the decay of the emotional state intensity is exponential with a factor equal to decayFactor per second.
return value*Math.pow(this.decayFactor, this.millisPassed/1000);
}
/**
* This is the emotion agent class taking care of emotion management for one entity
*
* @class TUDelft.Gamygdala.Agent
* @constructor
* @param {String} name The name of the agent to be created. This name is used as ref throughout the appraisal engine.
*/
TUDelft.Gamygdala.Agent = function(name){
this.name = name;
this.goals = [];
this.currentRelations = [];
this.internalState = [];
this.gain=1;
this.gamygdalaInstance;
this.mapPAD=[];
this.mapPAD['distress']=[-0.61,0.28,-0.36];
this.mapPAD['fear']=[-0.64,0.6,-0.43];
this.mapPAD['hope']=[0.51,0.23,0.14];
this.mapPAD['joy']=[0.76,.48,0.35];
this.mapPAD['satisfaction']=[0.87,0.2,0.62];
this.mapPAD['fear-confirmed']=[-0.61,0.06,-0.32];//defeated
this.mapPAD['disappointment']=[-0.61,-0.15,-0.29];
this.mapPAD['relief']=[0.29,-0.19,-0.28];
this.mapPAD['happy-for']=[0.64,0.35,0.25];
this.mapPAD['resentment']=[-0.35,0.35,0.29];
this.mapPAD['pity']=[-0.52,0.02,-0.21];//regretful
this.mapPAD['gloating']=[-0.45,0.48,0.42];//cruel
this.mapPAD['gratitude']=[0.64,0.16,-0.21];//grateful
this.mapPAD['anger']=[-0.51,0.59,0.25];
this.mapPAD['gratification']=[0.69,0.57,0.63];//triumphant
this.mapPAD['remorse']=[-0.57,0.28,-0.34];//guilty
};
/**
* Adds a goal to this agent's goal list (so this agent becomes an owner of the goal)
* @method TUDelft.Gamygdala.Agent.addGoal
* @param {TUDelft.Gamygdala.Goal} goal The goal to be added.
*/
TUDelft.Gamygdala.Agent.prototype.addGoal = function(goal) {
//no copy, cause we need to keep the ref,
//one goal can be shared between agents so that changes to this one goal are reflected in the emotions of all agents sharing the same goal
this.goals.push(goal);
};
/**
* Adds a goal to this agent's goal list (so this agent becomes an owner of the goal)
* @method TUDelft.Gamygdala.Agent.removeGoal
* @param {String} goalName The name of the goal to be added.
* @return {boolean} True if the goal could be removed, false otherwise.
*/
TUDelft.Gamygdala.Agent.prototype.removeGoal = function(goalName){
for(var i = 0; i < this.goals.length; i++){
if(this.goals[i].name === goalName){
this.goals.splice(i, 1);
return true;
}
}
return false;
};
/**
* Checks if this agent owns a goal.
* @method TUDelft.Gamygdala.Agent.hasGoal
* @param {String} goalName The name of the goal to be checked.
* @return {boolean} True if this agent owns the goal, false otherwise.
*/
TUDelft.Gamygdala.Agent.prototype.hasGoal= function(goalName){
return (this.getGoalByName(goalName)!=null);
}
/**
* If this agent has a goal with name goalName, this method returns that goal.
* @method TUDelft.Gamygdala.Agent.getGoalByName
* @param {String} goalName The name of the goal to be found.
* @return {TUDelft.Gamygdala.Goal} the reference to the goal.
*/
TUDelft.Gamygdala.Agent.prototype.getGoalByName = function(goalName){
for(var i = 0; i < this.goals.length; i++){
if(this.goals[i].name === goalName){
return this.goals[i];
}
}
return null;
}
/**
* Sets the gain for this agent.
* @method TUDelft.Gamygdala.Agent.setGain
* @param {double} gain The gain value [0 and 20].
*/
TUDelft.Gamygdala.Agent.prototype.setGain = function(gain){
if (gain<=0 || gain>20)
console.log('Error: gain factor for appraisal integration must be between 0 and 20');
else
this.gain=gain;
}
/**
* A facilitating method to be able to appraise one event only from the perspective of the current agent (this).
* Needs an instantiated gamygdala object (automatic when the agent is registered with Gamygdala.registerAgent(agent) to a Gamygdala instance).
* @method TUDelft.Gamygdala.Agent.appraise
* @param {TUDelft.Gamygdala.Belief} belief The belief to be appraised.
*/
TUDelft.Gamygdala.Agent.prototype.appraise = function(belief){
this.gamygdalaInstance.appraise(belief, this);
}
TUDelft.Gamygdala.Agent.prototype.updateEmotionalState = function(emotion){
for(var i = 0; i < this.internalState.length; i++){
if(this.internalState[i].name === emotion.name){
//Appraisals simply add to the old value of the emotion
//So repeated appraisals without decay will result in the sum of the appraisals over time
//To decay the emotional state, call .decay(decayFunction), or simply use the facilitating function in Gamygdala setDecay(timeMS).
this.internalState[i].intensity+=emotion.intensity;
return;
}
}
//copy on keep, we need to maintain a list of current emotions for the state, not a list references to the appraisal engine
this.internalState.push(new TUDelft.Gamygdala.Emotion(emotion.name, emotion.intensity));
};
/**
* This function returns either the state as is (gain=false) or a state based on gained limiter (limited between 0 and 1), of which the gain can be set by using setGain(gain).
* A high gain factor works well when appraisals are small and rare, and you want to see the effect of these appraisals
* A low gain factor (close to 0 but in any case below 1) works well for high frequency and/or large appraisals, so that the effect of these is dampened.
* @method TUDelft.Gamygdala.Agent.getEmotionalState
* @param {boolean} useGain Whether to use the gain function or not.
* @return {TUDelft.Gamygdala.Emotion[]} An array of emotions.
*/
TUDelft.Gamygdala.Agent.prototype.getEmotionalState = function(useGain){
if (useGain){
var gainState=[];
for (var i=0;i<this.internalState.length;i++){
var gainEmo=(this.gain*this.internalState[i].intensity)/(this.gain*this.internalState[i].intensity+1);
gainState.push(new TUDelft.Gamygdala.Emotion(this.internalState[i].name, gainEmo));
}
return gainState;
} else
return this.internalState;
};
/**
* This function returns a summation-based Pleasure Arousal Dominance mapping of the emotional state as is (gain=false), or a PAD mapping based on a gained limiter (limited between 0 and 1), of which the gain can be set by using setGain(gain).
* It sums over all emotions the equivalent PAD values of each emotion (i.e., [P,A,D]=SUM(Emotion_i([P,A,D])))), which is then gained or not.
* A high gain factor works well when appraisals are small and rare, and you want to see the effect of these appraisals.
* A low gain factor (close to 0 but in any case below 1) works well for high frequency and/or large appraisals, so that the effect of these is dampened.
* @method TUDelft.Gamygdala.Agent.getPADState
* @param {boolean} useGain Whether to use the gain function or not.
* @return {double[]} An array of doubles with Pleasure at index 0, Arousal at index [1] and Dominance at index [2].
*/
TUDelft.Gamygdala.Agent.prototype.getPADState = function(useGain){
var PAD=[];
PAD[0]=0;
PAD[1]=0;
PAD[2]=0;
for (var i=0;i<this.internalState.length;i++){
PAD[0]+=(this.internalState[i].intensity*this.mapPAD[this.internalState[i].name][0]);
PAD[1]+=(this.internalState[i].intensity*this.mapPAD[this.internalState[i].name][1]);
PAD[2]+=(this.internalState[i].intensity*this.mapPAD[this.internalState[i].name][2]);
}
if (useGain){
PAD[0]=(PAD[0]>=0?this.gain*PAD[0]/(this.gain*PAD[0]+1):-this.gain*PAD[0]/(this.gain*PAD[0]-1));
PAD[1]=(PAD[1]>=0?this.gain*PAD[1]/(this.gain*PAD[1]+1):-this.gain*PAD[1]/(this.gain*PAD[1]-1));
PAD[2]=(PAD[2]>=0?this.gain*PAD[2]/(this.gain*PAD[2]+1):-this.gain*PAD[2]/(this.gain*PAD[2]-1));
return PAD;
} else
return PAD;
};
/**
* This function prints to the console either the state as is (gain=false) or a state based on gained limiter (limited between 0 and 1), of which the gain can be set by using setGain(gain).
* A high gain factor works well when appraisals are small and rare, and you want to see the effect of these appraisals
* A low gain factor (close to 0 but in any case below 1) works well for high frequency and/or large appraisals, so that the effect of these is dampened.
* @method TUDelft.Gamygdala.Agent.printEmotionalState
* @param {boolean} useGain Whether to use the gain function or not.
*/
TUDelft.Gamygdala.Agent.prototype.printEmotionalState = function(useGain){
var output=this.name + ' feels ';
var i;
var emotionalState=this.getEmotionalState(useGain);
for (i=0;i<emotionalState.length; i++){
output+=emotionalState[i].name+":"+emotionalState[i].intensity+", ";
}
if (i>0)
console.log(output);
}
/**
* Sets the relation this agent has with the agent defined by agentName. If the relation does not exist, it will be created, otherwise it will be updated.
* @method TUDelft.Gamygdala.Agent.updateRelation
* @param {String} agentName The agent who is the target of the relation.
* @param {double} like The relation (between -1 and 1).
*/
TUDelft.Gamygdala.Agent.prototype.updateRelation = function(agentName, like){
if(!this.hasRelationWith(agentName)){
//This relation does not exist, just add it.
this.currentRelations.push(new TUDelft.Gamygdala.Relation(agentName,like));
}else {
//The relation already exists, update it.
for(var i = 0; i < this.currentRelations.length; i++){
if(this.currentRelations[i].agentName === agentName){
this.currentRelations[i].like = like;
}
}
}
};
/**
* Checks if this agent has a relation with the agent defined by agentName.
* @method TUDelft.Gamygdala.Agent.hasRelationWith
* @param {String} agentName The agent who is the target of the relation.
* @param {boolean} True if the relation exists, otherwise false.
*/
TUDelft.Gamygdala.Agent.prototype.hasRelationWith = function (agentName){
return (this.getRelation(agentName)!=null);
};
/**
* Returns the relation object this agent has with the agent defined by agentName.
* @method TUDelft.Gamygdala.Agent.getRelation
* @param {String} agentName The agent who is the target of the relation.
*/
TUDelft.Gamygdala.Agent.prototype.getRelation = function (agentName){
for(var i = 0; i < this.currentRelations.length; i++){
if(this.currentRelations[i].agentName === agentName){
return this.currentRelations[i];
}
}
return null;
};
/**
* Returns the relation object this agent has with the agent defined by agentName.
* @method TUDelft.Gamygdala.Agent.printRelations
* @param {String} [agentName] The agent who is the target of the relation will only be printed, or when omitted all relations are printed.
*/
TUDelft.Gamygdala.Agent.prototype.printRelations = function (agentName){
var output=this.name+ ' has the following sentiments:\n ';
var i;
var found=false;
for(i = 0; i < this.currentRelations.length; i++){
if(agentName==null || this.currentRelations[i].agentName === agentName){
for (var j=0;j<this.currentRelations[i].emotionList.length;j++){
output+=this.currentRelations[i].emotionList[j].name+'('+this.currentRelations[i].emotionList[j].intensity+') ';
found=true;
}
}
output+=' for '+this.currentRelations[i].agentName;
if (i<this.currentRelations.length-1)
output+=', and\n ';
}
if (found)
console.log(output);
};
/**
* This method decays the emotional state and relations according to the decay factor and function defined in gamygdala.
* Typically this is called automatically when you use startDecay() in Gamygdala, but you can use it yourself if you want to manage the timing.
* This function is keeping track of the millis passed since the last call, and will (try to) keep the decay close to the desired decay factor, regardless the time passed
* So you can call this any time you want (or, e.g., have the game loop call it, or have e.g., Phaser call it in the plugin update, which is default now).
* Further, if you want to tweak the emotional intensity decay of individual agents, you should tweak the decayFactor per agent not the "frame rate" of the decay (as this doesn't change the rate).
* @method TUDelft.Gamygdala.decayAll
* @param {TUDelft.Gamygdala} gamygdalaInstance A reference to the correct gamygdala instance that contains the decayFunction property to be used )(so you could use different gamygdala instances to manage different groups of agents)
*/
TUDelft.Gamygdala.Agent.prototype.decay = function(gamygdalaInstance){
for(var i= 0; i < this.internalState.length; i++){
var newIntensity=gamygdalaInstance.decayFunction(this.internalState[i].intensity);
if( newIntensity < 0){
this.internalState.splice(i,1);
} else{
this.internalState[i].intensity = newIntensity;
}
}
for (var i=0;i<this.currentRelations.length;i++)
this.currentRelations[i].decay(gamygdalaInstance);
};
/**
* This is the class that represents a relation one agent has with other agents.
* It's main role is to store and manage the emotions felt for a target agent (e.g angry at, or pity for).
* Each agent maintains a list of relations, one relation for each target agent.
* @class TUDelft.Gamygdala.Relation
* @constructor
* @param {String} targetName The agent who is the target of the relation.
* @param {double} relation The relation [-1 and 1].
*/
TUDelft.Gamygdala.Relation = function (targetName, like) {
this.agentName = targetName ;
this.like = like;
this.emotionList = [];
};
TUDelft.Gamygdala.Relation.prototype.addEmotion = function(emotion) {
var added = false;
for (var i = 0; i < this.emotionList.length; i++){
if (this.emotionList[i].name === emotion.name){
/*
if (this.emotionList[i].intensity < emotion.intensity){
this.emotionList[i].intensity = emotion.intensity;
}*/
this.emotionList[i].intensity += emotion.intensity;
added = true;
}
}
if(added === false){
//copy on keep, we need to maintain a list of current emotions for the relation, not a list refs to the appraisal engine
this.emotionList.push(new TUDelft.Gamygdala.Emotion(emotion.name, emotion.intensity));
}
};
TUDelft.Gamygdala.Relation.prototype.decay = function(gamygdalaInstance){
for (var i = 0; i < this.emotionList.length; i++){
var newIntensity=gamygdalaInstance.decayFunction(this.emotionList[i].intensity);
if (newIntensity < 0){
//This emotion has decayed below zero, we need to remove it.
this.emotionList.splice(i, 1);
}
else {
this.emotionList[i].intensity = newIntensity;
}
}
};
/**
* This class is a data structure to store one Belief for an agent
* A belief is created and fed into a Gamygdala instance (method Gamygdala.appraise()) for evaluation
* @class TUDelft.Gamygdala.Belief
* @constructor
* @param {double} likelihood The likelihood of this belief to be true.
* @param {String} causalAgentName The agent's name of the causal agent of this belief.
* @param {String[]} affectedGoalNames An array of affected goals' names.
* @param {double[]} goalCongruences An array of the affected goals' congruences (i.e., the extend to which this event is good or bad for a goal [-1,1]).
* @param {boolean} [isIncremental] Incremental evidence enforces gamygdala to see this event as incremental evidence for (or against) the list of goals provided, i.e, it will add or subtract this belief's likelihood*congruence from the goal likelihood instead of using the belief as "state" defining the absolute likelihood
*/
TUDelft.Gamygdala.Belief = function(likelihood, causalAgentName, affectedGoalNames, goalCongruences, isIncremental) {
if (isIncremental)
this.isIncremental=isIncremental;//incremental evidence enforces gamygdala to use the likelihood as delta, i.e, it will add or subtract this belief's likelihood from the goal likelihood instead of using the belief as "state" defining the absolute likelihood
else
this.isIncremental=false;
this.likelihood = Math.min(1,Math.max(-1,likelihood));
this.causalAgentName = causalAgentName;
this.affectedGoalNames = [];
this.goalCongruences = [];
//copy on keep
for(var i = 0; i < affectedGoalNames.length; i++){
this.affectedGoalNames.push(affectedGoalNames[i]);
}
for(var i = 0; i < goalCongruences.length; i++){
this.goalCongruences.push(Math.min(1,Math.max(-1,goalCongruences[i])));
}
};
/**
* This class is mainly a data structure to store an emotion with its intensity
* @class TUDelft.Gamygdala.Emotion
* @constructor
* @param {String} name The string ref of the emotion
* @param {double} intensity The intensity at which the emotion is set upon construction.
*/
TUDelft.Gamygdala.Emotion = function (name, intensity) {
this.name = name;
this.intensity = intensity;
};
/**
* This class is mainly a data structure to store a goal with it's utility and likelihood of being achieved
* This is used as basis for interpreting Beliefs
* @class TUDelft.Gamygdala.Goal
* @constructor
* @param {String} name The name of the goal
* @param {double} utility The utility of the goal
* @param {boolean} [isMaintenanceGoal] Defines if the goal is a maintenance goal or not. The default is that the goal is an achievement goal, i.e., a goal that once it's likelihood reaches true (1) or false (-1) stays that way.
*/
TUDelft.Gamygdala.Goal = function(name, utility, isMaintenanceGoal){
this.name = name;
this.utility = utility;
this.likelihood = 0.5; //The likelihood is unknown at the start so it starts in the middle between disconfirmed (0) and confirmed (1)
this.calculateLikelyhood=false; //This is set to false, in which case gamygdala assumes beliefs (events) will be used to calculate the goal likelyhood by calculateDeltaLikelihood method. If set to true, instead gamygdala assumes this property is function that calculates the likelyhood, so you can bind this as follows
//yourGoal.calculateLikelyhood=yourGoalLikelyhoodFunction.bind(yourGoal, [param1, param2, etc...]); This results in yourGoal's calculateLikelyhood property to set to a function that is bound to both this goal object AND the params you need, enabling it to be evaluated by a param free call. (in fact you make your function a method of this goal instance)
if (isMaintenanceGoal)
this.isMaintenanceGoal=isMaintenanceGoal; //There are maintenance and achievement goals. When an achievement goal is reached (or not), this is definite (e.g., to a the promotion or not). A maintenance goal can become true/false indefinitely (e.g., to be well-fed)
else
this.isMaintenanceGoal=false;
}