-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathconversation.py
More file actions
1168 lines (1033 loc) · 55.4 KB
/
conversation.py
File metadata and controls
1168 lines (1033 loc) · 55.4 KB
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
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import random
import time
from event import Event
from evidence import Statement, Declaration, Lie, Eavesdropping
from belief import PersonMentalModel, DwellingPlaceModel, BusinessMentalModel
class Conversation(Event):
"""A conversation between two characters in a city."""
def __init__(self, initiator, recipient, phone_call=False, debug=True):
"""Initialize a Conversation object."""
super(Conversation, self).__init__(game=initiator.game)
self.game = initiator.game
self.productionist = self.game.dialogue_productionist # NLG module
self.impressionist = self.game.impressionist # NLU module
self.productionist.debug = debug
self.initiator = initiator
self.recipient = recipient
self.participants = [initiator, recipient]
self.phone_call = phone_call
self.locations = (self.initiator.location, self.recipient.location)
self.debug = debug
self.subject = Subject(conversation=self) # The subject of conversation at a given point
self.discontinued_subjects = set() # Discontinued subjects of conversation
self.turns = [] # A record of the conversation as an ordered list of its turns
self.over = False # Whether the conversation is over (gets set by Move.fire())
# Obligations and goals -- these get populated as frames are inherited from
self.obligations = {self.initiator: set(), self.recipient: set()}
self.goals = {self.initiator: set(), self.recipient: set()}
self.satisfied_goals = {self.initiator: set(), self.recipient: set()}
# self.terminated_goals = {self.initiator: set(), self.recipient: set()}
self.resolved_obligations = {self.initiator: set(), self.recipient: set()}
self.topics = set()
self.moves = set() # A record of all dialogue moves, which are used as planning operators for goals
# Inherit from conversational frames that pertain to the contexts of this conversation
self.frames = set()
self._init_inherit_from_frames()
# Prepare containers for evidence objects that may be instantiated, depending on
# whether knowledge is propagated as part of this conversation
self.declarations = set()
self.statements = set()
self.lies = set()
self.eavesdroppings = set()
# TEST BLOCK
initiator.mind.preoccupation = next(
p for p in initiator.mind.mental_models if p.type == 'person' and initiator.belief(p, 'first name')
)
def __str__(self):
"""Return string representation."""
s = (
"Conversation between {initiator_name} and {recipient_name} at {location_name} on {date}.".format(
initiator_name=self.initiator.name, recipient_name=self.recipient.name,
location_name=self.initiator.location.name, date=self.date
)
)
return s
def _init_inherit_from_frames(self):
"""Inherit goals and initial obligations from conversational frames pertaining to the contexts
of this conversation.
"""
config = self.initiator.game.config
for frame_name in config.conversational_frames:
preconditions_satisfied = config.conversational_frames[frame_name]['preconditions'](
conversation=self
)
if preconditions_satisfied:
# Adopt frame
frame = Frame(conversation=self, name=frame_name)
self.frames.add(frame)
# Inherit its obligations
self.obligations[self.initiator] |= frame.obligations[self.initiator]
self.obligations[self.recipient] |= frame.obligations[self.recipient]
# Inherit its goals
self.goals[self.initiator] |= frame.goals[self.initiator]
self.goals[self.recipient] |= frame.goals[self.recipient]
@property
def speaker(self):
"""Return the current speaker."""
if self.turns:
return self.turns[-1].speaker
else:
return None
@property
def interlocutor(self):
"""Return the current interlocutor."""
return self.interlocutor_to(self.speaker)
@property
def completed_turns(self):
"""Return all turns that have already been completed."""
return [turn for turn in self.turns if hasattr(turn, 'line_of_dialogue')]
@property
def last_turn(self):
"""Return the last completed turn."""
completed_turns = self.completed_turns
return None if not completed_turns else completed_turns[-1]
@property
def last_speaker_turn(self):
"""Return the last turn completed by the current speaker."""
all_completed_speaker_turns = [
turn for turn in self.turns if hasattr(turn, 'line_of_dialogue') and turn.speaker is self.speaker
]
if all_completed_speaker_turns:
return all_completed_speaker_turns[-1]
else:
return None
@property
def last_interlocutor_turn(self):
"""Return the last turn completed by the current interlocutor."""
all_completed_interlocutor_turns = [
turn for turn in self.turns if hasattr(turn, 'line_of_dialogue') and turn.speaker is self.interlocutor
]
if all_completed_interlocutor_turns:
return all_completed_interlocutor_turns[-1]
else:
return None
@property
def goals_not_on_hold(self):
"""Return a dictionary listing the active goals for each conversational party whose plans are not on hold."""
goals_not_on_hold = {
self.initiator: {goal for goal in self.goals[self.initiator] if not goal.plan.on_hold},
self.recipient: {goal for goal in self.goals[self.recipient] if not goal.plan.on_hold}
}
return goals_not_on_hold
@property
def speaker_subject_match(self):
"""Return the speaker's match for the subject of conversation, if any."""
return self.subject.matches[self.speaker]
def interlocutor_to(self, speaker):
"""Return the interlocutor to the given speaker."""
return self.initiator if self.recipient is speaker else self.recipient
def outline(self):
"""Outline the conversational frames underpinning this conversation, including the
obligations and goals that they assert.
"""
for frame in self.frames:
frame.outline(n_tabs=1)
def restart(self):
"""Return a new Conversation object with the same context as this one."""
return Conversation(
initiator=self.initiator, recipient=self.recipient, phone_call=self.phone_call, debug=self.debug
)
def replay(self):
"""Replay the conversation by printing out each of its lines."""
for turn in self.turns:
print turn
def transpire(self):
"""Carry out the entire conversation."""
while not self.over:
if any(p for p in self.participants if p.player):
time.sleep(0.6)
self.proceed()
# self.replay()
for turn in self.turns:
print '\n{}\n'.format(turn)
def proceed(self):
"""Proceed with the conversation by advancing one turn."""
if not self.over:
next_speaker, targeted_obligation, targeted_goal = self.allocate_turn()
Turn(
conversation=self, speaker=next_speaker,
targeted_obligation=targeted_obligation,
targeted_goal=targeted_goal
)
def allocate_turn(self):
"""Allocate the next turn."""
targeted_obligation = None
targeted_goal = None
# If both conversational parties have obligations, randomly allocate the turn
if self.obligations[self.initiator] and self.obligations[self.recipient]:
next_speaker = random.choice(self.participants)
targeted_obligation = list(self.obligations[next_speaker])[0]
if self.debug:
print (
'[Both speakers currently have obligations. Randomly allocating turn according to {}]'.format(
targeted_obligation
)
)
# If the initiator has obligations, allocate the turn to them
elif self.obligations[self.initiator]:
next_speaker = self.initiator
targeted_obligation = list(self.obligations[next_speaker])[0]
if self.debug:
print '[Allocating turn according to {}]'.format(targeted_obligation)
# If the recipient has obligations, allocate the turn to them
elif self.obligations[self.recipient]:
next_speaker = self.recipient
targeted_obligation = list(self.obligations[next_speaker])[0]
if self.debug:
print '[Allocating turn according to {}]'.format(targeted_obligation)
# If both conversational parties have goals whose plans are not on hold, allocate randomly
elif self.goals_not_on_hold[self.initiator] and self.goals_not_on_hold[self.recipient]:
next_speaker = random.choice(self.participants)
targeted_goal = list(self.goals_not_on_hold[next_speaker])[0]
# If the initiator has a goal whose plan is not on hold, allocate to them
elif self.goals_not_on_hold[self.initiator]:
next_speaker = self.initiator
targeted_goal = list(self.goals_not_on_hold[next_speaker])[0]
if self.debug:
print '[Allocating turn according to {}]'.format(targeted_goal)
# If the recipient has a goal whose plan is not on hold, allocate to them
elif self.goals_not_on_hold[self.recipient]:
next_speaker = self.recipient
targeted_goal = list(self.goals_not_on_hold[next_speaker])[0]
if self.debug:
print '[Allocating turn according to {}]'.format(targeted_goal)
# If there are no obligations or unheld goals, probabilistically allocate the
# turn with consideration given to the parties' relative extroversion values
# TODO IMPROVE THE REASONING ABOUT ALLOCATION HERE
else:
if random.random() < 0.75:
next_speaker = max(self.participants, key=lambda p: p.personality.extroversion)
else:
next_speaker = min(self.participants, key=lambda p: p.personality.extroversion)
if self.debug:
print '[No obligations or goals, so probabilistically allocated turn to {}]'.format(next_speaker.name)
return next_speaker, targeted_obligation, targeted_goal
def understand_player_utterance(self, player_utterance):
"""Request that Impressionist process a player's free-text dialogue input.
This method will furnish an instantiated Impressionist.LineOfDialogue object, which will come
with all of the necessary semantic and pragmatic information already attributed.
"""
if self.debug:
print "[A request has been made to Impressionist to process the player utterance '{}']".format(
player_utterance
)
return self.impressionist.understand_player_utterance(player_utterance=player_utterance, conversation=self)
def target_move(self, move_name):
"""Request that Productionist generate a line of dialogue that may be used to perform a
targeted dialogue move.
This method will furnish an instantiated Productionist.LineOfDialogue object, which will come
with all of the necessary semantic and pragmatic information already attributed.
"""
if self.debug:
print "[A request has been made to Productionist to generate a line that will perform MOVE:{}]".format(
move_name
)
return self.productionist.target_dialogue_move(conversation=self, move_name=move_name)
def target_topic(self, topics=None):
"""Request that Productionist generate a line of dialogue that may be used to address one of the
given topics of conversation.
If particular topics are specified, this method will request a line that addresses any one
of them; otherwise, it will request a line that addresses any active topic.
"""
# TODO PUT TOPICS IN ORDER OF MOST RECENT TO LEAST RECENT
if self.debug:
print "[{} is requesting that Productionist generate a line that will address a topic]".format(
self.speaker.first_name
)
topics = self.topics if not topics else topics
topic_names = {topic.name for topic in topics}
return self.productionist.target_topics_of_conversation(conversation=self, topic_names=topic_names)
def count_move_occurrences(self, acceptable_speakers, name):
"""Count the number of times the acceptable speakers have performed a dialogue move with the given name."""
moves_meeting_the_specification = [
move for move in self.moves if move.speaker in acceptable_speakers and move.name == name
]
return len(moves_meeting_the_specification)
def earlier_move(self, speaker, name):
"""Return whether speaker has already performed a dialogue move with the given name."""
relevant_speakers = self.participants if speaker == 'either' else (speaker,)
return any(move for move in self.moves if move.speaker in relevant_speakers and move.name == name)
def turns_since_earlier_move(self, speaker, name):
"""Return the number of turns that have been completed since speaker performed a dialogue
move with the given name.
"""
relevant_speakers = self.participants if speaker == 'either' else (speaker,)
earlier_turns_that_performed_that_move = [
turn for turn in self.completed_turns if any(
move for move in turn.moves_performed if move.speaker in relevant_speakers and move.name == name
)
]
latest_such_turn = max(earlier_turns_that_performed_that_move, key=lambda t: t.index)
turns_completed_since_that_turn = self.completed_turns[-1].index - latest_such_turn.index
return turns_completed_since_that_turn
def last_speaker_move(self, name):
"""Return whether the interlocutor's last move was one with the given name."""
return self.speaker and self.last_speaker_turn.performed_move(name=name)
def last_interlocutor_move(self, name):
"""Return whether the interlocutor's last move was one with the given name."""
return self.last_interlocutor_turn and self.last_interlocutor_turn.performed_move(name=name)
def turns_taken(self, speaker):
"""Return the number of turns taken so far by the given speaker."""
return len([turn for turn in self.turns if turn.speaker is speaker])
def has_obligation(self, conversational_party, move_name):
"""Return whether the conversational party currently has an obligation to perform a move with the given name."""
return any(o for o in self.obligations[conversational_party] if o.move_name == move_name)
def no_obligation(self, conversational_party, move_name):
"""Return whether the conversational party currently has no obligation to perform a move with the given name."""
return not any(o for o in self.obligations[conversational_party] if o.move_name == move_name)
def outstanding_obligations(self):
"""Return whether there are any outstanding conversational obligations."""
return self.obligations[self.initiator] or self.obligations[self.recipient]
def already_a_topic(self, name):
"""Return whether there is already an active topic with the given name."""
return any(t for t in self.topics if t.name == name)
def get_evidence_object(self, evidence_type, subject, source, recipient, eavesdropper=None):
"""Return an evidence object satisfying the given criteria, if one has already been instantiated."""
if evidence_type == 'declaration':
try:
evidence_object = next(
d for d in self.declarations if d.subject == subject and d.source == source and
d.recipient == recipient
)
except StopIteration:
evidence_object = None
elif evidence_type == 'statement':
try:
evidence_object = next(
s for s in self.statements if s.subject == subject and s.source == source and
s.recipient == recipient
)
except StopIteration:
evidence_object = None
elif evidence_type == 'lie':
try:
evidence_object = next(
l for l in self.statements if l.subject == subject and l.source == source and
l.recipient == recipient
)
except StopIteration:
evidence_object = None
else: # evidence_type == 'eavesdropping'
try:
evidence_object = next(
l for l in self.eavesdroppings if l.subject == subject and l.source == source and
l.recipient == recipient and eavesdropper == eavesdropper
)
except StopIteration:
evidence_object = None
return evidence_object
class Turn(object):
"""An utterance delivered by one character to another; a unit of conversation."""
def __init__(self, conversation, speaker, targeted_obligation, targeted_goal):
"""Initialize an Turn object."""
self.conversation = conversation
self.speaker = speaker
self.interlocutor = conversation.interlocutor_to(speaker)
self.subject = conversation.subject
self.propositions = set()
self.targeted_obligation = targeted_obligation
self.targeted_goal = targeted_goal
self.moves_performed = set()
self.topics_addressed = set()
self.obligations_resolved = set()
self.index = len(conversation.turns)
self.conversation.turns.append(self)
self.realization = '' # Dialogue template as it was filled in during this turn
if self.speaker.player:
self.line_of_dialogue = self._process_player_dialogue_input()
else: # Speaker is an NPC
self.line_of_dialogue = self._decide_what_to_say()
self._realize_line_of_dialogue()
self.eavesdropper = self._potentially_be_eavesdropped()
self._update_conversation_state()
def __str__(self):
"""Return string representation."""
return '{}: {}'.format(self.speaker.name, self.realization)
def _process_player_dialogue_input(self):
"""Process the player's free-text dialogue input to instantiate a line of dialogue."""
# Ask the player to provide her next utterance
raw_player_utterance = self._solicit_player_utterance()
# Ask the conversation object associated with this turn to ask its Impressionist to
# process this line; this will furnish an Impressionist.LineOfDialogue object that has
# all the semantic and pragmatic information that we need, as well as a realize() method,
# which will simply print out the player's utterance
line_of_dialogue_object = self.conversation.understand_player_utterance(
player_utterance=raw_player_utterance
)
return line_of_dialogue_object
def _solicit_player_utterance(self):
"""Solicit free-text input from the player."""
prompt = "\n{player_character_name}: ".format(player_character_name=self.speaker.name)
raw_player_utterance = raw_input(prompt)
print '' # To closest_match the style of how NPC lines of dialogue are displayed
return raw_player_utterance
def _decide_what_to_say(self):
"""Have the speaker select a line of dialogue to deploy on this turn."""
if self.targeted_obligation:
selected_line = self.targeted_obligation.target()
elif self.targeted_goal:
if self.conversation.debug:
print "[{} is searching for a line that will achieve {}]".format(
self.conversation.speaker.first_name, self.targeted_goal
)
selected_line = self.targeted_goal.target()
elif self.conversation.topics:
if self.conversation.debug:
print "[{} is searching for a line that will address a relevant topic]".format(
self.speaker.first_name, self.targeted_goal
)
selected_line = self.conversation.target_topic()
else:
# Either engage in small talk or adopt a goal to end the conversation
if random.random() < max(self.speaker.personality.extroversion, 0.05): # TODO PUT MAGIC NUMBER IN CONFIG.PY
selected_line = self.conversation.target_move(move_name='make small talk')
else:
new_goal_to_end_conversation = Goal(
conversation=self.conversation, owner=self.speaker, name='END CONVERSATION'
)
self.conversation.goals[self.speaker].add(new_goal_to_end_conversation)
self.targeted_goal = new_goal_to_end_conversation
selected_line = self._decide_what_to_say() # Which will now target the new goal
if selected_line is None:
# You couldn't find a viable line, so just adopt and target a goal to
# end the conversation
new_goal_to_end_conversation = Goal(
conversation=self.conversation, owner=self.speaker, name='END CONVERSATION'
)
self.targeted_goal = new_goal_to_end_conversation
return self._decide_what_to_say()
else:
return selected_line
def _realize_line_of_dialogue(self):
"""Display the line of dialogue on screen."""
self.realization = self.line_of_dialogue.realize(conversation=self.conversation)
# If the speaker is an NPC, print their line out; if it's a player, the line
# has already been made visible from the player typing it
if not self.speaker.player:
print '\n{name}: {line}\n'.format(name=self.speaker.name, line=self.realization)
def _potentially_be_eavesdropped(self):
"""Potentially have the line of dialogue asserting this proposition be eavesdropped by a nearby character."""
# TODO maybe affect this by how salient subject is to eavesdropper
people_in_earshot = self.conversation.speaker.location.people_here_now - {self.speaker, self.interlocutor}
eavesdropper = None if not people_in_earshot else random.choice(list(people_in_earshot))
if eavesdropper and random.random() < self.speaker.game.config.chance_someone_eavesdrops_statement_or_lie:
if self.conversation.debug:
print '-- Eavesdropped by {}'.format(eavesdropper.name)
return eavesdropper
else:
return None
def _update_conversation_state(self):
"""Update the conversation state and have the interlocutor consider any propositions."""
self._update_context()
self._assert_propositions()
self._reify_dialogue_moves()
self._satisfy_goals()
self._resolve_obligations()
self._push_obligations()
self._push_topics()
self._address_topics()
self._fire_dialogue_moves()
def _update_context(self):
"""Update the common-ground conversational context, which holds information about the subject of
conversation and other concerns.
"""
self._update_subject_of_conversation()
def _update_subject_of_conversation(self):
"""Update common-ground information surrounding the subject of conversation."""
line = self.line_of_dialogue
# Potentially discontinue the current subject of conversation
if line.clear_subject_of_conversation:
self.conversation.discontinued_subjects.add(self.conversation.subject)
self.conversation.subject = Subject(conversation=self.conversation)
self.subject = self.conversation.subject
# Push new features regarding the subject
subject_updates = [u[8:] for u in line.context_updates if u[:8] == 'subject:']
self.conversation.subject.update(
new_features=subject_updates,
force_match_to_speaker_preoccupation=line.force_speaker_subject_match_to_speaker_preoccupation
)
def _assert_propositions(self):
"""Assert propositions about the world that are expressed by the content of the generated line."""
for proposition_specification in self.line_of_dialogue.propositions:
# TODO INFER LIES VIA VIOLATIONS REASONING (PROB SHOULD JUST HAVE SEPARATE LIE-CONDITIONS TAGSET)
proposition_object = Proposition(
conversation=self.conversation, this_is_a_lie=False, specification=proposition_specification
)
self.propositions.add(proposition_object)
def _reify_dialogue_moves(self):
"""Instantiate objects for the dialogue moves performed on this turn."""
for move_name in self.line_of_dialogue.moves:
move_object = Move(conversation=self.conversation, speaker=self.speaker, name=move_name)
self.conversation.moves.add(move_object)
self.moves_performed.add(move_object)
def _satisfy_goals(self):
"""Satisfy any goals whose targeted move was constituted by the execution of this turn."""
# Satisfy speaker goals
for goal in list(self.conversation.goals[self.speaker]):
if goal.achieved:
self.conversation.goals[self.speaker].remove(goal)
self.conversation.satisfied_goals[self.speaker].add(goal)
if self.conversation.debug:
print '-- Satisfied {}'.format(goal)
# Satisfy interlocutor goals
for goal in list(self.conversation.goals[self.interlocutor]):
if goal.achieved:
self.conversation.goals[self.interlocutor].remove(goal)
self.conversation.satisfied_goals[self.interlocutor].add(goal)
if self.conversation.debug:
print '-- Satisfied {}'.format(goal)
def _resolve_obligations(self):
"""Resolve any conversational obligations according to the mark-up of the generated line."""
# Resolve speaker obligations
for move_name in self.line_of_dialogue.moves:
if any(obligation for obligation in self.conversation.obligations[self.speaker] if
obligation.move_name == move_name):
obligation_to_resolve = next(
obligation for obligation in self.conversation.obligations[self.speaker] if
obligation.move_name == move_name
)
self.conversation.obligations[self.speaker].remove(obligation_to_resolve)
self.conversation.resolved_obligations[self.speaker].add(obligation_to_resolve)
self.obligations_resolved.add(obligation_to_resolve)
if self.conversation.debug:
print '-- Resolved {}'.format(obligation_to_resolve)
# Resolve interlocutor obligations
# TODO SUPPORT THIS ONCE YOU HAVE A USE CASE
def _push_obligations(self):
"""Push new conversational obligations according to the mark-up of this line."""
# Push speaker obligations
for obligation_name in self.line_of_dialogue.speaker_obligations_pushed:
obligation_object = Obligation(
conversation=self.conversation, obligated_party=self.speaker, move_name=obligation_name
)
self.conversation.obligations[self.speaker].add(obligation_object)
if self.conversation.debug:
print '-- Pushed {}'.format(obligation_object)
# Push interlocutor obligations
for obligation_name in self.line_of_dialogue.interlocutor_obligations_pushed:
obligation_object = Obligation(
conversation=self.conversation, obligated_party=self.interlocutor, move_name=obligation_name
)
self.conversation.obligations[self.interlocutor].add(obligation_object)
if self.conversation.debug:
print '-- Pushed {}'.format(obligation_object)
def _push_topics(self):
"""Push new topics of conversation according to the mark-up of this line."""
for topic_name in self.line_of_dialogue.topics_pushed:
if not any(t for t in self.conversation.topics if t.name == topic_name):
topic_object = Topic(name=topic_name)
self.conversation.topics.add(topic_object)
self.topics_addressed.add(topic_object)
if self.conversation.debug:
print '-- Pushed "{}"'.format(topic_object)
def _address_topics(self):
"""Address topics of conversation according to the mark-up of this line."""
for topic_name in self.line_of_dialogue.topics_addressed:
try:
topic_object = next(t for t in self.conversation.topics if t.name == topic_name)
self.topics_addressed.add(topic_object)
if self.conversation.debug:
print '-- Addressed "{}"'.format(topic_object)
except StopIteration: # Topic has not been introduced yet
if self.speaker.player: # If the speaker is a player character, let it slide
pass
else:
raise Exception(
"{speaker} is attempting to address a topic ({topic}) that has not yet been introduced.".format(
speaker=self.speaker.name,
topic=topic_name
)
)
def _fire_dialogue_moves(self):
"""Fire rules associated with the dialogue moves performed on this turn."""
for move in self.moves_performed:
move.fire()
def performed_move(self, name):
"""Return whether this turn performed a move with the given name."""
return any(m for m in self.moves_performed if m.name == name)
def did_not_perform_move(self, name):
"""Return whether this turn did *not* perform a move with the given name."""
return not any(m for m in self.moves_performed if m.name == name)
def addressed_topic(self, name):
"""Return whether this turn addressed a topic with the given name."""
return any(t for t in self.topics_addressed if t.name == name)
def did_not_address_topic(self, name):
"""Return whether this turn did *not* address a topic with the given name."""
return not any(t for t in self.topics_addressed if t.name == name)
def resolved_obligation(self, name=None):
"""Return whether this turn resolved an obligation with the given name.
If None is passed for 'name', this method will return whether this turn resolved *any* obligation.
"""
if name:
return any(o for o in self.obligations_resolved if o.name == name)
else:
return True if self.obligations_resolved else False
class Move(object):
"""A dialogue move by a conversational party."""
def __init__(self, conversation, speaker, name):
"""Initialize a Move object."""
self.conversation = conversation
self.speaker = speaker
self.interlocutor = conversation.interlocutor_to(speaker)
self.name = name
if conversation.debug:
print '-- Performed {}'.format(self)
def __str__(self):
"""Return string representation."""
return "MOVE:{}:{}".format(self.speaker.name, self.name)
def fire(self):
"""Change the world according to the illocutionary force of this move."""
# If someone storms off, or both parties say goodbye (and neither has any
# outstanding obligations), end the conversation
if self.name == 'storm off':
self.conversation.over = True
elif self.name == "say goodbye back" and not self.conversation.outstanding_obligations():
self.conversation.over = True
class Obligation(object):
"""A conversational obligation imposed on one conversational party by a line of dialogue."""
def __init__(self, conversation, obligated_party, move_name):
"""Initialize an Obligation object."""
self.conversation = conversation
self.obligated_party = obligated_party
self.move_name = move_name # Name of the move that this obligates obligated_party to perform next
def __str__(self):
"""Return string representation."""
return 'OBLIGATION:{}:{}'.format(self.obligated_party.name, self.move_name)
def outline(self, n_tabs):
"""Outline this obligation for debugging purposes."""
print '{}{}'.format('\t'*n_tabs, self)
def target(self):
"""Select a line of dialogue that would resolve this obligation."""
return self.conversation.target_move(move_name=self.move_name)
class Proposition(object):
"""A proposition about the world asserted by the content of a line of dialogue."""
def __init__(self, conversation, this_is_a_lie, specification):
"""Initialize a Proposition object."""
self.conversation = conversation
# Attribute speaker and interlocutor as source and recipient of this proposition, respectively
self.source = conversation.speaker
self.recipient = conversation.interlocutor
# Parse the specification to resolve and assign our other crucial attributes
self.subject = None
self.feature_value = ''
self.feature_object_itself = None
self.feature_type = ''
self._init_parse_specification(specification=specification)
# Inherit eavesdropper of the current conversation turn, if any
self.eavesdropper = conversation.turns[-1].eavesdropper
# Instantiate and/or attribute evidence objects
self.declaration = None
self.statement = None
self.lie = None
self.eavesdropping = None
# Print debug statement
if conversation.debug:
print '-- Asserting {}...'.format(self)
# If they don't exist yet, establish mental models pertaining to the subject of this
# proposition that will be owned by its source, recipient, and eavesdropper (if there
# is one)
self._establish_mental_models_of_subject()
# Instantiate Declaration, Statement, Lie, and Eavesdropping evidence objects, as appropriate
self._instantiate_and_or_attribute_evidence_objects(this_is_a_lie=this_is_a_lie)
# Have the source, recipient, and eavesdropper (if any) of this proposition consider
# adopting a belief in response to it by evaluating the appropriate pieces of evidence
self._have_all_parties_consider_this_proposition_as_evidence()
def __str__(self):
"""Return string representation."""
return 'PROPOSITION:{feature_type}({subject}, "{feature_value}")'.format(
feature_type=self.feature_type,
subject=self.subject.name,
feature_value=self.feature_value
)
def _init_parse_specification(self, specification):
"""Parse the specification for this proposition to set this object's individual specification attributes."""
subject, feature_type, feature_value, feature_object_itself = specification.split(';')
# Make sure the specification is well-formed
assert 'subject=' in subject, 'Ill-formed proposition specification: {}'.format(specification)
assert 'feature_type=' in feature_type, 'Ill-formed proposition specification: {}'.format(specification)
assert 'feature_value=' in feature_value, 'Ill-formed proposition specification: {}'.format(specification)
assert 'feature_object_itself=' in feature_object_itself, (
'Ill-formed proposition specification: {}'.format(specification)
)
# Parse the individual elements of the specification
subject_ref = subject[len('subject='):]
feature_type = feature_type[len('feature_type='):]
feature_value_ref = feature_value[len('feature_value='):]
feature_object_itself_ref = feature_object_itself[len('feature_object_itself='):]
# Evaluate the references to resolve to attributes for this object (this requires
# us to pull in some variables from the conversational context)
speaker, interlocutor, subject = (
self.conversation.speaker, self.conversation.interlocutor, self.conversation.subject
)
self.subject = eval(subject_ref)
self.feature_value = eval(feature_value_ref)
self.feature_object_itself = eval(feature_object_itself_ref)
# Feature type doesn't need to be evaluated (it's just a string), so attribute it as is
self.feature_type = feature_type
def _establish_mental_models_of_subject(self):
"""If necessary, reify mental models pertaining to the subject of this proposition that will be owned by its
source, recipient, and eavesdropper (if any)."""
if self.subject not in self.source.mind.mental_models:
PersonMentalModel(owner=self.source, subject=self.subject, observation_or_reflection=None)
if self.subject not in self.recipient.mind.mental_models:
PersonMentalModel(owner=self.recipient, subject=self.subject, observation_or_reflection=None)
if self.eavesdropper:
if self.subject not in self.eavesdropper.mind.mental_models:
PersonMentalModel(owner=self.eavesdropper, subject=self.subject, observation_or_reflection=None)
def _instantiate_and_or_attribute_evidence_objects(self, this_is_a_lie):
"""Instantiate and/or attribute Declaration and Statement/Lie evidence objects."""
self._instantiate_and_or_attribute_declaration_object()
if self.conversation.debug:
print "\t- Reified declaration piece of evidence"
if this_is_a_lie:
self._instantiate_and_or_attribute_lie_object()
if self.conversation.debug:
print "\t- Reified lie piece of evidence"
else:
self._instantiate_and_or_attribute_statement_object()
if self.conversation.debug:
print "\t- Reified statement piece of evidence"
if self.eavesdropper:
self._instantiate_and_or_adopt_eavesdropping_object()
if self.conversation.debug:
print "\t- Reified eavesdropping piece of evidence ({} eavesdropped)".format(
self.eavesdropper.name
)
def _instantiate_and_or_attribute_declaration_object(self):
"""Instantiate and/or attribute a Declaration object."""
declaration_object = self.conversation.get_evidence_object(
evidence_type='declaration', source=self.source, subject=self.subject, recipient=self.recipient,
)
if declaration_object:
self.declaration = declaration_object
else:
declaration_object = Declaration(subject=self.subject, source=self.source, recipient=self.recipient)
self.declaration = declaration_object
self.conversation.declarations.add(declaration_object)
def _instantiate_and_or_attribute_statement_object(self):
"""Instantiate and/or attribute a Statement object."""
statement_object = self.conversation.get_evidence_object(
evidence_type='statement', source=self.source, subject=self.subject, recipient=self.recipient,
)
if statement_object:
self.statement = statement_object
else:
statement_object = Statement(subject=self.subject, source=self.source, recipient=self.recipient)
self.statement = statement_object
self.conversation.statements.add(statement_object)
def _instantiate_and_or_attribute_lie_object(self):
"""Instantiate and/or attribute a Lie object."""
lie_object = self.conversation.get_evidence_object(
evidence_type='lie', source=self.source, subject=self.subject, recipient=self.recipient,
)
if lie_object:
self.lie = lie_object
else:
lie_object = Lie(subject=self.subject, source=self.source, recipient=self.recipient)
self.lie = lie_object
self.conversation.lies.add(lie_object)
def _instantiate_and_or_adopt_eavesdropping_object(self):
"""Instantiate and/or attribute a Eavesdropping object."""
# Instantiate and/or attribute the object
eavesdropping_object = self.conversation.get_evidence_object(
evidence_type='eavesdropping', source=self.source, subject=self.subject, recipient=self.recipient,
eavesdropper=self.eavesdropper
)
if eavesdropping_object:
self.eavesdropping = eavesdropping_object
else:
eavesdropping_object = Eavesdropping(
subject=self.subject, source=self.source, recipient=self.recipient, eavesdropper=self.eavesdropper
)
self.eavesdropping = eavesdropping_object
self.conversation.eavesdroppings.add(eavesdropping_object)
def _have_all_parties_consider_this_proposition_as_evidence(self):
"""Have the source, recipient, and eavesdropper (if any) of this proposition consider adopting a
belief in response to it by evaluating the appropriate pieces of evidence
"""
# Have the recipient consider the Statement object conveying this proposition
self.recipient.mind.mental_models[self.subject].consider_new_evidence(
feature_type=self.feature_type, feature_value=self.feature_value,
feature_object_itself=self.feature_object_itself, new_evidence=self.statement
)
# Have the source of this proposition reinforce their own belief with a Declaration object
self.source.mind.mental_models[self.subject].consider_new_evidence(
feature_type=self.feature_type, feature_value=self.feature_value,
feature_object_itself=self.feature_object_itself, new_evidence=self.declaration
)
# If someone is eavesdropping, have them consider this proposition via an Eavesdropping object
if self.eavesdropping:
self.eavesdropper.mind.mental_models[self.subject].consider_new_evidence(
feature_type=self.feature_type, feature_value=self.feature_value,
feature_object_itself=self.feature_object_itself, new_evidence=self.eavesdropping
)
if self.conversation.debug:
print "\t- All conversational parties considered their new evidence"
class Violation(object):
"""A violate of a conversational obligation or norm by a conversational party."""
def __init__(self):
"""Initialize a Violation of object."""
pass
class Flouting(object):
"""An intentional violation of a conversational obligation or norm,
This is an operationalization of the notion of a flouting in Grice's theory of
the cooperative principle, which is famous in linguistic pragmatics."""
def __init__(self):
"""Initialize a Flouting object."""
pass
class Goal(object):
"""A conversational goal held by a conversational party."""
def __init__(self, conversation, owner, name, required_number_of_occurrences=1):
"""Initialize a Goal object."""
self.conversation = conversation
self.owner = owner
self.name = name
self.required_number_of_occurrences = required_number_of_occurrences
self.plan = Plan(goal=self)
# Specification for the dialogue move that would satisfy this goal (and is thus
# the last step in this goal's plan)
self.move_acceptable_speakers = self.plan.steps[-1].move_acceptable_speakers
self.move_name = self.plan.steps[-1].move_name
def __str__(self):
"""Return string representation."""
return 'GOAL:{}:{}{}'.format(
self.owner.name,
self.name,
'' if self.required_number_of_occurrences == 1 else ' (x{})'.format(self.required_number_of_occurrences)
)
@property
def achieved(self):
"""Return whether this step has been achieved."""
move_occurrences_count = self.conversation.count_move_occurrences(
acceptable_speakers=self.move_acceptable_speakers, name=self.move_name
)
if move_occurrences_count >= self.required_number_of_occurrences:
return True
else:
return False
def outline(self, n_tabs):
"""Outline this goal for debugging purposes."""
print '{}{}'.format('\t'*n_tabs, self)
self.plan.outline(n_tabs+1)
def target(self):
"""Select a line of dialogue to target the achievement of this goal."""
if self.conversation.debug:
print "[{} is searching for a line that will resolve {}]".format(
self.conversation.speaker.first_name, self
)
return self.plan.execute()
class Plan(object):
"""A plan to achieve a conversational goal in the form of a sequence of steps."""
def __init__(self, goal):
"""Initialize a Plan object."""
self.conversation = goal.conversation
self.goal = goal
self.steps = self._init_steps()
def __str__(self):
"""Return string representation."""
return "PLAN:{}".format(self.goal)
def _init_steps(self):
"""Instantiate the steps in this plan according to the specifications of our config file.
The steps of a plan will be a sequence of Step and Goal objects, the latter of which will
have their own plans.
"""
steps = []
config = self.goal.owner.game.config
for move_speaker_ref, move_name, required_number_of_occurrences in config.conversational_goals[self.goal.name]:
if move_name in config.conversational_goals:
# Instantiate a Goal object for this subgoal, whose own plan will automatically be instantiated
steps.append(
Goal(
conversation=self.conversation, owner=self.goal.owner, name=move_name,
required_number_of_occurrences=required_number_of_occurrences
)
)
else:
# Instantiate a Step object
steps.append(
Step(
conversation=self.conversation, owner=self.goal.owner, move_speaker_ref=move_speaker_ref,
move_name=move_name, required_number_of_occurrences=required_number_of_occurrences
)
)
return steps
@property
def executed(self):
"""Return whether this plan has been fully executed, i.e., whether all its steps have been achieved."""
return all(step.achieved for step in self.steps)
@property
def on_hold(self):
"""Return whether this plan is on hold due to its next step having to be constituted
by the interlocutor performing some move.
"""
next_step = next(step for step in self.steps if not step.achieved)
whether_this_plan_is_on_hold = self.goal.owner not in next_step.move_acceptable_speakers
return whether_this_plan_is_on_hold
def outline(self, n_tabs):
"""Outline this plan for debugging purposes."""
for step in self.steps:
step.outline(n_tabs)
def execute(self):
"""Execute the next step in this plan."""
next_step = next(step for step in self.steps if not step.achieved)
assert not self.on_hold, (
"A call was made to the execute method of {}, but this plan is on hold.".format(self)
)
return next_step.target()
class Step(object):