From 9e1c50125c683e55176174ffac39086232cba2b2 Mon Sep 17 00:00:00 2001
From: Andreas Fischer <_@ndreas.de>
Date: Fri, 5 Jun 2020 17:50:12 +0200
Subject: [PATCH] Added exercise on hysteric eliza

---
 Software_Design_Patterns/doctor.txt           | 360 ++++++++++++++++++
 Software_Design_Patterns/eliza.py             | 236 ++++++++++++
 Software_Design_Patterns/hystericeliza.py     |  85 +++++
 .../test_hystericeliza.py                     |  61 +++
 4 files changed, 742 insertions(+)
 create mode 100644 Software_Design_Patterns/doctor.txt
 create mode 100644 Software_Design_Patterns/eliza.py
 create mode 100644 Software_Design_Patterns/hystericeliza.py
 create mode 100644 Software_Design_Patterns/test_hystericeliza.py

diff --git a/Software_Design_Patterns/doctor.txt b/Software_Design_Patterns/doctor.txt
new file mode 100644
index 0000000..6a89f31
--- /dev/null
+++ b/Software_Design_Patterns/doctor.txt
@@ -0,0 +1,360 @@
+initial: How do you do.  Please tell me your problem.
+final: Goodbye.  Thank you for talking to me.
+quit: bye
+quit: goodbye
+quit: quit
+pre: dont don't
+pre: cant can't
+pre: wont won't
+pre: recollect remember
+pre: dreamt dreamed
+pre: dreams dream
+pre: maybe perhaps
+pre: how what
+pre: when what
+pre: certainly yes
+pre: machine computer
+pre: computers computer
+pre: were was
+pre: you're you are
+pre: i'm i am
+pre: same alike
+post: am are
+post: your my
+post: me you
+post: myself yourself
+post: yourself myself
+post: i you
+post: you I
+post: my your
+post: i'm you are
+synon: belief feel think believe wish
+synon: family mother mom father dad sister brother wife children child
+synon: desire want need
+synon: sad unhappy depressed sick
+synon: happy elated glad better
+synon: cannot can't
+synon: everyone everybody nobody noone
+synon: be am is are was
+key: xnone
+  decomp: *
+    reasmb: I'm not sure I understand you fully.
+    reasmb: Please go on.
+    reasmb: What does that suggest to you ?
+    reasmb: Do you feel strongly about discussing such things ?
+key: sorry
+  decomp: *
+    reasmb: Please don't apologise.
+    reasmb: Apologies are not necessary.
+    reasmb: I've told you that apologies are not required.
+key: apologise
+  decomp: *
+    reasmb: goto sorry
+key: remember 5
+  decomp: * i remember *
+    reasmb: Do you often think of (2) ?
+    reasmb: Does thinking of (2) bring anything else to mind ?
+    reasmb: What else do you recollect ?
+    reasmb: Why do you recollect (2) just now ?
+    reasmb: What in the present situation reminds you of (2) ?
+    reasmb: What is the connection between me and (2) ?
+  decomp: * do you remember *
+    reasmb: Did you think I would forget (2) ?
+    reasmb: Why do you think I should recall (2) now ?
+    reasmb: What about (2) ?
+    reasmb: goto what
+    reasmb: You mentioned (2) ?
+key: if 3
+  decomp: * if *
+    reasmb: Do you think its likely that (2) ?
+    reasmb: Do you wish that (2) ?
+    reasmb: What do you know about (2) ?
+    reasmb: Really, if (2) ?
+key: dreamed 4
+  decomp: * i dreamed *
+    reasmb: Really, (2) ?
+    reasmb: Have you ever fantasized (2) while you were awake ?
+    reasmb: Have you ever dreamed (2) before ?
+    reasmb: goto dream
+key: dream 3
+  decomp: *
+    reasmb: What does that dream suggest to you ?
+    reasmb: Do you dream often ?
+    reasmb: What persons appear in your dreams ?
+    reasmb: Do you believe that dreams have something to do with your problems ?
+key: perhaps
+  decomp: *
+    reasmb: You don't seem quite certain.
+    reasmb: Why the uncertain tone ?
+    reasmb: Can't you be more positive ?
+    reasmb: You aren't sure ?
+    reasmb: Don't you know ?
+key: name 15
+  decomp: *
+    reasmb: I am not interested in names.
+    reasmb: I've told you before, I don't care about names -- please continue.
+key: deutsch
+  decomp: *
+    reasmb: goto xforeign
+    reasmb: I told you before, I don't understand German.
+key: francais
+  decomp: *
+    reasmb: goto xforeign
+    reasmb: I told you before, I don't understand French.
+key: italiano
+  decomp: *
+    reasmb: goto xforeign
+    reasmb: I told you before, I don't understand Italian.
+key: espanol
+  decomp: *
+    reasmb: goto xforeign
+    reasmb: I told you before, I don't understand Spanish.
+key: xforeign
+  decomp: *
+    reasmb: I speak only English.
+key: hello
+  decomp: *
+    reasmb: How do you do.  Please state your problem.
+    reasmb: Hi.  What seems to be your problem ?
+key: computer 50
+  decomp: *
+    reasmb: Do computers worry you ?
+    reasmb: Why do you mention computers ?
+    reasmb: What do you think machines have to do with your problem ?
+    reasmb: Don't you think computers can help people ?
+    reasmb: What about machines worrys you ?
+    reasmb: What do you think about machines ?
+key: am
+  decomp: * am i *
+    reasmb: Do you believe you are (2) ?
+    reasmb: Would you want to be (2) ?
+    reasmb: Do you wish I would tell you you are (2) ?
+    reasmb: What would it mean if you were (2) ?
+    reasmb: goto what
+  decomp: *
+    reasmb: Why do you say 'am' ?
+    reasmb: I don't understand that.
+key: are
+  decomp: * are you *
+    reasmb: Why are you interested in whether I am (2) or not ?
+    reasmb: Would you prefer if I weren't (2) ?
+    reasmb: Perhaps I am (2) in your fantasies.
+    reasmb: Do you sometimes think I am (2) ?
+    reasmb: goto what
+  decomp: * are *
+    reasmb: Did you think they might not be (2) ?
+    reasmb: Would you like it if they were not (2) ?
+    reasmb: What if they were not (2) ?
+    reasmb: Possibly they are (2) .
+key: your
+  decomp: * your *
+    reasmb: Why are you concerned over my (2) ?
+    reasmb: What about your own (2) ?
+    reasmb: Are you worried about someone else's (2) ?
+    reasmb: Really, my (2) ?
+key: was 2
+  decomp: * was i *
+    reasmb: What if you were (2) ?
+    reasmb: Do you think you were (2) ?
+    reasmb: Were you (2) ?
+    reasmb: What would it mean if you were (2) ?
+    reasmb: What does (2) suggest to you ?
+    reasmb: goto what
+  decomp: * i was *
+    reasmb: Were you really ?
+    reasmb: Why do you tell me you were (2) now ?
+    reasmb: Perhaps I already know you were (2) .
+  decomp: * was you *
+    reasmb: Would you like to believe I was (2) ?
+    reasmb: What suggests that I was (2) ?
+    reasmb: What do you think ?
+    reasmb: Perhaps I was (2) .
+    reasmb: What if I had been (2) ?
+key: i
+  decomp: * i @desire *
+    reasmb: What would it mean to you if you got (3) ?
+    reasmb: Why do you want (3) ?
+    reasmb: Suppose you got (3) soon ?
+    reasmb: What if you never got (3) ?
+    reasmb: What would getting (3) mean to you ?
+    reasmb: What does wanting (3) have to do with this discussion ?
+  decomp: * i am * @sad *
+    reasmb: I am sorry to hear that you are (3) .
+    reasmb: Do you think that coming here will help you not to be (3) ?
+    reasmb: I'm sure it's not pleasant to be (3) .
+    reasmb: Can you explain what made you (3) ?
+  decomp: * i am * @happy *
+    reasmb: How have I helped you to be (3) ?
+    reasmb: Has your treatment made you (3) ?
+    reasmb: What makes you (3) just now ?
+    reasmb: Can you explan why you are suddenly (3) ?
+  decomp: * i was *
+    reasmb: goto was
+  decomp: * i @belief * i *
+    reasmb: Do you really think so ?
+    reasmb: But you are not sure you (3) .
+    reasmb: Do you really doubt you (3) ?
+  decomp: * i * @belief * you *
+    reasmb: goto you
+  decomp: * i am *
+    reasmb: Is it because you are (2) that you came to me ?
+    reasmb: How long have you been (2) ?
+    reasmb: Do you believe it is normal to be (2) ?
+    reasmb: Do you enjoy being (2) ?
+  decomp: * i @cannot *
+    reasmb: How do you think that you can't (3) ?
+    reasmb: Have you tried ?
+    reasmb: Perhaps you could (3) now.
+    reasmb: Do you really want to be able to (3) ?
+  decomp: * i don't *
+    reasmb: Don't you really (2) ?
+    reasmb: Why don't you (2) ?
+    reasmb: Do you wish to be able to (2) ?
+    reasmb: Does that trouble you ?
+  decomp: * do i feel *
+    reasmb: Tell me more about such feelings.
+    reasmb: Do you often feel (2) ?
+    reasmb: Do you enjoy feeling (2) ?
+    reasmb: Of what does feeling (2) remind you ?
+  decomp: * i * you *
+    reasmb: Perhaps in your fantasies we (2) each other.
+    reasmb: Do you wish to (2) me ?
+    reasmb: You seem to need to (2) me.
+    reasmb: Do you (2) anyone else ?
+  decomp: *
+    reasmb: You say (1) ?
+    reasmb: Can you elaborate on that ?
+    reasmb: Do you say (1) for some special reason ?
+    reasmb: That's quite interesting.
+key: you
+  decomp: * you remind me of *
+    reasmb: goto alike
+  decomp: * you are *
+    reasmb: What makes you think I am (2) ?
+    reasmb: Does it please you to believe I am (2) ?
+    reasmb: Do you sometimes wish you were (2) ?
+    reasmb: Perhaps you would like to be (2) .
+  decomp: * you * me *
+    reasmb: Why do you think I (2) you ?
+    reasmb: You like to think I (2) you -- don't you ?
+    reasmb: What makes you think I (2) you ?
+    reasmb: Really, I (2) you ?
+    reasmb: Do you wish to believe I (2) you ?
+    reasmb: Suppose I did (2) you -- what would that mean ?
+    reasmb: Does someone else believe I (2) you ?
+  decomp: * you *
+    reasmb: We were discussing you -- not me.
+    reasmb: Oh, I (2) ?
+    reasmb: You're not really talking about me -- are you ?
+    reasmb: What are your feelings now ?
+key: yes
+  decomp: *
+    reasmb: You seem to be quite positive.
+    reasmb: You are sure.
+    reasmb: I see.
+    reasmb: I understand.
+key: no
+  decomp: *
+    reasmb: Are you saying no just to be negative?
+    reasmb: You are being a bit negative.
+    reasmb: Why not ?
+    reasmb: Why 'no' ?
+key: my 2
+  decomp: $ * my *
+    reasmb: Lets discuss further why your (2) .
+    reasmb: Earlier you said your (2) .
+    reasmb: But your (2) .
+    reasmb: Does that have anything to do with the fact that your (2) ?
+  decomp: * my * @family *
+    reasmb: Tell me more about your family.
+    reasmb: Who else in your family (4) ?
+    reasmb: Your (3) ?
+    reasmb: What else comes to mind when you think of your (3) ?
+  decomp: * my *
+    reasmb: Your (2) ?
+    reasmb: Why do you say your (2) ?
+    reasmb: Does that suggest anything else which belongs to you ?
+    reasmb: Is it important that your (2) ?
+key: can
+  decomp: * can you *
+    reasmb: You believe I can (2) don't you ?
+    reasmb: goto what
+    reasmb: You want me to be able to (2) .
+    reasmb: Perhaps you would like to be able to (2) yourself.
+  decomp: * can i *
+    reasmb: Whether or not you can (2) depends on you more than me.
+    reasmb: Do you want to be able to (2) ?
+    reasmb: Perhaps you don't want to (2) .
+    reasmb: goto what
+key: what
+  decomp: *
+    reasmb: Why do you ask ?
+    reasmb: Does that question interest you ?
+    reasmb: What is it you really wanted to know ?
+    reasmb: Are such questions much on your mind ?
+    reasmb: What answer would please you most ?
+    reasmb: What do you think ?
+    reasmb: What comes to mind when you ask that ?
+    reasmb: Have you asked such questions before ?
+    reasmb: Have you asked anyone else ?
+key: because
+  decomp: *
+    reasmb: Is that the real reason ?
+    reasmb: Don't any other reasons come to mind ?
+    reasmb: Does that reason seem to explain anything else ?
+    reasmb: What other reasons might there be ?
+key: why
+  decomp: * why don't you *
+    reasmb: Do you believe I don't (2) ?
+    reasmb: Perhaps I will (2) in good time.
+    reasmb: Should you (2) yourself ?
+    reasmb: You want me to (2) ?
+    reasmb: goto what
+  decomp: * why can't i *
+    reasmb: Do you think you should be able to (2) ?
+    reasmb: Do you want to be able to (2) ?
+    reasmb: Do you believe this will help you to (2) ?
+    reasmb: Have you any idea why you can't (2) ?
+    reasmb: goto what
+  decomp: *
+    reasmb: goto what
+key: everyone 2
+  decomp: * @everyone *
+    reasmb: Realy, (2) ?
+    reasmb: Surely not (2) .
+    reasmb: Can you think of anyone in particular ?
+    reasmb: Who, for example?
+    reasmb: Are you thinking of a very special person ?
+    reasmb: Who, may I ask ?
+    reasmb: Someone special perhaps ?
+    reasmb: You have a particular person in mind, don't you ?
+    reasmb: Who do you think you're talking about ?
+key: everybody 2
+  decomp: *
+    reasmb: goto everyone
+key: nobody 2
+  decomp: *
+    reasmb: goto everyone
+key: noone 2
+  decomp: *
+    reasmb: goto everyone
+key: always 1
+  decomp: *
+    reasmb: Can you think of a specific example ?
+    reasmb: When ?
+    reasmb: What incident are you thinking of ?
+    reasmb: Really, always ?
+key: alike 10
+  decomp: *
+    reasmb: In what way ?
+    reasmb: What resemblence do you see ?
+    reasmb: What does that similarity suggest to you ?
+    reasmb: What other connections do you see ?
+    reasmb: What do you suppose that resemblence means ?
+    reasmb: What is the connection, do you suppose ?
+    reasmb: Could here really be some connection ?
+    reasmb: How ?
+key: like 10
+  decomp: * @be * like *
+    reasmb: goto alike
+   
\ No newline at end of file
diff --git a/Software_Design_Patterns/eliza.py b/Software_Design_Patterns/eliza.py
new file mode 100644
index 0000000..4f7337f
--- /dev/null
+++ b/Software_Design_Patterns/eliza.py
@@ -0,0 +1,236 @@
+import logging
+import random
+import re
+from collections import namedtuple
+
+# Fix Python2/Python3 incompatibility
+try: input = raw_input
+except NameError: pass
+
+log = logging.getLogger(__name__)
+
+
+class Key:
+    def __init__(self, word, weight, decomps):
+        self.word = word
+        self.weight = weight
+        self.decomps = decomps
+
+
+class Decomp:
+    def __init__(self, parts, save, reasmbs):
+        self.parts = parts
+        self.save = save
+        self.reasmbs = reasmbs
+        self.next_reasmb_index = 0
+
+
+class Eliza:
+    def __init__(self):
+        self.initials = []
+        self.finals = []
+        self.quits = []
+        self.pres = {}
+        self.posts = {}
+        self.synons = {}
+        self.keys = {}
+        self.memory = []
+
+    def load(self, path):
+        key = None
+        decomp = None
+        with open(path) as file:
+            for line in file:
+                if not line.strip():
+                    continue
+                tag, content = [part.strip() for part in line.split(':')]
+                if tag == 'initial':
+                    self.initials.append(content)
+                elif tag == 'final':
+                    self.finals.append(content)
+                elif tag == 'quit':
+                    self.quits.append(content)
+                elif tag == 'pre':
+                    parts = content.split(' ')
+                    self.pres[parts[0]] = parts[1:]
+                elif tag == 'post':
+                    parts = content.split(' ')
+                    self.posts[parts[0]] = parts[1:]
+                elif tag == 'synon':
+                    parts = content.split(' ')
+                    self.synons[parts[0]] = parts
+                elif tag == 'key':
+                    parts = content.split(' ')
+                    word = parts[0]
+                    weight = int(parts[1]) if len(parts) > 1 else 1
+                    key = Key(word, weight, [])
+                    self.keys[word] = key
+                elif tag == 'decomp':
+                    parts = content.split(' ')
+                    save = False
+                    if parts[0] == '$':
+                        save = True
+                        parts = parts[1:]
+                    decomp = Decomp(parts, save, [])
+                    key.decomps.append(decomp)
+                elif tag == 'reasmb':
+                    parts = content.split(' ')
+                    decomp.reasmbs.append(parts)
+
+    def _match_decomp_r(self, parts, words, results):
+        if not parts and not words:
+            return True
+        if not parts or (not words and parts != ['*']):
+            return False
+        if parts[0] == '*':
+            for index in range(len(words), -1, -1):
+                results.append(words[:index])
+                if self._match_decomp_r(parts[1:], words[index:], results):
+                    return True
+                results.pop()
+            return False
+        elif parts[0].startswith('@'):
+            root = parts[0][1:]
+            if not root in self.synons:
+                raise ValueError("Unknown synonym root {}".format(root))
+            if not words[0].lower() in self.synons[root]:
+                return False
+            results.append([words[0]])
+            return self._match_decomp_r(parts[1:], words[1:], results)
+        elif parts[0].lower() != words[0].lower():
+            return False
+        else:
+            return self._match_decomp_r(parts[1:], words[1:], results)
+
+    def _match_decomp(self, parts, words):
+        results = []
+        if self._match_decomp_r(parts, words, results):
+            return results
+        return None
+
+    def _next_reasmb(self, decomp):
+        index = decomp.next_reasmb_index
+        result = decomp.reasmbs[index % len(decomp.reasmbs)]
+        decomp.next_reasmb_index = index + 1
+        return result
+
+    def _reassemble(self, reasmb, results):
+        output = []
+        for reword in reasmb:
+            if not reword:
+                continue
+            if reword[0] == '(' and reword[-1] == ')':
+                index = int(reword[1:-1])
+                if index < 1 or index > len(results):
+                    raise ValueError("Invalid result index {}".format(index))
+                insert = results[index - 1]
+                for punct in [',', '.', ';']:
+                    if punct in insert:
+                        insert = insert[:insert.index(punct)]
+                output.extend(insert)
+            else:
+                output.append(reword)
+        return output
+
+    def _sub(self, words, sub):
+        output = []
+        for word in words:
+            word_lower = word.lower()
+            if word_lower in sub:
+                output.extend(sub[word_lower])
+            else:
+                output.append(word)
+        return output
+
+    def _match_key(self, words, key):
+        for decomp in key.decomps:
+            results = self._match_decomp(decomp.parts, words)
+            if results is None:
+                log.debug('Decomp did not match: %s', decomp.parts)
+                continue
+            log.debug('Decomp matched: %s', decomp.parts)
+            log.debug('Decomp results: %s', results)
+            results = [self._sub(words, self.posts) for words in results]
+            log.debug('Decomp results after posts: %s', results)
+            reasmb = self._next_reasmb(decomp)
+            log.debug('Using reassembly: %s', reasmb)
+            if reasmb[0] == 'goto':
+                goto_key = reasmb[1]
+                if not goto_key in self.keys:
+                    raise ValueError("Invalid goto key {}".format(goto_key))
+                log.debug('Goto key: %s', goto_key)
+                return self._match_key(words, self.keys[goto_key])
+            output = self._reassemble(reasmb, results)
+            if decomp.save:
+                self.memory.append(output)
+                log.debug('Saved to memory: %s', output)
+                continue
+            return output
+        return None
+
+    def respond(self, text):
+        if text.lower() in self.quits:
+            return None
+
+        text = re.sub(r'\s*\.+\s*', ' . ', text)
+        text = re.sub(r'\s*,+\s*', ' , ', text)
+        text = re.sub(r'\s*;+\s*', ' ; ', text)
+        log.debug('After punctuation cleanup: %s', text)
+
+        words = [w for w in text.split(' ') if w]
+        log.debug('Input: %s', words)
+
+        words = self._sub(words, self.pres)
+        log.debug('After pre-substitution: %s', words)
+
+        keys = [self.keys[w.lower()] for w in words if w.lower() in self.keys]
+        keys = sorted(keys, key=lambda k: -k.weight)
+        log.debug('Sorted keys: %s', [(k.word, k.weight) for k in keys])
+
+        output = None
+
+        for key in keys:
+            output = self._match_key(words, key)
+            if output:
+                log.debug('Output from key: %s', output)
+                break
+        if not output:
+            if self.memory:
+                index = random.randrange(len(self.memory))
+                output = self.memory.pop(index)
+                log.debug('Output from memory: %s', output)
+            else:
+                output = self._next_reasmb(self.keys['xnone'].decomps[0])
+                log.debug('Output from xnone: %s', output)
+
+        return " ".join(output)
+
+    def initial(self):
+        return random.choice(self.initials)
+
+    def final(self):
+        return random.choice(self.finals)
+
+    def run(self):
+        print(self.initial())
+
+        while True:
+            sent = input('> ')
+
+            output = self.respond(sent)
+            if output is None:
+                break
+
+            print(output)
+
+        print(self.final())
+
+def main():
+    eliza = Eliza()
+    eliza.load('doctor.txt')
+    hystericeliza = HystericEliza(eliza)
+    hystericeliza.run()
+
+if __name__ == '__main__':
+    logging.basicConfig()
+    main()
diff --git a/Software_Design_Patterns/hystericeliza.py b/Software_Design_Patterns/hystericeliza.py
new file mode 100644
index 0000000..70d5724
--- /dev/null
+++ b/Software_Design_Patterns/hystericeliza.py
@@ -0,0 +1,85 @@
+import logging
+from abc import abstractmethod
+
+from eliza import Eliza
+
+class ElizaState():
+    def __init__(self, hystericeliza):
+        self.hystericeliza = hystericeliza
+
+    @abstractmethod
+    def switch_state(self, output):
+        """ Decide how to react next based on the current output """
+
+    @abstractmethod
+    def process_output(self, output):
+        """ React on the current output by formatting it according to the state """
+
+class Normal(ElizaState):
+    """ Answer normally """
+    pass
+
+class Angry(ElizaState):
+    """ ANSWER ONLY IN UPPERCASE (use String.upper() to do this) """
+    pass
+
+class Sad(ElizaState):
+    """ answer only in lowercase (use String.lower() to do this) """
+    pass
+
+
+class HystericEliza():
+    def __init__(self):
+        self.eliza = Eliza()
+        self.state = "Normal"
+
+    def load(self, replies):
+        self.eliza.load(replies)
+
+    def process_output(self, output):
+        if self.state == "Normal":
+            if output.startswith("Please"):
+                self.state = "Sad"
+            elif "n't " in output:
+                self.state = "Angry"
+        elif self.state == "Angry":
+            if output.startswith("Do you") or output.startswith("Please"):
+                self.state = "Normal"
+            elif output.startswith("Why"):
+                self.state = "Sad"
+        elif self.state == "Sad":
+            if output.startswith("Do "):
+                self.state = "Normal"
+
+        if self.state == "Normal":
+            return output
+        elif self.state == "Angry":
+            return output.upper()
+        elif self.state == "Sad":
+            return output.lower()
+
+    def run(self):
+        initial = self.process_output(self.eliza.initial())
+        print(initial)
+
+        while True:
+            sent = input('> ')
+
+            output = self.eliza.respond(sent)
+            if output is None:
+                break
+            
+            formatted = self.process_output(output)
+            print(formatted)
+
+        final = self.process_output(self.eliza.final())
+        print(final)
+
+def main():
+    eliza = HystericEliza()
+    eliza.load('doctor.txt')
+    eliza.run()
+
+if __name__ == '__main__':
+    logging.basicConfig()
+    main()
diff --git a/Software_Design_Patterns/test_hystericeliza.py b/Software_Design_Patterns/test_hystericeliza.py
new file mode 100644
index 0000000..613619f
--- /dev/null
+++ b/Software_Design_Patterns/test_hystericeliza.py
@@ -0,0 +1,61 @@
+import unittest
+
+from hystericeliza import HystericEliza, ElizaState, Normal, Angry, Sad
+
+class TestFunctionality(unittest.TestCase):
+    def setUp(self):
+        self.eliza = HystericEliza()
+        self.eliza.load('doctor.txt')
+
+    def test_normal(self):
+        self.eliza.state = Normal(self.eliza)
+        output = self.eliza.process_output("Hello")
+        self.assertEqual(output, "Hello")
+
+    def test_angry(self):
+        self.eliza.state = Angry(self.eliza)
+        output = self.eliza.process_output("Hello")
+        self.assertEqual(output, "HELLO")
+
+    def test_sad(self):
+        self.eliza.state = Sad(self.eliza)
+        output = self.eliza.process_output("Hello")
+        self.assertEqual(output, "hello")
+
+    def test_normal_to_angry(self):
+        self.eliza.state = Normal(self.eliza)
+        string = "Don't upset me"
+        output = self.eliza.process_output(string)
+        self.assertEqual(output, string.upper())
+        self.assertIsInstance(self.eliza.state, Angry)
+
+    def test_normal_to_sad(self):
+        self.eliza.state = Normal(self.eliza)
+        string = "Please"
+        output = self.eliza.process_output(string)
+        self.assertEqual(output, string.lower())
+        self.assertIsInstance(self.eliza.state, Sad)
+
+    def test_angry_to_normal(self):
+        self.eliza.state = Angry(self.eliza)
+        string = "Please"
+        output = self.eliza.process_output(string)
+        self.assertEqual(output, string)
+        self.assertIsInstance(self.eliza.state, Normal)
+
+    def test_angry_to_sad(self):
+        self.eliza.state = Angry(self.eliza)
+        string = "Why"
+        output = self.eliza.process_output(string)
+        self.assertEqual(output, string.lower())
+        self.assertIsInstance(self.eliza.state, Sad)
+
+    def test_sad_to_normal(self):
+        self.eliza.state = Sad(self.eliza)
+        string = "Do you think"
+        output = self.eliza.process_output(string)
+        self.assertEqual(output, string)
+        self.assertIsInstance(self.eliza.state, Normal)
+
+if __name__ == "__main__":
+    unittest.main()
-- 
GitLab