From 37d0184be9d388705e7c85410940ab848a27b461 Mon Sep 17 00:00:00 2001 From: jakory Date: Fri, 19 Aug 2016 13:09:48 -0400 Subject: [PATCH 01/30] Change demo scene names, adjust pause times --- src/ss_personalization_manager.py | 4 ++-- src/ss_script_handler.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ss_personalization_manager.py b/src/ss_personalization_manager.py index 9d2f8a5..bf58fa6 100644 --- a/src/ss_personalization_manager.py +++ b/src/ss_personalization_manager.py @@ -186,8 +186,8 @@ def get_next_story_details(self): # If this is a demo session, load a demo scene. if (self._session == -1): # Demo set: - graphic_names = ["scenes/CR1-scene1.png", "scenes/CR1-scene2.png", - "scenes/CR1-scene3.png", "scenes/CR1-scene4.png"] + graphic_names = ["scenes/CR1-B-a.png", "scenes/CR1-B-b.png", + "scenes/CR1-B-c.png", "scenes/CR1-B-d.png"] # Demo story has scenes in order. in_order = True diff --git a/src/ss_script_handler.py b/src/ss_script_handler.py index 18b2eea..bee83a5 100644 --- a/src/ss_script_handler.py +++ b/src/ss_script_handler.py @@ -46,7 +46,7 @@ class ss_script_handler(): # Constants for script playback: # Time to pause after showing answer feedback and playing robot # feedback speech before moving on to the next question. - ANSWER_FEEDBACK_PAUSE_TIME = 3 + ANSWER_FEEDBACK_PAUSE_TIME = 2 # Time to wait for robot to finish speaking or acting before # moving on to the next script line (in seconds). WAIT_TIME = 30 From e85676ae989faf038e35469268c25392fd255111 Mon Sep 17 00:00:00 2001 From: jakory Date: Mon, 22 Aug 2016 16:03:09 -0400 Subject: [PATCH 02/30] Add scene highlighting commands to script --- source_ods/story-test-set.ods | Bin 32957 -> 37800 bytes src/ss_process_story_ods.py | 51 ++++++++++++++++++++++++---------- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/source_ods/story-test-set.ods b/source_ods/story-test-set.ods index 98a773325469e0b3a6f13a5eafd27c44866e6774..40778408694bad90443a8b418c95a0bd7ffbeb77 100644 GIT binary patch delta 36163 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.\n") # Add clear and pause lines. outfile.write("OPAL\tCLEAR\tANSWERS\n" From 4df6587d95b7144a0257acf28a02c2b4609daa4a Mon Sep 17 00:00:00 2001 From: jakory Date: Mon, 22 Aug 2016 16:53:52 -0400 Subject: [PATCH 03/30] Add highlight to demo story, fix highlight cmd --- .../session_scripts/demo-yesno-scene.txt | 2 +- game_scripts/story_scripts/demo-story-1.txt | 22 +++++++++++++------ src/ss_ros.py | 5 ++--- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/game_scripts/session_scripts/demo-yesno-scene.txt b/game_scripts/session_scripts/demo-yesno-scene.txt index cda7c9a..d1da8e2 100644 --- a/game_scripts/session_scripts/demo-yesno-scene.txt +++ b/game_scripts/session_scripts/demo-yesno-scene.txt @@ -1 +1 @@ -{ "name": "buttons/start_button_wide.png", "tag": "PlayObject", "draggable": "false", "slot": "1", "isAnswerSlot": "false"} +{ "name": "buttons/start_button_square.png", "tag": "PlayObject", "draggable": "false", "slot": "1", "isAnswerSlot": "false"} diff --git a/game_scripts/story_scripts/demo-story-1.txt b/game_scripts/story_scripts/demo-story-1.txt index 4ca7e29..d3db9f6 100644 --- a/game_scripts/story_scripts/demo-story-1.txt +++ b/game_scripts/story_scripts/demo-story-1.txt @@ -1,23 +1,31 @@ -ROBOT DO "Lisa was putting on her shoes in the morning. Bella the dog took Lisa's favorite shoe. Lisa tried to get her shoe back from Bella. Bella wouldn't give it back. Lisa tried again but Bella still wouldn't give it back. Lisa's mom came into the room. She took the shoe from Bella. Then Lisa's mom gave the shoe back to her." +OPAL HIGHLIGHT scene0 +ROBOT DO "Lisa was putting on her shoes in the morning. Bella the dog took Lisa's favorite shoe." +OPAL HIGHLIGHT scene1 +ROBOT DO "Lisa tried to get her shoe back from Bella. Bella wouldn't give it back." +OPAL HIGHLIGHT scene2 +ROBOT DO "Lisa tried again but Bella still wouldn't give it back." +OPAL HIGHLIGHT scene3 +ROBOT DO "Lisa's mom came into the room. She took the shoe from Bella. Then Lisa's mom gave the shoe back to her." ROBOT DO "The end." +OPAL HIGHLIGHT PAUSE 2 OPAL LOAD_ANSWERS answers/lisa_happy.png, answers/lisa_sad.png, answers/lisa_excited.png, answers/lisa_surprised.png OPAL SET_CORRECT {"correct":["lisa_sad"], "incorrect":["lisa_happy","lisa_excited","lisa_surprised"]} -ROBOT DO How did Lisa feel when she saw Bella take her favorite shoe? +ROBOT DO "How did Lisa feel when she saw Bella take her favorite shoe?" WAIT CORRECT_INCORRECT 10 -ROBOT DO Lisa felt sad. +ROBOT DO "Lisa felt sad." OPAL CLEAR ANSWERS PAUSE 1 OPAL LOAD_ANSWERS answers/lisa_excited.png, answers/lisa_happy.png, answers/lisa_bored.png, answers/lisa_frustrated.png OPAL SET_CORRECT {"correct":["lisa_frustrated"], "incorrect":["lisa_excited","lisa_happy","lisa_bored"]} -ROBOT DO How did Lisa feel when she couldn't get her shoe back? +ROBOT DO "How did Lisa feel when she couldn't get her shoe back?" WAIT CORRECT_INCORRECT 10 -ROBOT DO Lisa felt frustrated. +ROBOT DO "Lisa felt frustrated." OPAL CLEAR ANSWERS PAUSE 1 OPAL LOAD_ANSWERS answers/lisa_angry.png, answers/lisa_afraid.png, answers/lisa_frustrated.png, answers/lisa_happy.png OPAL SET_CORRECT {"correct":["lisa_happy"], "incorrect":["lisa_angry","lisa_afraid","lisa_frustrated"]} -ROBOT DO How did Lisa feel when she got her shoe back from her mom? +ROBOT DO "How did Lisa feel when she got her shoe back from her mom?" WAIT CORRECT_INCORRECT 10 -ROBOT DO Lisa felt happy. +ROBOT DO "Lisa felt happy." OPAL CLEAR ANSWERS diff --git a/src/ss_ros.py b/src/ss_ros.py index ec70951..92b9ff3 100644 --- a/src/ss_ros.py +++ b/src/ss_ros.py @@ -135,15 +135,14 @@ def send_opal_command(self, command, properties=None, response=None, self._logger.warning("Did not get properties for a " + "MOVE_OBJECT command! Not sending empty command.") return - elif "HIGHLIGHT_OBJECT" in command: + elif "HIGHLIGHT" in command: msg.command = OpalCommand.HIGHLIGHT_OBJECT # Properties: a string with name of the object to highlight. if properties: msg.properties = properties else: self._logger.warning("Did not get properties for a " - + "HIGHLIGHT_OBJECT command! Not sending empty command.") - return + + "HIGHLIGHT_OBJECT command! Adding null properties.") elif "REQUEST_KEYFRAME" in command: msg.command = OpalCommand.REQUEST_KEYFRAME elif "FADE_SCREEN" in command: From 76947db1162d3908d5ee9f3cd7bd7b6406dabd0a Mon Sep 17 00:00:00 2001 From: jakory Date: Thu, 25 Aug 2016 10:59:00 -0400 Subject: [PATCH 04/30] Update performance metrics reported Format performance metrics reported in GameState.END messages as json and include fields for all types of questions asked. --- src/ss_db_manager.py | 2 +- src/ss_personalization_manager.py | 29 +++++++++++++++++++++-------- src/ss_script_handler.py | 17 ++++++++++++++--- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/ss_db_manager.py b/src/ss_db_manager.py index e56729b..c841f1f 100644 --- a/src/ss_db_manager.py +++ b/src/ss_db_manager.py @@ -149,7 +149,7 @@ def get_percent_correct_responses(self, participant, session, + participant + " for session " + session + " in the database!") total_responses = 0 - return 0 + return None else: # Return percent responses correct (database gave us # these values in tuples). diff --git a/src/ss_personalization_manager.py b/src/ss_personalization_manager.py index bf58fa6..5b27ea0 100644 --- a/src/ss_personalization_manager.py +++ b/src/ss_personalization_manager.py @@ -92,10 +92,17 @@ def get_level_for_session(self): if (level is None): return 1 # If participant got 75%-80% questions correct last time, level - # up. If no responses were found, do not level up. + # up. If no responses were found or not enough were answered + # correctly, do not level up. #TODO total performance or just last time's performance? - if(self._db_man.get_percent_correct_responses( - self._participant, (self._session - 1)) > percent_correct_to_level): + past_performance = self._db_man.get_percent_correct_responses( + self._participant, (self._session - 1)) + if past_performance is None: + self._logger.info("Participant did not answer any questions last " + + "time, so we will not level up. Level will be " + str(level) + + ".") + return level + elif (past_performance > percent_correct_to_level): self._logger.info("Participant got more than " + (percent_correct_to_level*100) + "% questions correct last " + "time, so we can level up! Level will be " + str(level+1) @@ -109,14 +116,20 @@ def get_level_for_session(self): return level - def get_emotion_performance_this_session(self): - """ Get the user's performance on the emotion questions in this - session (and ignore performance on any other questions). + def get_performance_this_session(self): + """ Get the user's performance on all questions asked this + session, by question type, and format as a json object. """ # Only get the user's performance if this isn't a DEMO session. if (self._session != -1): - return self._db_man.get_percent_correct_responses(self._participant, - self._session, "emotion") + # Get the user's performance on the emotion questions, on + # the theory of mind questions, and on the order questions. + return self._db_man.get_percent_correct_responses( + self._participant, self._session, "emotion"), \ + self._db_man.get_percent_correct_responses(self._participant, + self._session, "ToM"), \ + self._db_man.get_percent_correct_responses( \ + self._participant, self._session, "order") else: return None diff --git a/src/ss_script_handler.py b/src/ss_script_handler.py index bee83a5..21dd96c 100644 --- a/src/ss_script_handler.py +++ b/src/ss_script_handler.py @@ -197,9 +197,20 @@ def iterate_once(self): self._logger.info("No more script lines to get!") # Pass on the stop iteration exception, with additional # information about the player's performance during the - # game. - e.performance = self._personalization_man. \ - get_emotion_performance_this_session() + # game, formatted as a json object. + emotion, tom, order = self._personalization_man. \ + get_performance_this_session() + performance = {} + if emotion is not None: + performance["child-emotion-question-accuracy"] = \ + performance_emotion + if tom is not None: + performance["child-tom-question-accuracy"] = \ + performance_emotion + if order is not None: + performance["child-order-question-accuracy"] = \ + performance_emotion + e.performance = json.dumps(performance) raise except ValueError: From 1b36fc00a80bf3783fde8fff43bf31f04379b3ef Mon Sep 17 00:00:00 2001 From: jakory Date: Thu, 25 Aug 2016 11:41:56 -0400 Subject: [PATCH 05/30] Add session script selection for non-demo sessions --- src/ss_script_handler.py | 8 +++++--- src/ss_script_parser.py | 35 ++++++++++++++++++++++++++--------- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/ss_script_handler.py b/src/ss_script_handler.py index 21dd96c..dd684d3 100644 --- a/src/ss_script_handler.py +++ b/src/ss_script_handler.py @@ -92,14 +92,16 @@ def __init__(self, ros_node, session, participant, script_path, self._story_parser = None self._repeat_parser = None - # Get session script from script parser and story scripts from - # the personalization manager, and give to the script parser. + # Get session script from script parser and give to the script + # parser. Story scripts we will get later from the + # personalization manager. try: self._script_parser.load_script(self._script_path + self._session_script_path + self._script_parser.get_session_script(session)) except IOError: - self._logger.exception("Script parser could not open session script!") + self._logger.exception("Script parser could not open session " + + "script!") # Pass exception up so whoever wanted a script handler knows # they didn't get a script. raise diff --git a/src/ss_script_parser.py b/src/ss_script_parser.py index 4d62ea8..5f412d0 100644 --- a/src/ss_script_parser.py +++ b/src/ss_script_parser.py @@ -35,18 +35,35 @@ def __init__(self): self._logger = logging.getLogger(__name__) self._logger.info("Setting up script parser...") + def get_session_script(self, session): """ Get scripts for the specified session """ - if session == -1: - # We will use the demo session script. - # TODO get script! + if session <= 0: + # We will use the demo session script if this is a demo + # session or if the session number doesn't make sense. return "demo.txt" + + # This isn't a demo session, so we need to select a script for + # the specified session. We only have specific scripts some + # sessions (e.g., to give extra instructions for the first time + # the user plays); the rest will use a generic session script. + elif session < 3: + self._logger.info("We assume session scripts are named with the " + + "pattern \"session-[session_number].txt\", where the " + + "session number is an integer starting at 1 for session 1." + + "But if this is a later session, we will use a generic " + + "session script instead, which we expect to be called " + + "\"session-general.txt\". So for this session, we will load" + + " \"session-" + str(session) + ".txt\".") + return "session-" + str(session) + ".txt" else: - # This isn't a demo session, so we need to select a script - # for the specified session. - # TODO get script! - self._logger.info("TODO pick session script -- using DEMO script") - return "demo.txt" + self._logger.info("We assume session scripts are named with the " + + "pattern \"session-[session_number].txt\", where the " + + "session number is an integer starting at 1 for session 1." + + "But this is a later session, so we will use a generic " + + "session script instead, which we expect to be called " + + "\"session-general.txt\".") + return "session-general.txt" def load_script(self, script): @@ -56,7 +73,7 @@ def load_script(self, script): self._fh = open(script, "r") except IOError as e: self._logger.exception("Cannot open script: " + str(script)) - #Ppass exception up so anyone trying to load a script + # Pass exception up so anyone trying to load a script # knows it didn't work. raise else: From 2772f4d5d7c1ac1b290763c9096d84823228a1e5 Mon Sep 17 00:00:00 2001 From: jakory Date: Tue, 13 Sep 2016 15:44:24 -0400 Subject: [PATCH 06/30] Fix order question generation for story scripts Order questions did not have the right responses listed, since they do not have character faces as responses. Also made the story names and file names all lowercase. --- src/ss_process_story_ods.py | 100 +++++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 30 deletions(-) diff --git a/src/ss_process_story_ods.py b/src/ss_process_story_ods.py index c4e3995..60c12a1 100755 --- a/src/ss_process_story_ods.py +++ b/src/ss_process_story_ods.py @@ -142,8 +142,8 @@ def ss_process_story_ods(): continue # Add question to questions table at this level. - insert_to_questions_table(cursor, sheet.name, level+1, - question_num, question_type, + insert_to_questions_table(cursor, sheet.name.lower(), + level+1, question_num, question_type, # Target response is the first in the list # of response options sheet_dict[responses][level].split(',')[0] @@ -151,8 +151,8 @@ def ss_process_story_ods(): # Add responses to emotions_in_question table # at this level. - insert_to_responses_table(cursor, sheet.name, level+1, - question_num, question_type, + insert_to_responses_table(cursor, sheet.name.lower(), + level+1, question_num, question_type, sheet_dict[responses][level].split(',')) # Make dict of level: question text, responses. @@ -197,16 +197,16 @@ def ss_process_story_ods(): or (sheet[level,key] == ["-"]): print("Skipping empty cell") continue - insert_to_graphics_table(cursor, sheet.name, - level + 1, scene_num + 1, + insert_to_graphics_table(cursor, + sheet.name.lower(), level + 1, scene_num + 1, sheet[level,key].lower()) # For each level, generate story. # Rows are 0-indexed but levels are 1-indexed. for level in range(0,10): # Use story to generate game script for robot - generate_script_for_story(args.out_dir, sheet.name, level+1, - sheet[level, "Story"], question_list[level], + generate_script_for_story(args.out_dir, sheet.name.lower(), + level+1, sheet[level, "Story"], question_list[level], midway_question_list[level]) # Commit after each story. @@ -312,13 +312,13 @@ def insert_to_responses_table(cursor, story, level, question_num, def fill_levels_table(cursor): """ Initialize levels table. """ # level = The level number. - # num_scenes = The number of answer options for questions asked + # num_answers = The number of answer options for questions asked # about the story this level. # in_order = Whether the scenes for stories at that level are shown # in order (1=True) or out of order (0=False). try: cursor.execute(""" - INSERT INTO levels (level, num_scenes, in_order) + INSERT INTO levels (level, num_answers, in_order) VALUES ("1", "1", "1"), ("2", "2", "1"), @@ -413,30 +413,70 @@ def find_character(words): def add_question_to_script(question, outfile): """ Add a question to a game script. """ - # Find character this question is about. + # Find the character this question is about. + # The question provided has two parts: + # [0] = the question text + # [1] = the comma-separated list of responses character = find_character(question[0].split()) - # Load answers line. - outfile.write("OPAL\tLOAD_ANSWERS\t") # Make a string so we can deal with commas. s = "" - for response in question[1]: - s += "answers/" + character + "_" + response.lower() \ - + ".png, " - # Remove last comma before adding ending punctuation and - # writing the rest of the line to the file. - outfile.write(s[:-2] + "\n") - - # Set correct line. - outfile.write("OPAL\tSET_CORRECT\t{\"correct\":[\"" + character + "_" - + question[1][0].lower() + "\"], \"incorrect\":[") - # Make a string so we can deal with commas. - s = "" - for i in range (1, len(question[1])): - s += "\"" + character + "_" + question[1][i].lower() + "\"," - # Remove last comma before adding ending punctuation and - # writing the rest of the line to the file. - outfile.write(s[:-1] + "]}" + "\n") + + # We only want to load character faces as answers if the question is + # an emotion or ToM question about a character. + if "scene" not in question[1][0]: + for response in question[1]: + # Load answers line. + outfile.write("OPAL\tLOAD_ANSWERS\t") + s += "answers/" + character + "_" + response.lower().strip() \ + + ".png, " + + # Remove last comma before adding ending punctuation and + # writing the rest of the line to the file. + outfile.write(s[:-2] + "\n") + + # Set correct line. + outfile.write("OPAL\tSET_CORRECT\t{\"correct\":[\"" + character + "_" + + question[1][0].lower().strip() + "\"], \"incorrect\":[") + # Make a string so we can deal with commas. + s = "" + for i in range (1, len(question[1])): + s += "\"" + character + "_" + question[1][i].lower().strip() + "\"," + # Remove last comma before adding ending punctuation and + # writing the rest of the line to the file. + outfile.write(s[:-1] + "]}" + "\n") + + # For order questions, we don't need to load answers -- we will use + # the scenes as the answer slots. So we will just need to set the + # scenes as correct or incorrect. + else: + # Set correct line. Scene slots in the game are 0-indexed, but + # in the story scripts, they are 1-indexed, so we need to + # convert them. + responses_0indexed = [] + for response in question[1]: + try: + num = re.findall(r'\d+', response)[0] + responses_0indexed.append("scene" + str(int(num)-1)) + except: + # If there is no number, we have a problem. Order + # questions should always have numbered scene responses. + print("No scene number found in question responses!") + raise + + # Now that we have 0-indexed responses, build a line to set + # the correct and incorrect responses. + outfile.write("OPAL\tSET_CORRECT\t{\"correct\":[\"" + + responses_0indexed[0] + + "\"], \"incorrect\":[") + # Make a string so we can deal with commas. + s = "" + for i in range (1, len(responses_0indexed)): + s += "\"" + responses_0indexed[i] + "\"," + + # Remove last comma before adding ending punctuation and + # writing the rest of the line to the file. + outfile.write(s[:-1] + "]}" + "\n") # Robot will say the question text next. outfile.write("ROBOT\tDO\t" + question[0] + "\n") From b605c367aa36a848c0ff2024b03a5597cf0d8b9e Mon Sep 17 00:00:00 2001 From: jakory Date: Wed, 14 Sep 2016 16:10:14 -0400 Subject: [PATCH 07/30] Make story names lowercase, fix LOAD_ANSWERS lines --- src/ss_process_story_ods.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ss_process_story_ods.py b/src/ss_process_story_ods.py index 60c12a1..666aaec 100755 --- a/src/ss_process_story_ods.py +++ b/src/ss_process_story_ods.py @@ -224,7 +224,7 @@ def insert_to_stories_table(cursor, story_names): cursor.execute(""" INSERT INTO stories (story_name) VALUES (?) - """, (name,)) + """, (name.lower(),)) except sqlite3.IntegrityError as e: print("Error adding story " + name + " to DB! It may already " "exist. Exception: " + str(e)) @@ -425,12 +425,11 @@ def add_question_to_script(question, outfile): # We only want to load character faces as answers if the question is # an emotion or ToM question about a character. if "scene" not in question[1][0]: + # Load answers line. + outfile.write("OPAL\tLOAD_ANSWERS\t") for response in question[1]: - # Load answers line. - outfile.write("OPAL\tLOAD_ANSWERS\t") s += "answers/" + character + "_" + response.lower().strip() \ + ".png, " - # Remove last comma before adding ending punctuation and # writing the rest of the line to the file. outfile.write(s[:-2] + "\n") From 589ea95ed4bcea0fa4a587a0bc3e359fef8b8648 Mon Sep 17 00:00:00 2001 From: jakory Date: Wed, 14 Sep 2016 16:11:17 -0400 Subject: [PATCH 08/30] Fix minor str concat and list copy bugs - Fix string concatenation in multiple places so everything concatenated is converted to a string first. - Fix a list copy so the list is actually copied (before reference to original list was being used for the new list too). - Add missing "self" as first argument to a couple function definitions. --- src/ss_db_manager.py | 40 ++++++++++++++++--------------- src/ss_personalization_manager.py | 2 +- src/ss_script_parser.py | 4 ++-- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/ss_db_manager.py b/src/ss_db_manager.py index c841f1f..357859e 100644 --- a/src/ss_db_manager.py +++ b/src/ss_db_manager.py @@ -118,7 +118,7 @@ def get_percent_correct_responses(self, participant, session, if total_correct is None: self._logger.warn("Could not find any correct responses for " - + participant + " for session " + session + + participant + " for session " + str(session) + " in the database!") total_correct = 0 @@ -146,7 +146,7 @@ def get_percent_correct_responses(self, participant, session, if total_responses is None or total_responses[0] == 0: self._logger.warn("Could not find any responses for " - + participant + " for session " + session + + participant + " for session " + str(session) + " in the database!") total_responses = 0 return None @@ -156,7 +156,7 @@ def get_percent_correct_responses(self, participant, session, return float(correct_responses[0]) / total_responses[0] except Exception as e: self._logger.exception("Could not find any responses for " - + participant + " for session " + session + + participant + " for session " + str(session) + " in the database!") # Pass on exception for now. raise @@ -185,7 +185,7 @@ def get_most_recent_incorrect_emotions(self, participant, current_session): """, (participant, current_session)).fetchall() if result is None or result == []: self._logger.warn("Could not find any incorrect responses for " - + participant + " for session " + (current_session-1) + + participant + " for session " + str(current_session-1) + " in the database!") return [] else: @@ -194,7 +194,7 @@ def get_most_recent_incorrect_emotions(self, participant, current_session): return [emotion[0] for emotion in result] except Exception as e: self._logger.exception("Could not find any incorrect responses for " - + participant + " for session " + (current_session-1) + + participant + " for session " + str(current_session-1) + " in the database!") # Pass on exception for now. raise @@ -216,7 +216,7 @@ def get_next_new_story(self, participant, current_session, emotions): # correct number of ?'s into the query for the number of # emotions and supply a list with a matching number of # parameters. - params = emotions + params = list(emotions) params.append(participant) params.append(current_session) @@ -241,7 +241,7 @@ def get_next_new_story(self, participant, current_session, emotions): if result is None or result == []: self._logger.warn("Could not find any unplayed stories for " + participant + " for session " + str(current_session) - + " with emotions " + emotions + " in the database!" + + + " with emotions " + str(emotions) + " in the database!" + " Will try to find any unplayed story...") # Query again, but look for any unplayed stories, not @@ -259,7 +259,7 @@ def get_next_new_story(self, participant, current_session, emotions): AND stories_played.session = (?)) ORDER BY stories.id LIMIT 1 - """ , (participant, session)).fetchall() + """ , (participant, current_session)).fetchall() if result is None or result == []: self._logger.warn("Could not find unplayed stories for " @@ -271,12 +271,13 @@ def get_next_new_story(self, participant, current_session, emotions): # or didn't, and found an unplayed story without them. # Return the name of a new story to play. The DB gives # us the name of the story in a tuple. - return result[0] + self._logger.info("Found a story to play: " + str(result[0][0])) + return result[0][0] except Exception as e: self._logger.exception("Could not find any unplayed stories for " + participant + " for session " + str(current_session) - + " with emotions " + emotions + " in the database!") + + " with emotions " + str(emotions) + " in the database!") # Pass on exception for now. raise @@ -297,7 +298,7 @@ def get_next_review_story(self, participant, current_session, emotions): # correct number of ?'s into the query for the number of # emotions and supply a list with a matching number of # parameters. - params = emotions + params = list(emotions) params.append(participant) params.append(current_session) @@ -325,7 +326,7 @@ def get_next_review_story(self, participant, current_session, emotions): if result is None: self._logger.warn("Could not find any stories to review for " + participant + " for session " + str(current_session) - + " with emotions " + emotions + " in the database!" + + " with emotions " + str(emotions) + " in the database!" + " Looking for a story without those emotions...") # If no stories have the desired emotions to review, @@ -362,7 +363,7 @@ def get_next_review_story(self, participant, current_session, emotions): except Exception as e: self._logger.exception("Could not find any stories to review for " + participant + " for session " + str(current_session) - + " with emotions " + emotions + " in the database!") + + " with emotions " + str(emotions) + " in the database!") # Pass on exception for now. raise @@ -423,7 +424,7 @@ def get_graphics(self, story, level): raise - def record_story_played(participant, session, level, story): + def record_story_played(self, participant, session, level, story): """ Insert the participant ID, session number, story level, current date and time, and a reference to the current story into the stories_played table. @@ -447,13 +448,13 @@ def record_story_played(participant, session, level, story): except Exception as e: self._logger.exception("Could not insert record into stories_played" + " table in database! Tried to insert: participant=" + - participant + ", session=" + session + ", level=" + level + + participant + ", session=" + str(session) + ", level=" + level + ", story=" + story) # Pass on exception for now. raise - def record_response(participant, session, level, story, question_num, + def record_response(self, participant, session, level, story, question_num, question_type, response): """ Insert a user response into the responses table: we need the question ID, stories_played ID, and the actual response. @@ -496,8 +497,9 @@ def record_response(participant, session, level, story, question_num, except Exception as e: self._logger.exception("Could not insert record into questions" + " table in database! Tried to insert: participant=" + - participant + ", session=" + session + ", level=" + level + - ", story=" + story + ", question_num=" + question_num + - ", question_type=" + question_type + ", response=" + response) + participant + ", session=" + str(session) + ", level=" + + str(level) + ", story=" + story + ", question_num=" + + str(question_num) + ", question_type=" + question_type + + ", response=" + response) # Pass on exception for now. raise diff --git a/src/ss_personalization_manager.py b/src/ss_personalization_manager.py index 5b27ea0..46e5f15 100644 --- a/src/ss_personalization_manager.py +++ b/src/ss_personalization_manager.py @@ -189,7 +189,7 @@ def get_next_story_script(self): self._current_story = story # Return name of story script: story name + level + file extension. - return story + str(self._level) + ".txt" + return (story + "-" + str(self._level) + ".txt").lower() def get_next_story_details(self): diff --git a/src/ss_script_parser.py b/src/ss_script_parser.py index 5f412d0..ba1998d 100644 --- a/src/ss_script_parser.py +++ b/src/ss_script_parser.py @@ -50,7 +50,7 @@ def get_session_script(self, session): elif session < 3: self._logger.info("We assume session scripts are named with the " + "pattern \"session-[session_number].txt\", where the " - + "session number is an integer starting at 1 for session 1." + + "session number is an integer starting at 1 for session 1. " + "But if this is a later session, we will use a generic " + "session script instead, which we expect to be called " + "\"session-general.txt\". So for this session, we will load" @@ -59,7 +59,7 @@ def get_session_script(self, session): else: self._logger.info("We assume session scripts are named with the " + "pattern \"session-[session_number].txt\", where the " - + "session number is an integer starting at 1 for session 1." + + "session number is an integer starting at 1 for session 1. " + "But this is a later session, so we will use a generic " + "session script instead, which we expect to be called " + "\"session-general.txt\".") From 01026dcd8b167a23abea499b9b4f196a058d26e3 Mon Sep 17 00:00:00 2001 From: jakory Date: Wed, 14 Sep 2016 16:38:48 -0400 Subject: [PATCH 09/30] Change join to left join since table may be empty --- src/ss_db_manager.py | 10 +++++----- src/ss_script_handler.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ss_db_manager.py b/src/ss_db_manager.py index 357859e..ba71321 100644 --- a/src/ss_db_manager.py +++ b/src/ss_db_manager.py @@ -225,7 +225,7 @@ def get_next_new_story(self, participant, current_session, emotions): FROM stories JOIN questions ON questions.story_id = stories.id - JOIN stories_played + LEFT JOIN stories_played ON stories_played.story_id = stories.id WHERE questions.target_response IN (%s) AND stories.id @@ -249,7 +249,7 @@ def get_next_new_story(self, participant, current_session, emotions): result = self._cursor.execute(""" SELECT DISTINCT stories.story_name FROM stories - JOIN stories_played + LEFT JOIN stories_played ON stories_played.story_id = stories.id WHERE stories.id NOT IN ( @@ -310,7 +310,7 @@ def get_next_review_story(self, participant, current_session, emotions): FROM stories JOIN questions ON questions.story_id = stories.id - JOIN stories_played + LEFT JOIN stories_played ON stories_played.story_id = stories.id WHERE questions.target_response IN (%s) AND stories.id @@ -337,7 +337,7 @@ def get_next_review_story(self, participant, current_session, emotions): result = self._cursor.execute(""" SELECT stories.story_name FROM stories_played - JOIN stories + LEFT JOIN stories ON stories_played.story_id = stories.id WHERE stories_played.participant = (?) AND stories_played.session <> (?) @@ -345,7 +345,7 @@ def get_next_review_story(self, participant, current_session, emotions): ORDER BY count(stories_played.story_id) ASC, stories_played.time ASC LIMIT 1 - """, (participant, session)).fetchone() + """, (participant, current_session)).fetchone() if result is None or result == []: self._logger.warn("Could not find any review stories for " diff --git a/src/ss_script_handler.py b/src/ss_script_handler.py index dd684d3..c4353ff 100644 --- a/src/ss_script_handler.py +++ b/src/ss_script_handler.py @@ -812,8 +812,8 @@ def _load_next_story(self): self._personalization_man.get_next_story_details() except NoStoryFound: # If no story was found, we can't load the story! - self._logger.exception("Cannot load story - no story to load was \ - found!") + self._logger.exception("Cannot load story - no story to load was" + + " found!") self._doing_story = False return From 06a4e184f19f3ce85d95b9ca0b9d006b1024c673 Mon Sep 17 00:00:00 2001 From: jakory Date: Mon, 19 Sep 2016 21:27:41 -0400 Subject: [PATCH 10/30] Add unit tests for script parser - Add value checking for session numbers. --- src/ss_script_parser.py | 8 +++ src/test_script_parser.py | 102 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100755 src/test_script_parser.py diff --git a/src/ss_script_parser.py b/src/ss_script_parser.py index ba1998d..8c07127 100644 --- a/src/ss_script_parser.py +++ b/src/ss_script_parser.py @@ -38,6 +38,14 @@ def __init__(self): def get_session_script(self, session): """ Get scripts for the specified session """ + if not isinstance(session, int): + raise TypeError("session should be an integer") + + if session < -1: + raise ValueError("Session number out of range. Should be -1 to " + "play the demo or a positive integer to play a particular " + "session.") + if session <= 0: # We will use the demo session script if this is a demo # session or if the session number doesn't make sense. diff --git a/src/test_script_parser.py b/src/test_script_parser.py new file mode 100755 index 0000000..705ad3c --- /dev/null +++ b/src/test_script_parser.py @@ -0,0 +1,102 @@ +# Jacqueline Kory Westlund +# September 2016 +# +# The MIT License (MIT) +# +# Copyright (c) 2016 Personal Robots Group +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import unittest +import mock +import random +from mock import Mock +from ss_script_parser import ss_script_parser + +class test_script_parser(unittest.TestCase): + + def setUp(self): + self.sp = ss_script_parser() + + + def test_get_session_script(self): + # Test different session values. + self.assertEqual(self.sp.get_session_script(-1), "demo.txt") + self.assertEqual(self.sp.get_session_script(1), "session-1.txt") + self.assertEqual(self.sp.get_session_script(2), "session-2.txt") + self.assertEqual(self.sp.get_session_script(3), "session-general.txt") + self.assertEqual(self.sp.get_session_script(333), "session-general.txt") + # Test invalid session values. + with self.assertRaises(TypeError): + self.sp.get_session_script("hi") + with self.assertRaises(TypeError): + self.sp.get_session_script() + with self.assertRaises(TypeError): + self.sp.get_session_script(3.14) + with self.assertRaises(TypeError): + self.sp.get_session_script(0.5) + with self.assertRaises(ValueError): + self.sp.get_session_script(-5) + + + @mock.patch("__builtin__.open", create=True, autospec=True) + def test_load_script(self, mock_open): + value = random.randint(-1000,1000) + mock_open.side_effect = [ IOError, value ] + + # Get IOError when we try to load a non-existent script. + with self.assertRaises(IOError): + self.sp.load_script("demo.txt") + + # Get file handle when we try to load an existent script. + self.sp.load_script("demo.txt") + self.assertEqual(self.sp._fh, value) + + + def test_next_line(self): + # Set up mock iterator for the file handle that returns lines + # from the file. + value = str(random.randint(-1000,1000)) + m = Mock() + self.sp._fh = m + m.next.side_effect = [ + value, + AttributeError, + ValueError, + StopIteration ] + + # A line is returned. + self.assertEqual(self.sp.next_line(), value) + + # Check each error type: + # No script file loaded. + with self.assertRaises(AttributeError): + self.sp.next_line() + + # Script file closed. + with self.assertRaises(ValueError): + self.sp.next_line() + + # End of script file. + with self.assertRaises(StopIteration): + self.sp.next_line() + + +if __name__ == '__main__': + unittest.main(verbosity=2) From 5e17e6cc8817b6747c66e9d35dfe85708ed30413 Mon Sep 17 00:00:00 2001 From: jakory Date: Mon, 19 Sep 2016 21:31:24 -0400 Subject: [PATCH 11/30] Refactor main game node --- src/ss_game_node.py | 57 ++++++++++++++++++++++--------------- src/test_game_node.py | 65 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 23 deletions(-) create mode 100644 src/test_game_node.py diff --git a/src/ss_game_node.py b/src/ss_game_node.py index 4fd0a29..c3bf096 100755 --- a/src/ss_game_node.py +++ b/src/ss_game_node.py @@ -47,16 +47,8 @@ class ss_game_node(): platforms). """ - # Set up ROS node globally. - # TODO If running on network where DNS does not resolve local - # hostnames, get the public IP address of this machine and - # export to the environment variable $ROS_IP to set the public - # address of this node, so the user doesn't have to remember - # to do this before starting the node. - _ros_node = rospy.init_node('social_story_game', anonymous=True) - # We could set the ROS log level here if we want: - #log_level=rospy.DEBUG) - # The rest of our logging is set up in the log config file. + # We will have a ROS node, which we initialize when we launch the game. + _ros_node = None def __init__(self): """ Initialize anything that needs initialization """ @@ -70,7 +62,7 @@ def __init__(self): with open(config_file) as json_file: json_data = json.load(json_file) logging.config.dictConfig(json_data) - self._logger.debug("==============================\n" + + self._logger.debug("\n==============================\n" + "STARTING\nLogger configuration:\n %s", json_data) except Exception as e: # Could not read config file -- use basic configuration. @@ -82,7 +74,7 @@ def __init__(self): + "log to \"ss.log\". Will not be logging to rosout!") - def parse_arguments_and_launch(self): + def parse_arguments(self): # Parse python arguments. # The game node requires the session number and participant ID be # provided so the appropriate game scripts can be loaded. @@ -105,24 +97,42 @@ def parse_arguments_and_launch(self): args = parser.parse_args() self._logger.debug("Args received: %s", args) - # Give the session number and participant ID to the game launcher - # where they will be used to load appropriate game scripts. + # Return the session number and participant ID so they can be + # used by the game launcher, where they will be used to load + # appropriate game scripts. # - # If the session number doesn't make sense, or we've specified that - # this is a demo, run demo. - if args.session < 0 or args.participant == 'DEMO': - self._launch_game(-1, 'DEMO') - # Otherwise, launch the game for the provided session and ID + # If the session number doesn't make sense, throw an error. + if args.session < -1: + raise ValueError("Session number out of range. Should be -1 to " + "play the demo or a positive integer to play a particular " + "session.") + + # If the args indicate that this is a demo, return demo args. + if args.session <= 0 or args.participant.lower() == "demo": + return (-1, "DEMO") + + # Otherwise, return the provided session and ID. else: - self._launch_game(args.session, args.participant) + return (args.session, args.participant) - def _launch_game(self, session, participant): + def launch_game(self, session, participant): """ Load game based on the current session and participant """ # Log session and participant ID. - self._logger.info("==============================\nSOCIAL STORIES " + + self._logger.info("\n==============================\nSOCIAL STORIES " + "GAME\nSession: %s, Participant ID: %s", session, participant) + # Initialize the ROS node. + # TODO If running on network where DNS does not resolve local + # hostnames, get the public IP address of this machine and + # export to the environment variable $ROS_IP to set the public + # address of this node, so the user doesn't have to remember + # to do this before starting the node. + self._ros_node = rospy.init_node('social_story_game', anonymous=True) + # We could set the ROS log level here if we want: + #log_level=rospy.DEBUG) + # The rest of our logging is set up in the log config file. + # Set up ROS node publishers and subscribers. self._ros_ss = ss_ros(self._queue) @@ -338,7 +348,8 @@ def _signal_handler(self, sig, frame): # Try launching the game! try: game_node = ss_game_node() - game_node.parse_arguments_and_launch() + (session, participant) = game_node.parse_arguments() + game_node.launch_game(session, participant) # If roscore isn't running or shuts down unexpectedly... except rospy.ROSInterruptException: diff --git a/src/test_game_node.py b/src/test_game_node.py new file mode 100644 index 0000000..1bdd686 --- /dev/null +++ b/src/test_game_node.py @@ -0,0 +1,65 @@ +# Jacqueline Kory Westlund +# September 2016 +# +# The MIT License (MIT) +# +# Copyright (c) 2016 Personal Robots Group +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import unittest +import mock +import random +from ss_game_node import ss_game_node + +class test_game_node(unittest.TestCase): + + #def setUp(self): + #self.gn = ss_game_node() + + #@mock.patch('argparse.ArgumentParser', autospec=True) + #def test_parse_arguments(self, mock_args): + #mock_args.return_value.parse_args.side_effect = [ + #{"session": 1, "participant": "test"}, + #{"session": -1, "participant": "test"}, + #{"session": -4, "participant": "test"}, + #{"session": 0.4, "participant": "test"} + #] + + #(session, participant) = self.gn.parse_arguments() + #self.assertEqual(session, 1) + #self.assertEqual(participant, 'test') + + #(session, participant) = self.gn.parse_arguments() + #self.assertEqual(session, -1) + #self.assertEqual(participant, "DEMO") + + #with self.assertRaises(ValueError): + #self.gn.parse_arguments() + + #with self.assertRaises(ValueError): + #self.gn.parse_arguments() + + + def test_launch_game(self): + pass + + +if __name__ == '__main__': + unittest.main(verbosity=2) From 07ae46b29998993a2871c72986864952d425ae5c Mon Sep 17 00:00:00 2001 From: jakory Date: Mon, 19 Sep 2016 21:33:04 -0400 Subject: [PATCH 12/30] Add some unit tests for db_manager - Add tests for db_manager for a database that doesn't have any participant data recorded. - Add "level" field to functions for getting new or review stories so we can account for the current level, since not all stories have the same emotions present at every level. - Fix levels table initialization to have correct number of answer options listed for each level. - Fix string concatenation where an int wasn't converted into a string - Fix database queries that didn't use "fetchone()" to get the first result to do that. --- src/ss_db_manager.py | 70 ++++---- src/ss_personalization_manager.py | 6 +- src/ss_process_story_ods.py | 27 +-- src/test_db_manager.py | 283 ++++++++++++++++++++++++++++++ 4 files changed, 342 insertions(+), 44 deletions(-) create mode 100644 src/test_db_manager.py diff --git a/src/ss_db_manager.py b/src/ss_db_manager.py index ba71321..26fd422 100644 --- a/src/ss_db_manager.py +++ b/src/ss_db_manager.py @@ -76,9 +76,9 @@ def get_most_recent_level(self, participant, current_session): # Database gives us a tuple, so return the first element. return result[0] except Exception as e: - self._logger.exception("Could not find level of previous session" - + " for " + participant + " for session " - + str(current_session) + " in the database!") + self._logger.exception("Failed when trying to find the level of " + "previous session" + " for " + participant + " for session " + + str(current_session) + " in the database!") # Pass on exception for now. raise @@ -155,7 +155,7 @@ def get_percent_correct_responses(self, participant, session, # these values in tuples). return float(correct_responses[0]) / total_responses[0] except Exception as e: - self._logger.exception("Could not find any responses for " + self._logger.exception("Failed when trying to find responses for " + participant + " for session " + str(session) + " in the database!") # Pass on exception for now. @@ -193,19 +193,21 @@ def get_most_recent_incorrect_emotions(self, participant, current_session): # a list before returning. return [emotion[0] for emotion in result] except Exception as e: - self._logger.exception("Could not find any incorrect responses for " - + participant + " for session " + str(current_session-1) - + " in the database!") + self._logger.exception("Failed when trying to find incorrect " + "responses for " + participant + " for session " + + str(current_session-1) + " in the database!") # Pass on exception for now. raise - def get_next_new_story(self, participant, current_session, emotions): - """ Get the next unplayed story from the story table with at - least one of the listed emotions present in the story. If no - unplayed story has the desired emotions or if there are no - desired emotions, return the name of the next unplayed story. - If there are no more unplayed stories, return None. + def get_next_new_story(self, participant, current_session, emotions, + level): + """ Get the next unplayed story for the desired level from the + story table with at least one of the listed emotions present in + the story. If no unplayed story has the desired emotions or if + there are no desired emotions, return the name of the next + unplayed story. If there are no more unplayed stories, return + None. """ try: # Parameters are the list of emotions, participant, session. @@ -219,6 +221,7 @@ def get_next_new_story(self, participant, current_session, emotions): params = list(emotions) params.append(participant) params.append(current_session) + params.append(level) result = self._cursor.execute(""" SELECT DISTINCT stories.story_name @@ -234,6 +237,7 @@ def get_next_new_story(self, participant, current_session, emotions): FROM stories_played WHERE stories_played.participant = (?) AND stories_played.session = (?)) + AND questions.level = (?) ORDER BY stories.id LIMIT 1 """ % ",".join("?"*len(emotions)), params).fetchall() @@ -275,14 +279,16 @@ def get_next_new_story(self, participant, current_session, emotions): return result[0][0] except Exception as e: - self._logger.exception("Could not find any unplayed stories for " - + participant + " for session " + str(current_session) - + " with emotions " + str(emotions) + " in the database!") + self._logger.exception("Failed when trying to find unplayed " + "stories for " + participant + " for session " + + str(current_session) + " with emotions " + str(emotions) + + " in the database!") # Pass on exception for now. raise - def get_next_review_story(self, participant, current_session, emotions): + def get_next_review_story(self, participant, current_session, emotions, + level): """ Get a review story with at least one of the listed emotions present in the story that wasn't played in the current session. If no played stories have the desired emotions, return the @@ -301,6 +307,7 @@ def get_next_review_story(self, participant, current_session, emotions): params = list(emotions) params.append(participant) params.append(current_session) + params.append(level) # This gives us a randomly picked story from a list of # stories played not this session with at least one of the @@ -319,6 +326,7 @@ def get_next_review_story(self, participant, current_session, emotions): FROM stories_played WHERE stories_played.participant = (?) AND stories_played.session <> (?)) + AND questions.level = (?) ORDER BY RANDOM() LIMIT 1 """ % ",".join("?"*len(emotions)), params).fetchone() @@ -361,9 +369,10 @@ def get_next_review_story(self, participant, current_session, emotions): return result[0] except Exception as e: - self._logger.exception("Could not find any stories to review for " - + participant + " for session " + str(current_session) - + " with emotions " + str(emotions) + " in the database!") + self._logger.exception("Failed when trying to find stories to " + "review for " + participant + " for session " + + str(current_session) + " with emotions " + str(emotions) + + " in the database!") # Pass on exception for now. raise @@ -378,9 +387,9 @@ def get_level_info(self, level): SELECT num_answers, in_order FROM levels WHERE level=(?) - """, (level,)) + """, (level,)).fetchone() if result is None: - self._logger.warn("Could not find info for level " + level + self._logger.warn("Could not find info for level " + str(level) + " in the database!") return None else: @@ -389,8 +398,8 @@ def get_level_info(self, level): # to a boolean. return result[0], (True if result[1] == 1 else False) except Exception as e: - self._logger.exception("Could not find info for level " + level - + " in the database!") + self._logger.exception("Failed when trying to find info for level " + + str(level) + " in the database!") # Pass on exception for now. raise @@ -409,17 +418,18 @@ def get_graphics(self, story, level): FROM stories WHERE story_name=(?)) """, (level, story)).fetchall() - if result is None: + if result is None or result == []: self._logger.warn("Could not find graphics for story " + story - + " at level " + level + " in the database!") + + " at level " + str(level) + " in the database!") return None else: # Database gives us a list of tuples of graphic names, # so make this into a list of graphic names. return [name[0] for name in result] except Exception as e: - self._logger.exception("Could not find graphics for story " + story - + " at level " + level + " in the database!") + self._logger.exception("Failed when trying to find graphics for " + "story " + story + " at level " + str(level) + + " in the database!") # Pass on exception for now. raise @@ -448,8 +458,8 @@ def record_story_played(self, participant, session, level, story): except Exception as e: self._logger.exception("Could not insert record into stories_played" + " table in database! Tried to insert: participant=" + - participant + ", session=" + str(session) + ", level=" + level + - ", story=" + story) + participant + ", session=" + str(session) + ", level=" + + str(level) + ", story=" + story) # Pass on exception for now. raise diff --git a/src/ss_personalization_manager.py b/src/ss_personalization_manager.py index 46e5f15..84be9c9 100644 --- a/src/ss_personalization_manager.py +++ b/src/ss_personalization_manager.py @@ -154,7 +154,7 @@ def get_next_story_script(self): # story. elif self._tell_new_story: story = self._db_man.get_next_new_story(self._participant, - self._session, self._emotion_list) + self._session, self._emotion_list, self._level) # If there are no more new stories to tell, or if we need to # tell a review story next, get a review story that has one of @@ -162,14 +162,14 @@ def get_next_story_script(self): # those emotions, get the oldest, least played review story. if (story is None) or not self._tell_new_story: story = self._db_man.get_next_review_story(self._participant, - self._session, self._emotion_list) + self._session, self._emotion_list, self._level) # If there are no review stories available, get a new story # instead (this may happen if we are supposed to tell a review # story but haven't told very many stories yet). if (story is None): story = self._db_man.get_next_new_story(self._participant, - self._session, self._emotion_list) + self._session, self._emotion_list, self._level) # If we still don't have a story, then for some reason there # are no new stories and no review stories we can tell. This is diff --git a/src/ss_process_story_ods.py b/src/ss_process_story_ods.py index 666aaec..abc4595 100755 --- a/src/ss_process_story_ods.py +++ b/src/ss_process_story_ods.py @@ -41,11 +41,15 @@ def ss_process_story_ods(): # which will each be parsed for stories. parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, - description="""Read .ods spreadsheets containing story info for the - SAR Social Stories game stories. Generate game scripts that will be - used to load graphics and tell the robot how to read aloud the - story. Add meta-information about the stories and the questions to - ask about each story to the database.""") + description="Read .ods spreadsheets containing story info for the" + " SAR Social Stories game stories. Generate game scripts that will" + " be used to load graphics and tell the robot how to read aloud" + " the story. Add meta-information about the stories and the" + " questions to ask about each story to the database.\nThis script" + " will clear any existing data from the tables, so you should run" + " it with all the spreadsheets at once -- if you run it again" + " later, some or all of the previously imported data may be" + " deleted.") parser.add_argument('-d', '--database', dest='db', action='store', nargs='?', type=str, default='socialstories.db', help= "The database filename for storing story and question info. " @@ -197,8 +201,9 @@ def ss_process_story_ods(): or (sheet[level,key] == ["-"]): print("Skipping empty cell") continue + # Graphics scene numbers are 1-indexed. insert_to_graphics_table(cursor, - sheet.name.lower(), level + 1, scene_num + 1, + sheet.name.lower(), level + 1, scene_num, sheet[level,key].lower()) # For each level, generate story. @@ -320,16 +325,16 @@ def fill_levels_table(cursor): cursor.execute(""" INSERT INTO levels (level, num_answers, in_order) VALUES - ("1", "1", "1"), - ("2", "2", "1"), + ("1", "3", "1"), + ("2", "3", "1"), ("3", "3", "1"), - ("4", "4", "1"), - ("5", "4", "0"), + ("4", "3", "1"), + ("5", "3", "0"), ("6", "4", "0"), ("7", "4", "0"), ("8", "4", "0"), ("9", "4", "0"), - ("10", "4", "0"), + ("10", "5", "0"), ("11", "4", "0"), ("12", "4", "0") """) diff --git a/src/test_db_manager.py b/src/test_db_manager.py new file mode 100644 index 0000000..c67e505 --- /dev/null +++ b/src/test_db_manager.py @@ -0,0 +1,283 @@ +# Jacqueline Kory Westlund +# September 2016 +# +# The MIT License (MIT) +# +# Copyright (c) 2016 Personal Robots Group +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import unittest +import mock +import random +from mock import Mock +from ss_db_manager import ss_db_manager + +class test_db_manager(unittest.TestCase): + + def setUp(self): + self.dbm = ss_db_manager("ss_test_no_participant_data.db") + #self.dbm2 = ss_db_manager("ss_test_with_participant_data.db") + #TODO add second database with participant data + #TODO add mocks to test exceptions for all the functions + + + def test_get_most_recent_level(self): + # Test different participant and session values. + # If there is no participant data, we should always get None. + self.assertEqual(self.dbm.get_most_recent_level("P001", 1), None) + self.assertEqual(self.dbm.get_most_recent_level("P001", -5), None) + self.assertEqual(self.dbm.get_most_recent_level("P001", 0), None) + self.assertEqual(self.dbm.get_most_recent_level("P001", 7), None) + self.assertEqual(self.dbm.get_most_recent_level("P001", 0.7), None) + self.assertEqual(self.dbm.get_most_recent_level("aaaa", 2), None) + self.assertEqual(self.dbm.get_most_recent_level("0928u4ijos", 2), None) + self.assertEqual(self.dbm.get_most_recent_level("", 2), None) + self.assertEqual(self.dbm.get_most_recent_level("0xa7", 2), None) + + def test_get_percent_correct_responses(self): + # If there is no participant data, we should always get None. + self.assertEqual(self.dbm.get_percent_correct_responses("p234", 2), + None) + self.assertEqual(self.dbm.get_percent_correct_responses("93", 1), + None) + self.assertEqual(self.dbm.get_percent_correct_responses("93", 0.1), + None) + self.assertEqual(self.dbm.get_percent_correct_responses("1", 0), + None) + self.assertEqual(self.dbm.get_percent_correct_responses("", 0), + None) + self.assertEqual(self.dbm.get_percent_correct_responses("p234", 2, + "order"), None) + self.assertEqual(self.dbm.get_percent_correct_responses("p234", 2, + "emotion"), None) + self.assertEqual(self.dbm.get_percent_correct_responses("p234", 2, + "ToM"), None) + + def test_get_most_recent_incorrect_emotions(self): + # If there is no participant data, we should get an empty list. + self.assertEqual( + self.dbm.get_most_recent_incorrect_emotions("p134", 4), []) + self.assertEqual( + self.dbm.get_most_recent_incorrect_emotions("1", 0), []) + self.assertEqual( + self.dbm.get_most_recent_incorrect_emotions("d81", -33), []) + self.assertEqual( + self.dbm.get_most_recent_incorrect_emotions("d81", -1), []) + self.assertEqual( + self.dbm.get_most_recent_incorrect_emotions("p134", 1), []) + self.assertEqual( + self.dbm.get_most_recent_incorrect_emotions("0xa7", 4), []) + self.assertEqual( + self.dbm.get_most_recent_incorrect_emotions("", 4), []) + + def test_get_next_new_story(self): + # If there is no participant data, we should get the name of an + # unplayed story with the relevant emotions at that level. Some + # emotions are only present at higher levels, so for these, at + # lower levels, we expect to get whichever story is first in + # stories table. + self.assertEqual(self.dbm.get_next_new_story("p391", 1, ["angry"], 1), + "story-fo1") + self.assertEqual(self.dbm.get_next_new_story("", 2, ["angry"], 10), + "story-fo1") + self.assertEqual(self.dbm.get_next_new_story("", 2, ["angry"], 22), + "story-fo1") + self.assertEqual(self.dbm.get_next_new_story("p391", 1, ["sad"], 2), + "story-cr1") + self.assertEqual(self.dbm.get_next_new_story("33", 10, ["sad"], 10), + "story-fo1") + self.assertEqual(self.dbm.get_next_new_story("p391", 1, ["happy"], 3), + "story-am1") + self.assertEqual(self.dbm.get_next_new_story("33", 2, ["happy"], 8), + "story-fo1") + self.assertEqual(self.dbm.get_next_new_story("3811", 13, ["nervous"], + 8), "story-am2") + self.assertEqual(self.dbm.get_next_new_story("0x7a", -0.1, ["nervous"], + 3), "story-fo1") + self.assertEqual(self.dbm.get_next_new_story("", 55, ["excited"], 9), + "story-fo2") + self.assertEqual(self.dbm.get_next_new_story("P001", -22, ["excited"], + 4), "story-fo1") + self.assertEqual(self.dbm.get_next_new_story("002", 0, ["guilty"], 10), + "story-am1") + self.assertEqual(self.dbm.get_next_new_story("0.03a", 9, ["guilty"], 1), + "story-fo1") + self.assertEqual(self.dbm.get_next_new_story("0", 8, ["surprised"], 7), + "story-fo1") + self.assertEqual(self.dbm.get_next_new_story("P01", 2, ["surprised"], + 6), "story-sr1") + self.assertEqual(self.dbm.get_next_new_story("0", 5, ["afraid"], 5), + "story-fo2") + self.assertEqual(self.dbm.get_next_new_story("P01", 2, ["afraid"], 10), + "story-fo2") + self.assertEqual(self.dbm.get_next_new_story("0", 9, ["frustrated"], + 8), "story-cr1") + self.assertEqual(self.dbm.get_next_new_story("P01", 2, ["frustrated"], + 3), "story-fo1") + self.assertEqual(self.dbm.get_next_new_story("0", 5, ["calm"], 10), + "story-st1") + self.assertEqual(self.dbm.get_next_new_story("P01", 2, ["calm"], 1), + "story-fo1") + self.assertEqual(self.dbm.get_next_new_story("0", 5, ["bored"], 8), + "story-sr2") + self.assertEqual(self.dbm.get_next_new_story("P01", 2, ["bored"], 3), + "story-fo1") + + self.assertEqual(self.dbm.get_next_new_story("0", 9, ["frustrated", + "bored", "happy"], 8), "story-fo1") + self.assertEqual(self.dbm.get_next_new_story("0", 9, ["frustrated", + "surprised", "sad"], 8), "story-fo1") + self.assertEqual(self.dbm.get_next_new_story("p391", 1, ["happy", + "frustrated", "afraid", "sad"], 1), "story-fo2") + self.assertEqual(self.dbm.get_next_new_story("p391", 2, [""], 1), + "story-fo1") + + + def test_get_next_review_story(self): + # If there is no participant data, there will be no stories to + # review, since none have been recorded as played yet. + self.assertIsNone(self.dbm.get_next_review_story("p391", 1, ["angry"], + 1)) + self.assertIsNone(self.dbm.get_next_review_story("", 2, ["angry"], 2)) + self.assertIsNone(self.dbm.get_next_review_story("", 2, ["angry"], 22)) + self.assertIsNone(self.dbm.get_next_review_story("p391", 1, ["sad"], + 2)) + self.assertIsNone(self.dbm.get_next_review_story("33", 10, ["sad"], + 10)) + self.assertIsNone(self.dbm.get_next_review_story("p391", 1, ["happy"], + 3)) + self.assertIsNone(self.dbm.get_next_review_story("33", 2, ["happy"], + 8)) + self.assertIsNone(self.dbm.get_next_review_story("3811", 13, + ["nervous"], 8)) + self.assertIsNone(self.dbm.get_next_review_story("0x7a", -0.1, + ["nervous"], 3)) + self.assertIsNone(self.dbm.get_next_review_story("", 55, ["excited"], + 9)) + self.assertIsNone(self.dbm.get_next_review_story("P001", -22, + ["excited"], 4)) + self.assertIsNone(self.dbm.get_next_review_story("002", 0, ["guilty"], + 10)) + self.assertIsNone(self.dbm.get_next_review_story("0.03a", 9, + ["guilty"], 1)) + self.assertIsNone(self.dbm.get_next_review_story("0", 8, ["surprised"], + 7)) + self.assertIsNone(self.dbm.get_next_review_story("P01", 2, + ["surprised"], 6)) + self.assertIsNone(self.dbm.get_next_review_story("0", 5, ["afraid"], + 5)) + self.assertIsNone(self.dbm.get_next_review_story("P01", 2, ["afraid"], + 10)) + self.assertIsNone(self.dbm.get_next_review_story("0", 9, + ["frustrated"], 8)) + self.assertIsNone(self.dbm.get_next_review_story("P01", 2, + ["frustrated"], 3)) + self.assertIsNone(self.dbm.get_next_review_story("0", 5, ["calm"], + 10)) + self.assertIsNone(self.dbm.get_next_review_story("P01", 2, ["calm"], + 1)) + self.assertIsNone(self.dbm.get_next_review_story("0", 5, ["bored"], 8)) + self.assertIsNone(self.dbm.get_next_review_story("P01", 2, ["bored"], + 3)) + + self.assertIsNone(self.dbm.get_next_review_story("0", 9, ["frustrated", + "bored", "happy"], 8)) + self.assertIsNone(self.dbm.get_next_review_story("0", 9, ["frustrated", + "surprised", "sad"], 8)) + self.assertIsNone(self.dbm.get_next_review_story("p391", 1, ["happy", + "frustrated", "afraid", "sad"], 1)) + self.assertIsNone(self.dbm.get_next_review_story("p391", 2, [""], 1)) + + + def test_get_level_info(self): + # If a level doesn't exist, we should get None. + self.assertIsNone(self.dbm.get_level_info(0)) + self.assertIsNone(self.dbm.get_level_info(0.04)) + self.assertIsNone(self.dbm.get_level_info(14)) + self.assertIsNone(self.dbm.get_level_info(-5)) + self.assertIsNone(self.dbm.get_level_info(50333330)) + + # If a level does exist, we get its number of answers and if it + # should be shown in order. + ans, order = self.dbm.get_level_info(1) + self.assertEqual(ans, 3) + self.assertEqual(order, 1) + + ans, order = self.dbm.get_level_info(2) + self.assertEqual(ans, 3) + self.assertEqual(order, 1) + + ans, order = self.dbm.get_level_info(5) + self.assertEqual(ans, 3) + self.assertEqual(order, 0) + + ans, order = self.dbm.get_level_info(10) + self.assertEqual(ans, 5) + self.assertEqual(order, 0) + + ans, order = self.dbm.get_level_info(7) + self.assertEqual(ans, 4) + self.assertEqual(order, 0) + + + def test_get_graphics(self): + # If the story requested or the level requested doesn't exist, + # we should get None. + self.assertIsNone(self.dbm.get_graphics("-3-139uadfbio", 1)) + self.assertIsNone(self.dbm.get_graphics("dioufad-story", 4)) + self.assertIsNone(self.dbm.get_graphics("story-bb3", 9)) + self.assertIsNone(self.dbm.get_graphics("STORY-FO1", 10)) + self.assertIsNone(self.dbm.get_graphics("", 10)) + self.assertIsNone(self.dbm.get_graphics("story-fo1", 11)) + self.assertIsNone(self.dbm.get_graphics("story-cl2", -5)) + self.assertIsNone(self.dbm.get_graphics("story-fo3", 50)) + self.assertIsNone(self.dbm.get_graphics("story-sp2", 0)) + + # If a story exists at the level, we get a list of graphics + # file names. + self.assertIsInstance(self.dbm.get_graphics("story-fo1", 1), list) + self.assertIsInstance(self.dbm.get_graphics("story-sr2", 10), list) + self.assertIsInstance(self.dbm.get_graphics("story-fo2", 3), list) + self.assertIsInstance(self.dbm.get_graphics("story-ki2", 4), list) + + self.assertListEqual(self.dbm.get_graphics("story-fo1", 1), + ["story-fo1-P-a.png"]) + self.assertListEqual(self.dbm.get_graphics("story-fo1", 2), + ["story-fo1-P-a.png", "story-fo1-P-b.png"]) + self.assertListEqual(self.dbm.get_graphics("story-fo1", 8), + ["story-fo1-B-a.png", "story-fo1-B-b.png", + "story-fo1-B-c.png", "story-fo1-B-d.png"]) + + def test_record_story_played(self): + # Add a record to the stories_played table. + # args: participant, session, level, story + #self.dbm.record_story_played("P001", 1, 1, "story-fo1") + # Check that it was inserted correctly. + # Reset database: remove all the data we added. + pass + + def test_record_response(self): + # Add a record to the responses table. + # args: participant, session, level, story, q_num, q_type, response + #self.dbm.record_response("P001", 1, 1, "story-fo1", 1, "emotion", + #"happy") + # Check that it was inserted correctly. + # Reset database: remove all the data we added. + pass From 893e0112a2f82ee0c02ab16ff047775443d7806c Mon Sep 17 00:00:00 2001 From: jakory Date: Tue, 20 Sep 2016 11:08:00 -0400 Subject: [PATCH 13/30] Add testing info to README - Specify in the README and in the test_db_manager that some tests require the full set of SAR stories, which are not included in the repository due to different licensing. A comment explains how to modify those tests to just use the example stories. --- README.md | 34 ++++++++++++++++++++++++++++++++-- src/test_db_manager.py | 13 ++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b3eba52..7a89728 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,15 @@ the Opal device that is paired with this node. Due to licensing, the full set of graphics required for the game is available on request from students in the Personal Robots Group. Please email students in the group to inquire. +### Stories + +Due to licensing, only a couple sample stories are provided in this repository. +These can imported into the database, and story scripts generated, using the +`source_ods/story-test-set.ods` spreadsheet and the provided +`ss_process_story_ods.py` script (described below). The full game requires a +set of 42 stories. This full set is available on request from students in the +Personal Robots Group. Please email students in the group to inquire. + ## ROS messages This node subscribes to the ROS topic `/sar/robot_state` to receive messages of @@ -352,10 +361,15 @@ load graphics and tell the robot how to read aloud the story, and add meta-information about the stories and the questions to ask about each story to the database. - Note that the script assumes a very particular organization of the +Note that the script assumes a very particular organization of the spreadsheet. An example spreadsheet containing two stories is provided in `source_ods/story-test-set.ods`. +In addition, the script clears existing data from several tables, so you should +run the script with all the spreadsheets you need to import at once. If you run +the script again later, some or all of the previously imported data may be +deleted. + Run as follows: `python ss_process_story_ods.py [-h] [-d [DB]] [-o [OUT_DIR]] ods_files @@ -381,7 +395,7 @@ Optional arguments: ### Personalization -There are two levels of personalization. First is the level of the story +There are two kinds of personalization. First is the level of the story presented. Players start at level 1. If they get sufficient emotion questions correct about the stories they hear, in the next session, they are leveled up. The percentage of questions they need to get correct to level up can be set in @@ -406,6 +420,22 @@ couple guiding principles: We query the database to determine the player's past performance and stories heard. +## Testing + +We are using python's unittest framework for testing. Some of the tests require +an initialized and filled database, so you will need to create one prior to +running the tests. + +Steps: +- Initialize the database with the `ss_init_db.py` script as described above. +- Fill the database with the full set of 42 SAR stories using the + `ss_process_story_ods.py` script. If you use only the example stories, a + couple tests may need to be modified, since they assume that the full set of + stories will be present and can be referenced. +- Run `python -m unittest discover` from the `src/` directory. This will + automatically find all files in that directory containing tests, and will run + all the tests. + ## Version notes This program was developed and tested with: diff --git a/src/test_db_manager.py b/src/test_db_manager.py index c67e505..490ae85 100644 --- a/src/test_db_manager.py +++ b/src/test_db_manager.py @@ -93,6 +93,16 @@ def test_get_next_new_story(self): # emotions are only present at higher levels, so for these, at # lower levels, we expect to get whichever story is first in # stories table. + # + # Note that these tests assume that all 42 SAR stories have + # been imported into the database, and that the ods sheets were + # imported in alphabetical order. A more general version of the + # tests here could merely assert that we get back a string, or + # could provide a list of all the story names, and assert that + # the string we get back is one of the story names. However, + # that would not test whether the story included the correct + # emotions for the story's level, since not all stories have + # the same emotions present at every level. self.assertEqual(self.dbm.get_next_new_story("p391", 1, ["angry"], 1), "story-fo1") self.assertEqual(self.dbm.get_next_new_story("", 2, ["angry"], 10), @@ -251,7 +261,8 @@ def test_get_graphics(self): self.assertIsNone(self.dbm.get_graphics("story-sp2", 0)) # If a story exists at the level, we get a list of graphics - # file names. + # file names. Note that these tests assume that all 42 SAR + # stories have been imported into the database. self.assertIsInstance(self.dbm.get_graphics("story-fo1", 1), list) self.assertIsInstance(self.dbm.get_graphics("story-sr2", 10), list) self.assertIsInstance(self.dbm.get_graphics("story-fo2", 3), list) From c5150bb240377ecda0ad721e6bc047e5b0579726 Mon Sep 17 00:00:00 2001 From: jakory Date: Tue, 20 Sep 2016 11:43:59 -0400 Subject: [PATCH 14/30] Update example stories --- source_ods/story-test-set.ods | Bin 37800 -> 33599 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/source_ods/story-test-set.ods b/source_ods/story-test-set.ods index 40778408694bad90443a8b418c95a0bd7ffbeb77..f7de115526c5ff5c0dacbd72b12ca87ad13c58e9 100644 GIT binary patch literal 33599 zcmaI7byytFzU_^>3>IW?2<{LdxVw9B_rcxWHF&TP+}$C#ySux)+vT_SKKGn^-}}6O z)Y#Kg(_PcuwZ3b8N}YOc;-v6@^m%|n)$&yi z4D6qV^Vz40xiiSn#=yebk`c#2+n?7d*SOjKTYSbU2k(YIk(Av zeSNwCKq+EE_b}b0cq6)ZzJ#qYxQrZ)|J5gSul(M5bPh7U*%UP;v-1ddMmznw7#};D zO>8EX!I0rz9slG675>6#aDSL#vh9kNcj3}HXX$Kc_-B`5_{|&qk(13&k|#2%RK6)k$A{jAb@ElV_v10q zn|5vF180fS-Y2&g;tDMfQWAv^GHyW`YReb5ZJe|SS+EJH1jQ<&=<{C4A`=!)`W6mP zEXlunW8Iq_)>B*PF2 z^<{Kte9dNfhCmPyKur9b=V$IhC%Zt>7gBTJc|Ayh3JV%r?qGukp81e||9Ww^pMDzn z{Y)*&u2j`hpF#n1#+;2@Qo#n@c`F1!cJ2U?RHX?b=?CBs&>)L+T0^Q>5bmzKp`&EU zM3UW54yK_MBAV%9#ZnC;lQX2~jkI3A^q2dd^G=pebnoh`LRVg~mv z#PYen7Omry%%%U_?tVC1MoU?S=y2aIk4+wXJ>02#HekpAq!Xy-CRi{IdNa_gsH#6o zGZcUa$iNL}*1F+~?p9`R{&bv;*$}Nt(HG}q@ePQJGmRW8-A0pvg-1zmAE>(0S#CV% z*e;IkcxUkJGvl}6Q`NA><#p)HSaKvJ{AjybF7_&gXwO`!x(S0h)`FeDMn8-VXWn{x zdUA~zNv9A~z=1x58<&$IA>Zv`N9~d?KQ9J>fqg-D)}S}p1JKbnJC-)Bu5;3#Ef;K5 zu2D-_uf9hYPdr>KzK3Q~#=P;kdXA5snLT_vnkQ<&3=tK{YcyXd)Y6ikLdN~GNf)=s z@?-(T;Q)u8AidxPYbIk6foUEyr43&BLa)iLzbZ1l*XDNh6Ozn81Q!FOUHur^?JI|y zz*V|UYS3KZeu6@R@S38>k;R#wLTKA-$5KV-o}@9p(u)qx5zr9M^W_Ye zvLaD7_4T?>I)z2ksepJ%8H3`SJL#eqE>-F8!xd$=_tn}Ni_v`8yAgvf!yu!1{B(ct z_oFg4yYIoS`84RjhOw*lnv}RFNY870u2pF55#uBaM^Sz|M7*UP`$NAV1UeMKxTuv@ zs~Vm(wiW+s>MA$=mEDM!VO*ux^HH*&eR)DTP*&jEj;Z;}0R&4fbGhHUQv(|DH)TST zfs-)Gw%yI2z5a+`zUVXXiy=*-sYh~Y)lM~|1pfg~9`PHCjtwqChap9%IPt21 z4^XG3wKbtl6@dXY6aT|4M7?z`YFj0BNB|v58yA)t;!jb8s9{A{0?qaf+(@8Z3TSJ_cOd{jm=;o&lBOo8pF>yfIZh=DA@R>ID{>K9xHjV+$fDWdnAPEfaDC3 zsYbmgK+lYgfC-0#)ftZHyiM4uRlGVEONvFK>T{lEv=Bv%3gZ-6si#r5OPNMO!^?c@ zDO-N(n|Lnq0SJ-ytvm>F4?5t8dw8fu8umc;?+6Eb9#d=!m`*fA#I<50=%6%lR6yQnSMy(H8s>BQLk* z*2Bi8O;Z(8`ulM&mNfQKMye1IT5qp8{t0QQPRcnr;SX%%=)fG%%zIcAE*~xnNc4>z1kt?F5-eQG9Jne3~K$X(967PK$ zv;Ay%;-o>8k^?x|-q+nHO}dlLW{cTsyRlKw)X&Jrx*rOCLDXSat3Hv{;q}qnV($vA zdppn%s*cul9uL!Lx-CrXr<R8IaFXO()HE-=|K%BU!Zx-`qL>M(-1c-t@)yP4;|l z`&~Fhk2~G_c^a8g%9LGxE!*4ec=RDDIWpU;9%^1p#_M{@-O;oZ&&7I;iYq7hcr+(7 z+ou+PO04aEIZ?KQfx2#!qjKXH%g$MP;r%I}mm1L*WKQOi!%R95&SOsOLh_*hnsRYH zSNShuxIo>vr2uzvTVdvsw%Wo(2K?dib>HjJ;8JL4Q>j9+kEiSYy~#_s2-0Gyi4lOq z49t=-Ep_U{yC+Ua+toa6YoiSzyM@cpe*R00c9Yk+@%@JH1+N_s1>gN;ct(|JL;^NK zAy4+>s-rTW*7c42jqZyJw1eHmJEg8Wx9t{Xy36Ur`pZKuhVJWm;F7$2y1UWDLr2Ar z+S`%fOwWh+=b2bM@}M7BSAolg^gsTV$csg?8$|3bGNm`;_p;)z!QLoYeKU(8i~+6Q z7Y~FiOG)+1y16@jFf7A>l;d9eB7Jl~zhtg>J#DR4QJl%Y)f`I6^~YnRP_*A3XQX7KSS(h1TXI6J zMe}N^g)kS-$Dq&)%L{_U*V0e8)JzBzoM-h2B_ZB$A)-U@JKsj>uj}JUDd{&*QM} zwu3OVD83;fYUnaj^!LwEp}~?7GBHUWT-+ynZdmwreD?l!@ZIs?&d1z%D*UV}=e_8i zY((0|r`D|;DVbS!-U`U`Wt|W$<+*>fvD{SJx=Aw!`E0rz+r_1omF_&J%!R1!MQ$Yv zwLXl(Wq-5t#H8+Iot%03+^NxG!A8&Nac|qusQr1Z<*x1Wz_M`7G%DNJD5)OTgE4rCUM-UUbcwH*|ij7;chX4&Z9Q%?gG zgdH+Dcz?!?GaGSsBAKZcXdn{UKSnrwoSf}h^Sem&WOQCx*K;Sk_F%s9xmobWg_l*i z%td_<7EY<5YX{dPxyB)Xd9r;&SjThG_i18!{;YMqVQ()Fy3YbleoTe04S$ z)NIy`#UdOwWz41!G-55vs#*V6F*UROqDBsTxmx)_h{g8e+i5BYO_!n2-_W#ZId ztr2pquaw`akvqrIyGa>9%gm;;RQV*Pn{iWC^S0U_oZxnMcS2pA=H;QBk|8pMW!BJTv4h5(wUGp^yd0su z6!LMSXiV;B)@=VdL2E)xKojWo`02Y$shP#*^C`KDcS~%9`I$@(ih>nMrGgovtktEVHQ1XjJ z_>^6%Rrig>mFV63tmS^qLv$W-Kmitjxd)RIz)j9$(;Z|_G~q@_5q416k^ZqDf8z6^ zD!OSf1wX;V>v5k*+36wc{T7|HG1|kF7zAh+dtVsmzer_P7U3lr3ty3!gA%l<)_{rf zs&MR3WWYw5?$6szyeHt&)czFSICS5=F4oUI0(BSk2VXrhqsqRQCTRHTtV)djeIa+O9HvA*jTI;F}{Cu@uxR#sBdMsfV;u4!so z^ii)Y66%)K6fMkx^gnKUJdSz|IElAU5Wb^no#`9aXzsLtT?(S0drz1+&}&!3$xdCY z6o(bb@n3G291=L5A#@!%4h}77I7KQ(y30LTnkl@aW({;h>1{U9p)_?mFPseS7fHqY z`w^~&Ekwb816DR!vN`g!@Mjyn8_60q8GSwi!By#@P9~!cs$nm>ScdiTo`u?lJ z?xX^Sf>>w|8Xj(7Ctxu#CnrZBwEX~C7coc0!pQJ0%NP}6s%8KjjV2$Bhf;!tloUc_ z2QO3iL~=*qT>zbYaZ-{Rxyw}49uNGM1xL2-VrJ^KxVsz7Th*L1>_*UUiRl}bu%d)n z9QcK_q5&Q~F%Sc&-W4@?fhJHoW*Qq71KCbz$~rhhJRznwV}YBf!2e|+e6fjh5?TgB z(Q;?!mKX%DS}e(tn1LC#@HH#5BkJ)}V4 zcmY1XT!jV+vMo12F}Rat0zB*T05bq@XbMJbv+99WM(o=nWxc8ee72KNk{HZ=hN4II zI+k9(0Et>rw^<~ateir(Y4`*%9YaC{*mg4f!&;8<9@_VflZ|@b`1@<^5A+$Baj5U< zi!Whg-_5w3xaFljqS+O|pURV5A;X)g1Y`R;u&lA$zMN9AQx%Fv!NPB&6RI12;S;|N z1!o3FPzvcaqwzk|;ci|?_J}oFYLp?GU%Fg#xhP+VfwwV#FUNj-N#9^_qhD0CR*;Lt zK}GL{CX!?&>z8!K1z`LpHj(fum|j@lL+oU{;0z&wb4E+WLfU-*KKf+ta6)^t10U@n z2ZrnqrK85L68&R=ZjS)l+Gh+7ID}|klXtS}=1a3<-_=eYgj9A3oKfP{4}b({v+2)o z(4|WmZG*oBd=z`t4$Ki#s_$41S;!`9#=V&nGvTX2jV33b8Mzzn)F6?H2Ln1(AJY+) z5}nK)Zh~XCA`JOqSa7{HK;(-TxvZe||M>-~27E(0NkC#Jl8jszA(=VwHErt66o>=Y z$#Iw?l?kv$?MFVB!y!$;2G4@4$fV1GN}C z1tj<&wf>i|EjXneVWu8Tt2S6%ic?mEQ5 z7s6--%C2j6K-Qmg4}^VEuw~*yidSa8fV^IS7oMcASYV2oLv>HHMLBkLn*4JFH#;~0;Fye|)EJ0pdhyuO#^7|itq%e4eI2Lj~^ZU9ET(%vOg!8-pln{CkW{7>snU&d~0`6Sq(E(QouiC2|Csa zah6g?En$4E_89+tigam(i-k`rZ5Mw8s)@w&wS#p8$L>SBwBqoEQV<0+2u2B=Eq$Jr z_sAY37En11g8JJqf&>U22uHYUwpkE{4hV-GFqN@z9td4A&fUsPTxe0GZzk6pAmF(q zCcL1X3AgsQZ=t=ea~&(}Zd#keA{z%(4_pyt;GJ3O=`8%C`hM4#zx@pgaH=Rveq#j` z+XV?Ek?Jc0a_PM37vrs9c+@vhC4aI}BFVQU&{_Z~t|We?(PY4pxGh}{ggqM_Kc4aR z4GxsW7ydqU9SQiRW>u#dUD8&Tas3(~UOrWvSm*KSpdeKi8Rsb!&snC;9Fecx(q^HF z>??a=UI35{)4se%)G)WpQ_Dvp#it{|z;v0`LpVbZcOBABqFhjH0S+YmZU#rlVTrcs zQNKEUfrknn#FAT&PBc1oIrdqgGn3 zbXq?Qg}rK{ls~nf$>+A~@btJ9s4dqDR0}Cm6_6&NDwweZi$QcA2ILC~b_`_@A3prT zu+gYgT{=SvkH!QP#TSC1h~?BJuWR3)EdX7!%x7|KmEI0%3u`yrT)S!+Wm@d_H?OnR zQID36E8jv}d6+hPL;5^Xbu(F1|GuY~1iL&CL%kS!jnx-RSQ|YLLf0ec8n-3skwgYMpZjiQ^6)AL-G)only{+#12-jNCafTl| z{RWV++Nz^j_a59K_xjO5kLj!%e;rInkGpTV#!6&;jgfr~1=JYJJUO7F=ylp=ji-Iz zmdw(kF+3h@nob(OzBrDBJ;-O%q#QZ- z{L(v0%sA87XyI!9`VvC7%+ z2$g2DqI$fwkpah|g>1}C!hZK*Sa0xjen)8JHb}AOtNEo!thTS9IZ&*`R{ zifWCu=M%GgovIvRaem2ds*jW-@SWjYjBgRZ!$X-Px{hk6rENs@3Odb4@7=m$$k14i zt-Z!CK>d-s)zw#E@K^J6m-4Z_;O_^Fy=*9;se-+7BMxUxkcL|ulBppoc-AY&f+6CN zDf+d_A1Yevzsed;^dAUVS@WDr&wYQeIOK@NPWfZV?VFUS^SuR;?Z}r3PUtl0zZCXa za-_W1b9=td#A2kf! z6r*}NA0yrzjDbJ(`o+gSSSGl*SV?62WMXBhzadg6gZm@Mcgc;kAO(v*aCT7xbYZf{eQY=Kg;C6!|KB!uOs}VY|Tu_*? zhYT-r5+=g!Vbh>D)xcsgJ>BthPak6tK0mMZgsHpfwpVU%8pMI>t5O?FPryuE=fU71 zAsU?C*CZrlAB|2H?WaJ-<09o#X}4Xmb^kIpn&W-S)>B{Fs)8(^jT3B#k)Qt+39DQU zv5rur&7>&s`@a1m9Ub!w#gZYrauSwfZ<#mo1TeM3rm?wgRp&dy;MvH?2#=OZztd*+ zS2&6*e0(3W!pKIl4(!untUdBWp=Ojqj5UK!q(yI$V zo(*TrVZAyw@@EXLCQo_GT((7$a*9iXyU?TVF=k^X4wYwn^eHkt>&fQ-Qmn~NgK;#T z!;YIfXmN0|{Hz2PrcmFv>R3;PC6cbX$&Zpp{Oj)%ac_zk1E}n=rBsv29?~?zzhd%X z#L+0`>BAkdvK#0zfvK*wdR-`s;@4XPIia8*W_TNr73M%RR8Z{o5ue5M&r0N#PBxfL zR+;dQX(;Fe#*j@%Es2Tli8InJRh<;3ZR9;oD7vu?lxkBlnN?RYu)^u z)i>+)2aZ|(B_uB6+5SDR^Fz~yUHl`t5adbN6OIFdUXZ%qP{HS6{&EBa*b8Sq^Fm|A z*-m=edV5%9|5djBmmcX|Evciu<2Swv#buT&8S(ot0Q_4VAVKXfe6x;oaelS((G!Hq zW|+yID#eO_bhCH001oI}fr8fR9oKC@lCRNa)a_O#KGw*Q%&nLplPh8e?umC$jTWMWEDG z?6Lsz%HQ#yF|$+s^049qdu(@|w!*aHei5&{QcKfvWUKnBWXoqaBk%sm)8C5LumAZO z2?Ra$^2dGt1A6b!y(_}6E_?w{wq9)2Q+;lip+lBij~VfsG+nz=)|($ZLilMbjq|Gw}V$ z=&2uBO@fm=c7*oJ(6-Pk(3Q~KY8<>@a)&4x9wmPLdl*x}ps9toS(ED+&->-hI;c|&(BH$S1k40DGvaXfe{^`qWxzHDMqC}K zgWY4aSI$-8L=FWB+kIkwQu;&*f5}AQL>*KOA%ziZK!g97l1?>pR5YqU$dNM@I$lpa zrZ4^1xX*e?835FYg7r-;`w}yO3%@SK7I7xSoJ4ARqy~^#Pr_hd6pc z&Dr?gOZuwS<R4^RwM=zNzFzBEceIKoyM{(&b8ELyp|yr#MYhu7cfP!TDRg6-ZpO zNoIjym>YSuiTd9Hmsy#<|0k3nGZ**`rpg@Hr%6lu0kM5X;=@hkenzmL0|CBE|1jdtLR=b3)5cCVg|_fP{GfG zH#9c-RL_R2S43?LJ%x?;MTT41?iCp=Cf#uh>so;Am)Y6 zHGi16VEXQ!@~|u&M*vZq7Fsf#xwajhLJI^3+JqQG2ON|)>kd2bxuzmDg1?;4cwIDP@n=p z4cPCaO?Xb%UDV^hLW<#mk1$;j?D#;o|A7l=J`@7vY~9sa2KR&yCrL%LmEw+;v?{dD z?ZZWe%?=u*L#h@h3V8C^F1U{H1P}T#klb>gbPbQ}Is_5JP0&Lx?>Rt=D(49aMY%{H zjge|uJ7LBc*hKhTv6BYcXXp=?R6FhP1^c;2yfBo5xyTnctTr*-)h~@L8}A?J0bgEc zE6SB@*P1HWQ|oAYgVFSzu`Jil5XB2S-XupF=x-2h_#UoL%G1fUTRpxpvkJtrovO*hkM@E?a@C5l#g3D-CCryu4l)<7cMW8SS9A@iSkv zFptIGWt9hvhultwcadiNKE5$5bnd6ZnIE-=Saiy!F2_w~Yw{7#X)}d9w7E=O@KVM^ z)6_pxt>5I$7JWb3ZJ%?bOPW+IWv8M)Rl)?nZ_iX^mSvxM`uelWQ+)P*`AU`1JZ&uG zfS|{)+j`sU#OuVtM%A#P(<3VXY1YTgwH|+}VDTU2KXf0>E@qFDp+C1t;k%=ouJS0- z9ziGLyq?nb&T0b%kzd|lyEvpHQOcK^b~+&gL+ZyQ#@ZcT&NvEX)1Ss`M4MfiWN$bh zQ?O_a@;&C2W37yXA~&xmvJ+Dmq4zaZvof49@kOKfX3z)q79?hx9~!^~jf;A8I}G-< zY{ynRt_Ff5eUYYu<2dD}{fR)Me%1oFhgDiYwv*fW(MFE9+c{KeHA~?rPO;F){hIC6 z^GrQY=IutTNXGRBIbR#hBasnM#k;qs&+E8WEYAZ=h9?vmHGt=*ld+uosi02N_-|Mi z@m+^F3lR~KhLiaU$=dF3TCRab(3IN5ewP+7d3_91q-!JDf*I&rUQ z2bAZJCR^`4l<3 zsp~L>yJ$HipOQ-k)E&v_C+T(D$I&#B#OBhbR(i7BoGw#5Re~wW*m3T#Wm%6$wpJ;Z zRi;p^mziwr7;iw)1IgrYGmQ&tUB&~QBTA(aBW5@Juz4HaT%J~Vxx0j!&l+U@jix{! zE45H6SCtkEP#DvoR6a$F5}H6xEk|&+90TN8rbI-ngndUP1m^>vkcvjBPm5-z);S^b z`wW>NZ^4Yi3nmqYWtUGm4@+5bBq1O%sk)2j@n#cO&h7jXe=^YS+ zqIY2S7038`@~nJHRfl_gLA-XPjSqgIEJ_%&c%b#A+!YtI(z`8hSR{$CaD9XySYLL% z+N{SO&Ga@GrtYov!9jk#^;r{z#F~A@03Gp_wp)JbveXq!w3p4_;4t3?UodTcf~9NZ z&S`TtUugInjc)$!yu;uOK@Vc?J#<>^Byf7@4bKZ!ZH{esnW`0X;ui3uk!N4pz$j>) z%rD^?bV>jY_%Bi=i6yu`PgZ-~EE+)S$Z@+j#xzJ5&Ix64Ei?U5pxGUD4=Idc&s!*I zcdmO|dJa%RZ@YUBwsLufGA&m*LSQ8N*qpIlX`D!9v$&2~^R(H;cY8aV$olp88eJQj z{BAZ^RW|Q=Pjvq4+va0PU+V{h+tt^;5ItYInvqZ>Pl-SQEUhSYaqhSV@Ko^pmfh*< zZznG8Pv>=~V^*eb2`|H}sGk@f{a?wEkf|1BGsfi+8{_O2nO}%=njnYV+b9Z2?go&> ze1US2n%wH0KNd5!_TA(!UVIhj8^I*l9L!iPCKLV8G85MZ>t&RF*^yJsd>7EV;KG#6 z6qDu3)<0_XJoaD=(9c@1z0D~&{hC-e0f{>^PIIF}g#B|n^11pIpy zRW-Up=(QT&ACC&!BkU5+7rp{LkbS)rHejoCdG`{nmVTO$z?`XPdoR~0zOC~ryHx7b zTnM@U>1Th*J<)6Dk1ozdo}+OKf^i7!4Uy$)xR@;V?jM4qjhtfqo*&_9-4j^DwC#I!m&O~e*Di1YLF;xa5cx7BSxwht@T zsYS{yTmo-914R3KU#q>kx{<}zD>ZXc4448qPf-Sy#alNWbQOQNJ89De@8qDx=R6+< z%(Sj5h^RP}#IXIr>8}jY(CWl-vGd>Q^i4@h7R7O9fp8s}BO~-=L*Y$d|7&$o_O7*D z<^;%(On#eg-ylv9)7}5GTTgi#!Lm=2u8-Tpef=F)%w(b>YM#8;TP?p! zTM!p03@K-`wO^1l8QrJ=fszhtf+Y0wE;J|O>!qYyb28B`&+~xk{{yi?w^ol@GruO@ zp!&H+3o+$ABca`M(6KUm{j%?UB%*O9;jH0vC;}3n&gO=xsqPEa0t5p!4h3kTVl28b z_8B!ctA5x?!DCbmis9;cy-(fiypScZW~JU9KN)#4VmIshI>P9Pw+d=nIv}BZ*pHkZ zuQ3d1iMKX5BCH1g+Q#F(nH?;JR%i9(W2^drTwuhX;LCbBys2#9bZju`(MX`X9UJ@&XIhO25`hXS-y@USK*AfGCfb*`Ih2^Ht>XY)g1<*T22GzBpujf`4HbDfeKA0}a;vNZlY!RFI#26ZPc zNXC8XmN>hDW_7M3I}z_Ak;9&E5rvTyl+~)s0i}JDcI;v??6u?2)MccwzwF7~7FK;} zEN@q+`1P&1a?P}mtXwlmV{H9)X9|APODA6rHkTC3xlYGCt+pnnAxTSoJ9 z&t`UPK2p$jOB3^b%v4=7{hY{|=#Zl7b95iXfF;mLzeo~hl7sI+*zzW45wkrBEN-l5 z5e)j#ok^(=-RvqTB9&YhtpZ^Om{HC8lT;BRI#77`tG2R!g?*&li`WE8$54cj2?0wKQIK^0#K zl7bR8qhl%9J?b6Q#{vIJlKzPas_Owpk^#S=OfMfWmDpvr|I2{*?_fc+^VmQt7ao0A zc#=Kr+k68^p8>)YxVhPC?7qG4*nLhl_&+N;!DpFq^wg5$X*}YF{%KkoPI(k>_DgJC z1usL0pK2l~C>`-L!DKdqmj9$Z-1)e!82a4 zh(e*ubqJ;l(fVlHF=?)qCFRx-F{pPbSwE%J&UG|~P!W{a{g&?eSreK@lz1r}(qtDR9R6WuT+x}xb zY3QgX@BhFXp!~O+^WXh~;1@sd9jO7>adE0~0{_o+k;EXpa1JjKVbC?3oLwAn4Cq{^ zy6y-E5@)p{_apyPrrYS_LH-z$81;bNdWRKLgiuCJD1o9f2bR>Rgjx%KhB;lTXtOvd zC`3^4|3B7&)FiydS28)IPni4l$gv3iGX}eEuSUko`UJ9Hjjw&X*_l2%KSbq7+(s-g z^ZH>0jl0CBG5_!I%Fv4B-&MHX<;rJ}B)rLAAJBVNE8jY7`(QEAm!-Dg_I#1|Y9JPs zaLlsVv9t4!djL%E@0=Y*?(Tqu&ab5Q2{D@A8|9l|!;|tpKYBW%Z$ZGn zOrAS#85H?;H1bVq*VEG`&VU7hy;gp>oazH?0;vlQ4){ zFaI(tES0bp#3+B%zbju;l~=!=btN1TtAJs3ZfHWY$`n~1j%D-x=Zu&^N|3E+k^Hqu zuLs#Xui+sIeK3?W0rTFC7$GksFdZ@2^-s=d*Gn%UD`bB9-)8m_+)3MEacn?2L$?5iKG?kVkz`8R!dX6)l`I}g#L;DS$FO%k2_=sv7W z2vQ(-Mfn=wq*kiJ852+5ha}Unt6e-zv&Y`rQ_8H09azzbBKSvK2|e0ygoHv1KETjv z6U;}B#Jv53-+#SIKRC?s1D}?mR;=%9e!;9OmySoEf3J=lrqMc+EOrrj^~}ITqULEe zJTe|d|8k}rWOU$Kv$Gb6DdblalPDLy-0)4!0Z_g2xQFx>FDbqA8qr2fYyj`pBceBx zVO5_k30s%j<;kTn29)Un6pIr;!-BJ}T}qNl5{miFkobp3Z1)|KQvtVNVb*TB@A5m_ zQzwynT$x5?gdmAT&NQYT*ZY3dcccDT-D)R;4|~Sg(;0Tp3*Ar7-_ij;q6I8j%4(3` z=dir7!^3H{M;hCkf4{F0)=6fq-P3hvAD8xv_nJv>UeI>HE2;X(7g_3`Y-vJS+1EkU zJ*kzZvd^A7zwB+j9CsIUX2kEC@22Q~3T=2^d;-gl9rF8!^~0B8MNLJQkq2@~T$}Ud zzt)-+6>NIY6MpVG?bW`p_2fZZmueT4bj1B812sYY%V>0fy>k=!NqkF!mX=l?bGl-{ zWUl7L%~%-6vhlu`W6yYNzBaiffQxUWp<-&f|5sq>lES!zp?pSSH0={$89XwQi{iA~l(6);QRVk?xl6{XPug=ib~v4^zAP&GW2Np2 zTG3DtiaOu$Sxau;;_8dv5sy7zUi`hYsZPNbw3<30Dai?)eXg|JEHPPGg{+H>xWE=+ zyj}dl5OJ$b>=3-B>2pZ9f~` z_!DuIF+6+rZ1cys3>~LT<#j#LidM>{vwQbP)m!;A4htt3-8DC3*V#wwd0bf#>{Yck zy>9R;lHG*pyI5=R7i z!k5nq2J^0Vh{UZg&Q^U_aY*?+_!uca#HL_qr=a3< z?cNO~=>FpVn2zVagV~D@q>Pl$MvabWapdhFR+4XlpH;JQG=!hJo3%4U(j$25Tw_BABif) z0I`d=2f<8{teB2@03Ap4vfrhil6E@i^f2QSzkBH~0L@T8O*vKUbarEXy}}vlz`b(K zdMVJ)_g&VB2CF4Z`$QCcF0Yy8IU7)~;{;!W=bAipqjZb&F<94iGQB49;X~&#*2u?V zORZ}qp?A&mDwnJNMRBREobQm5uCAiyucT%BVfmWEQQux0vk~MH&7q%+ViwPKvZc9d zsn|a&prj9s2s3}5f2qDIK_eJ*|LHJ1#%xoMBEStVV-k_|vg8FZfF$t@l!MzH%l;ymQ%LoeR;yAHzzLj7P znOH1LdsZ6Tl0hvpAqy1J3@I7L=(>R`504MTpq5}3JbE(aK#cR&W1hMx7}U>BwBYOU z`nI@5w#k%oga;L39|7dj41Yx88Kzu#?d{Y^xz06(uqlyvDAJS!DAW3>?;Pf8rL!*S z&|;Xxeb@8aX06P8Q2oj(rVdfh!-xI`agW~*TN-V=&FiRz_92!9)UA7@q>t>-T_!2f znB;yF-UCEpz7v=S-;V%-*d0+|c^M;@kS5{f6|FBC92d)Ecwk^^md@DlbhKB<7(&U& zc_`?VLJU$D$g8~A^}`uDkX*1;hFE)--2fxH5l;a$j+OC+ewO#eSYxD%=c(TButW!# zm@eL%Iq`un=HE)jIv8F~4u7P{O!+Pz5}u`VXT|`c@3*gbFMMxKT8Vt{Z551y(n;;G zbtKE~Pc*@i<7jpAIrhDH^gwL!RSu(%lrzfPhil=)#Ic{8h4_Q@qU+H8^-OFFZ#vPb zMY|v>TfgiO@$=zNHIzJrY^K_3+6fw(-`ltWZk1_LoneLf(|8FCEj%3gQ$RU0+qWB; zt-1puGbz&^OdS}c9=m8USt6Hac!5Yh!ka^ceIZGN`6A5Exvaq#0{R=5_aCZYA$PJ5 z>u}|DuJU)vPfyL^wg|&>o?f^0U`Bqu)#JHy$SCxb!!agdOt_^9qyyqpXS|1!5YAxmv4Fb-BJa}+Q zsdTDeW_MzF1HPXD=t`qAAPjxw6zPBO*oD$pf5ryDZ}@CZA}LvrhB772p0%M-xY_7L z*aY9}LJ@v6dnrmubuVYX&*Xzf1R-->Ut4xh>6_xa?%ydl-VV~6i%O%GjJt0kx2aQ$ zhvTX|&^(MD19P*7_+D4z9o>=fzHv2~4J31mrnQSVSd?tIx9N|jqRFEWC2Q@DhpA_C zy6%Nn0Y+^?Ou}xPrbq2Ki>DzT9TM=2P7k*rH(nf$21x>c{=|5=SazV~KUYHId+8JB zcl(%G`Es*+_2<=4I2_iFoY)x>m|{5*2-;Q7WhEFrBj8nO?55 z1jAKmVdGR9VUOMasRjoq76?P}KXtvfU1wk%nCN>rt_Y%*4XZXvL7^E~THFH&Gu=o`=}b24Ff`JLP6J+J?j*hSHN`>WdB_KZ2%dav087X*NOs4Vv(p&Dddp1 zoy^M*A!RG@AmDDW(_7uB)|$_(^^{K?!Y=$9l8Yju)|!84<*+Vr01wDh>XiP{*2u>~ zQ?8>P9d*iTeR{86*l{cR;^(osZ zaOG|W_OuaQc3Cd$7p3a=EziSkJKpvWH~crAm2xU&|n!sWBrmgj@OF zs#I30&KB2^#dx@_<_8e*iNQpSPnpTD>6lG>do?UFOl$%K6kA%_n#<}|4HHU@u^k;V zjo-z;x*N!9#y=JEdprbcxqnwa@VS48b&S!(vzpmzAtPL^7}}E$`(r86He3_R_R1NeWz>l>gX_WPV8nJNfbrZ z+T3fMF!O8amT4Cp9D-Xj3Yuo!v)lD78=NoJIwW)RuFGN$7<37k^FA(5IF2d%Xtx$G zu-X3VXe_sxlxCLt;;=b5<>?wQJgBHv5+wT({&w-~N!HPoaZO+uJ7F~iS9W?g>^em2 znSX2A9CS9^+zeGl&n=MktOpe`92tpy6{Av5Uo#Rzb_|45p_)d(H_A$|xQ|{OLN5#s z4_7D@u3&Dk{DHc=7X$Ys(0SlnuWXND=UN``RVFJ9_)JQN7z7!wevklrMwS2^n8YKVn6UTkxBjKgUq5p|Mz z+f5W}tjOTCZ+=aKPdH?SkEN+ffQDc1wIlbKB8n`%(JHw{`YT5%QF`dKx;M0jOQIY_GwTpCgtX2_bA^5F#r3STMq^Z<2+N_Apr{J@TNv+GEq5k~)kX@i`A5&nf!e}syUcwc= zQaA&kqg~S;viCjXk7k2^!jdCOUf<#6jnTXDVmoKq%>WiARJP>!=_3!5OUy?XY$zi3uL0+6}`$biik|D`;#H;1$Qz z!55v!pQ}Cs8?Ncb{|}#`Y5po!z4z&aEK7z@!GAKBOM*=}1leeu9XgkqRYuNf*ttjY zx|<3AA0S|@;|X|+A7E$?nawF~`9N&52`8&?YUsGDP-?#l2D<@|4aNA$QtxG^&LMmv zrTsskPO)DA(j)=)f&ZYjn^}FxfE9MQm}BMNhNb9Hz&3&tsnyO^n3@Vzz%dup#y*KN z1L-Fp`G5Z~gWe~JKokWer%+PghM`ipqh9n4w1-U3Z$al^iVZmUTe1?^802w?NpYV4Jg!dpFK4GEk ztt_bjzy?wh8rv4mu*i$)!D+uS5&Dr;KWPVhNJv5XEyjcojI8aBMq~i3!CoWcHsU%$ z!%N-4Cqit+^X97bA8a5F&;(Wb-h__g^SM-8^~t;EHwbPS#-L|2cKD|21DQs)D|>9$ zG52M)_kRN^22A8x&-~`WAin@WkQAITrsOA3%ayJS#S)R2s-XZp4q|6I^LO{8f;{zq z!Xw+V`3&a?s@R~br}Dka|AR{Z%izkS{+B@^)5N~cb&eYFKZu*LTDBPZe<%La=wFOf zWgz&DU{ASc6eg8tP2{^?k<|o{EFozlc9pke0ykvsAkhD}sl{iTk#tGT#|$n>O9~sh zF?iG+pHBSgLVQhDvyMSWq$_w1xTUrw05Q@WB!7g(GSUb91{GoagQM>rVdy20vqkyR zF8&`-{8yK-UOKOsLsUpZ2+TaB@ClXYFXoIZ40#jNBP_vM3!nIi9BIZ%e7WV2^v zfrRlC0{o235$Gg-)V@=O;)TO!MLNAvQ~foEu;;_uz+XE7s6Gz{os&RF21Bler15o` zsQaZtAGE(yl_-|oA{gLeBBG<@DGqjj;drGt!+;N(b}!al&*%?T&xek{eqKD34Um?$ z@m{(p^R}Mlrl2DGEQ#vCmPsvHHbt4mLsO|#wT_*ZtU2=PIQL_yN)k?KhUOgD-Qjf{ zTV*M74BTGZ?TzfS2sSlApD76Yt|PmmIo0H+R#qs@lMA78bVXMv9|CU3UH}z;-HrDN z(zrY%aiy)Zmw2`&RI0br{HCJvl}z?phaMK5a@u~vXs=s@Py7hhP4b1*l!IK^Hm`5# zM>bMk)*8KEwIgRYsI}sk+l7Y4PL2m92HrHi8V0X6d%s}z36UZX>S*{-uz7p%K1|$O ztJPf=TSb4xXt^xjOCa-}^^HQz_9+p0e`7P5@Mf%Ht+6`+_b{e@wzeZ9$EA1Fy`Og~ zOfY*f_{g@f)tQ|y+MUriK{)1d0($rsMv&?sWS-e^`Xi1=!rppupx*j+$m7B=GTYtx z2j?}xa;LdRN+lJ*KK?4<78SUNj5eI z48hSt!Ol0Xe6NRmm0^b}B+XVi=4PJ>nT>P?-B|~t1LxO+CLWD?>+E^bquG*G?~XRF zj@^T}_Ae$jtkd~1F6(X+)?{Ax*Xicy3eq*hajPV9gfoveZ;j9?B*D3KVn>K1(&Eu@ zT8)R`BG+>1){qL-_IywoD(Zsy+0U@`&c z?OYBD`D(Ec1?v?lCBIG9BQ{tM=P2=d8qhMP?!p^h`I2aGamPErZ#DFA^2RJA6qNqe zj_i}VVdGcTE_|Ac(&dDhMM5r4VzTG#jB_o92J7Vht!oavUv)I1tu_%3aIV$AR^(=*D69#Z;f&0GY1l zB#)yZl^+?4U3EE=*0S;~ZchU4E&#&@%o&%NKU$sGI~E$&{Fk zX*_)5d*jqHr?QX06&Gp>Rux5aFg}W1TV9;4LBoc}^}~<+0{%>+wCD{0=)dwEFjx;L z#Hv;;C|LJMcS$ZYDEWffbH@7(opW}i9CttHdn0Kf3NMvt$Q0$4mn%}M+#jw@NNuAS z{P+?{PlsAG^DTpPHP=E$P#86*c8RiDGI6eoXOJLMT9H`AW#Smzc(+6{(D#Lr8{ZEv z^xGj?Oek^RKtLCEn+YGGwZo!8JSNEq0?dGIhGk5gf8O}5GPVY}A#?g#fh?L+1>!Wq ztR8p^34zKtB#4-}reHXpm}||AM4ml2FHbv|NQWi)-0HsdHc4xoRZy7HWVflr|>BaW; z?Q2^AmOHGu@@BX0YZd+xDiTxo6=^G0%#+e8Tk;kVSPaR@y~umvtIB0dQf}J2;uL7R z*9m03w5@|=4pwz(Tn7jc!R&VXyMV#6NT5>EA!A2Ne48k$pfSsF)*o?X3F|G+9Y|*} zsd`Q7q7x_tw#m>W2>?j;A!~rZRLbOj&9-qMMTvoUh5UewaHS0(iAz&VWo{zr?4raf z^5Y>PcMFYJ^>$Ax)Hf9@Chf>ecpaz9{e#A9I#|z}=UU*)j>ok=Ythik##Ltwq%;SL zVG8rDMhev@Dlz$`4veDWA6(h|9aaONz24HN-a zt8kWqY*`KrKmIlzC|toSy+wJ+1*_WC54 zhofF{quI)2Xs+((H(_fJJ2|nZP!k;s(ymU^kyN(}$-1BU#YibfUGI91PK@U56pV|>pkY5k zxcByk4r<>&xbJ0OsGZTEx1T36k}20?|Cw-+tJT@GjSJ?t8HF45-wYldgln4bdSLMyw z1b6CwX@aqimJltO)bu6Z9cLO%``Xl8E>F@A>s;FJj(W+Xv)k=H-db(k7fBZTw@ zMn+Ls1lh5pD3YmBG+#oXq=ttjA0J1IbSTxObxBXdQyAc~K_|)Y&{o{EFmI&j)=^`R^HL^tOw40u~SJij-cBg=^L#=2V~jmPlsi zhG~r;^qbUHZgHkvb`?ULD>j^ps`bk0C@bFkL`J&a?K}A^aPRv1ZbBXmaoJvZyC~sB zKjOZp>GDs`a8X`sj8wYtzvzYPJA6O*NWjy%117U)_>o$G%x<{~WmLp&N#kOCjH~(e za$_lVt#0%gwm&evCl;(iVVvs8Zf=rxs;V?*JqseeV65g2u(hPWkO}sMGr_omA+7sal2g>nJ+Mo{o!;hZ7-~6ZQHY( z9_;0k{QA19fwSMoiGB~#c zB|S7pvK@P=V^Ol!1|2Ja8>HT?(*-yLO)6_yMb_LMY?sWZP=%cB(H|(c6bId*2yc+{ z`Qjb0IjMai{3zl0BmEnIn2@w!CBLr!Z>i-VARzvguI|^7-{n`{I0r{JOCtwrS1Zdi zO$(b9R+QJyt{zzvFskZWoT!;a#5Plgl~nzeJkjN+T2+v*f+MTqdAw6KGdf+#1*9id zY<9$%SM@@eu_4LH2i&e=kX$|uVLjbc#$>ryqqf0~bl+=4ygq|7?2H^V?WbOuzePK= zs=~v`cpX;|U^y`Cx!_BK)3QnzmpT}it8x~S5*;xLBS;sorcZ+mxKGxHW(}7pMj*|* zG=rduIJAER143JxQMMlO)O|dEhN*alMN!?XXZ7TJhFvenZOJJT$p^8MpVCK+dpsFc zwa=2Ivgpu;Tgb;Z^4pUnMwGbUhw@;fR;!WC^PK3xM~N>?VsNC4jHDIuxA5=mp4*?a#CT< zrO~7ywz$F;`;zn zTd`|y8?;@|Hz`A!do+QZVa^CfGzDY>#tK%6KR?YRHuISPe>K3;F+$g}FsJLNka6pVC)xSRQj&_oXjD^VW2VHUj1t0~xm#N_ z42&$iV4&c*!c^K&)gO>Z`lZm5ctONPGV)RSa`8*ObYV?jAttJ#Hb6(7!$G4Sx%##V5&Y7B51{_2{BSWvQUQc_S(jA8T9p zN(eU7_4KYBPAoSkOXah>o?<4?;k!qQT_mI%0Q`C*#_I~DCMZ3N$B+!T>p>9hcl;@y zt)k8kF%}}o__M!6w`9zv^HJ;_blrr{XhU4|lY&yFXF`)c3@otAn0l6HEz?R9BS%LP;1kvpBSN>bW78ep>Qz$0% zru|2f}}$NboXK8J2rC7Jrg=FNSE0rEjKxYkPO^oaSmLQ z80T-W%K=PgnS6CMI4BioU!A31d4n?s^benAFQgK|H=8{)qOVqhtDwSh6_06-M2y9S{)wi@9PE) zKBwMX?zQ4>j?EvxIpS`!a%5$4PEbB_s3uRyTCYj;C$Uu|;x|hxZLwHdU175oSRS6n zhCI4D5mIQ6<&En5 z)SK&S_GqX?fdoDq#M9_a>y{|9Gw5UNIKy{Ikf-!?SlojF?pwP0#jq6bV(MaZYy~lM zu|&wKrePm`mn@Z?mVwhW_)O{MWOzoP(zUX9Z=Aec@yai;R^~|MEM7e5j#;Fv?gUV9 z`=Z?>JVEmQ+Ax|+qAFeRw zV2^_YvENVDNB=?PI^Nyu3a`$kSu=S+%P{f%%NWT__o%~_cKJ)23`6onu z66B0Qw((v@piL=$`RiH{RXppmi{DTExNOh52bIs>MjtnMHB8y_?2eWh~lH=xdB8%frt4yKTb|<5j!hV#F0>ttG6o zL8J{S0_rNsTD9Gw1p3-nQzcKjLHMmtFVQRQZmXtCTO)RLc6CvCQ1TC>MOb-CHYS0E zYQ^jq0;!Ne_+ctfW{g2~bQ#JetcpuKNd@CyZBpbE)+q`?!~w=E)+m1Bvbk;O2pAo3 z#W&8i5}9Blh&v!W31>1viD|ho_0yUz4k44>I6MV;#=WLf(2Ef=+W>nOL{eo3n#iCN#Th^$1(mnapA#`o2@#W z_epKulb#GnX(j%#QgtBFwG)S?vWnHnWxrozp#A6N{esKB@r)-RomxVO?r1LKP^5s{jy=yr^k<`##Co$=Rnj|ge`}b@Zi0Gjc3bJ)Am7a2KeNa6X?t1OuqVI2Y8vS( z?NS?SdR4x8?`C-Ck(B_4NR#P*PKE>k=oJ9~_N-AWp_nbaw1Xpp4@5=M#n1B#BM>2v`+!r$dcg~NYZ zaiO1U3|r{#2erNEK3TcI0orIDNN^);KT>D7!Ni>VqBSti6lR-J+)6#rnPgO`quPFF zL6RIhQeyA|zaT}qeO@TkbSIJY@q$)3vlC)Ov*x$GT?u)XE8v?kqAhti0wfP`QXEg3 z_O_#n9oAMH80K^-ABJPZRt(jMetkRv=h^4fGKZa?Aifi<+nZx zuF@CKLR!;RP+=hD=PO5&8goKbRmzUKqbJUs9xc3iivFyVA*ay7FlkL5Jg#76^iD>&gY&ch2u`SK))ZU$66i|s!l%p-s0xo2S z@23md{r=P;=5c>YQXWUNuuIGZ(r1$H4`iAf}IeZ(mj_R=jsO1>=#IypveZBaa6KSHh@eu~j+cXCsbmafKtpAMH}=p^TH zc`2f?;!~5VPhU}HgDA|jog)R5q@74ab$3%bU%R;NKX;DIb4AGw36=^6N&M& zV1vo)ZH7Q7$bjJM5Vx(5eLtm8H{oCD|KEv!NQqvDG9Ql^bsw=3rr+~9+Ci3@=n&b1&I zkB4V#T%?7kFg{1@)B-8NX?lR`(Lh#Bazm(1rR`w4ADpt~4qYSamuvkHxPyn#1g1ox(jAb0XfGk0~D{F7X4tH zdlChYPx;9{z1`=lm+x+wIW5&Hg47NL!->>{?OE!tuKCZ4z7NpgB4%?f4 z!+Dt1@;2p$;2k*z+Z)(!siIHaRINZJ40U@0-Ebm^T?`UUqe$nOv5Yd#Cp!*FlDNpP2xY`Dbp}WoI~RK z+OKYHT$m*my+$xt?mn_?5(&Sytf^kGJCdC43uuMB^;Nr79a^Ss&JTTCA`~tY#TI7< zAAC%vFJ~0>)HJYZh}r|Oi55DF7LZ0`7DMs?ydJZ^P9biFybQPdNx3;wFV_aBO&6FA z>Pa7aqrvi>LinN1m!>G+&Ez`;*jt_NN)ltu1aPMyw?$yPzC0^*3)6}8Hu6lrTc(4h z3_GwkU3>;$h72+`RJ(o@8*~#Fx)aGIIsXag?ph={9Xpr9J}z*dzT_B0MRjV?W6=9g zwZKU&%WirepP!dDxMLw90n16|Y1YYhTqsYb>06`X2qQnWo9ThKuRnX5 z|7embwRMNJQUy&9@NrFoVr?Ey?CZ=95eyBFRKA8|(1Fs->x+hyqRBd(yyPEW3l9gi z&`UvZpLq^wgMSx8UXeu-llm;tydB*>=<%_yeh&I+Jv?MBj+MCEnohPH2)C?uawHP9 zRg-AXTuWJBO`SgXB53jJ0@l2Frt(P>&fS+y~!Zn-QJCoF<%nOG^`TMnd3m;-ac0(RqpMy%i$qeuB>E@-kr*!SCUK#kMdq8>;;XDh7@?} zLS46$Hq}+pIuHbz%Or`I=G`e>92=3M8i{>`lf<7~zG-L-zw6AIr%rC%@tyad&#G!r zD#K4^+@Z4<@vtgRN1xCaR49ao*K*4}+9|}loW6sg6<^mEDN#>p(-g5wTGWox#RGpW z&(Btps7qduq2=MVn3!80minHopkeK$O07DU(`Yr@_&k@UX~p*;7Nm!DloN|+Nr-$_ zE8i;e65|Vy5Y?U_!gY-Xy1Tg!!tMaPv_NKo=A?7Mc&9rse{7fS(Ofq#Nm za?U*_xv-MXHz_*{N;DdY;@% zU4vPyz>(8Tf5ztIF2nupw&*3=h% zUg|y%>D>6G4I?H5IM>j8WtWt+>(j&-ETVKKT&X3S`%9LrBe{&Vxs=`|&2=|ttp4up z*a&?YzpvG|p6^8ip%*QMG)DTCM=*8pc1uw$?5*ktw6p@XD=qM@Tj$i-a!*AS5G~vI zR1Czif!yj};rOSyl@L;}1v!ufZ#0UoX(8h(VkvNEuQX9Fd>Du9hiomm6$yuXn?$x? z;|v1A(BD6$SBlic!=hX}=PfT^kHpp@aj-x+v9%=U&&mmfQSy&>v*NW?wzD0{e&j+w zLQnz99O;TDMCnKR^sXt88r`GRbAr#@a4SD5hNeJ0-sMw?b#i@1+v1Ov%V7b+DVjR0 zYI%0m6G&*OFRJe+eq zc1pRK)V#9BaE^csZD;Y-fSU9P$p3}K;}brURk#VB`_chS?|E3*t%G348rjBl?$B_Xh*?Ie@PfTrcE4gRy>f^1bQBklXD`p~s%|}q=8HN#VhA)gD(LVCAP2U_3Ec( z3?B(v#DLo17?=>*8ltOV=(-I@0JWT%ysnE$Q)()z-~w!nvBJXsZ!H4(#r3+Tq-7-Y(=JrVr`d-@Vb&Dk0gR}ClZo%do}lwSVZukbCNp5m%z ztNhAPv)kdXgHwyyCo^Q+82gB_m3r%+)o!YqW-7;gPP9 zpRigvHJ-|3dJ^%O^D*d#e!$T%#^B%1Jm#}{#t#_w&^6P;%TVmth;FyhmyKa4Q0|m- z9!K12XydM5aM@W0k!W1elV);wTP%QN-7W3umst~X#>jX z+iH1~5g(|TDgnhpu4P%Fn30SHVioh8T$kYg!~>b>REZ30bM5WXcT?=PvV2UF@|HCB z?ot^--90+U4)mkb&l(bS0Bp!Wj-Mfjw^M;Ox+!ENq`QnMJnsxyU4EAg4bde6(}Zd8 zOBgj#KwYa8(YPJx_>6P^&6cH+R3x>o)KWt^8R*B(PKD&^qDt9jdA{r;eT4=PyOkh4 z6(3^{?Oy2+FtMwd!2D?Vh)9U|!GV^FEmB9AF=yq^s zDr%(rnjMU2dxS>Qfo_~fF_ctOINIuLV8DbEoygMya-}Mf<;R39*0vi40$hRpOG^Dp z%pI-Eq9$sTy1_VSz_8|O0g+`db%FgqJ@Q+APr^g8>`igDHg>mU0S%}q@vCsdw*g|) zKjE%|>9>?29^de_u)zW1z=|_!hD#{Mpsp3^k14lXtgq=gW&;)zt$?N)fy?FCDL82L(gG$ha>h z!2rc_UKMZiCa&o|?zgyH#advaxb4+)yJiKYrKyo{^h12L1m`Km=3wGBOIUhLr#2&S z%@z{P7BMOn>%4#iph~P}`^{lL3h+OaRR<2Nw34Mc343)$D!{hgAN7B$Sv zB=G~GC2D^2`~wiakeZr^}k} zN1ROi#b4&)h&dGxQ(%iQS#vgh{ehuiNm+AfdI6<6B(@d`8rKz~_OwD-rnw#aww^96 z`tE+FFK#kN#Rk?Kj8r3sJ+KRJ?cuZ(ZVU`(Ok=mJ9?t#P@rP+)YY={@O@|8Q&L$>< z6VXev-iQYkxqrf5s>MxLV^!DnP&z+NM6+w)RWT*)RXsFdM$0(81Z?dklA0H2w=Axc zpB#P!vb~}?*``3HRu69>n%Ozdydo!D#qlN|lFXZ*%gUK(yE@;y#HmwnACLrR!F7$! zU!o+`IMuHBJQ3~p3~&zI47bT$9}91b``ruR0dREy-IfO_NpfF$8(idYFHscR6!0oY z0S{bXib|B~vcvT=Vcn0y(j?1k&mj437yw6zNqpXrm@Cl37&j_*!^(gMxZhp^z|IpQ z@`VLt-!+=SrzGysV)4<^DeM~L-)0h1wy3l?V&O|D5N(+kBE7dolL#jz0(Y$ziDb>ie|Mox=PQP@2&45QmgGb-v^5MRh0*hvh-Ar=KR(cufE^K^>D&5&_tue>)8H zp_Ese0sVUGhkkDYmKAl}X`4%}nHC@FgBU6sI?5tp@$1@+| zA{R4ZO_0O~N%M2%_gyDmG1z01PB>sy%F4gCHeWbmIKoq9 z8DRG+l8cgoBf5V)vc5CGuvN95bEGTd0N2==51yWQ#Ce92Y`hoshPvPq?*RybVwD4R z^;Z27j%9sAPqq2m99V*_cd?>dhVzXeZ@B3so?`z!^W0LsbL`U)F$em!kqy4*db zrR@%r&B2;BED=?*l$5Y>cbs0F+d;^7JY^yypzvkDoX5>DQzmEJ!lSSF0W)hIMcqc_ zn?JEdSE|rhrEIr+Bgl1@DWJd{x#2SvGL*CL`L}fuh?8Yho~Ej4k2q@#{T~=%Mkki9 zNN$X4tt2d>M$lLGz?vJbGOn7&cme&a+KBrx5SUAy9SsB5*t|ci>__lN*1RZJ1p4A2 zW|)ffpPUnAEmzT~?H=J1Uo(HOU&NjkqXNJ-ci*QYkKX^(ZPMxYk=fm+qRi&(>d(`I zqGMQBUgBmB2s;_`y1-!olJ-g=^*b9c4Kps|G7VaxM9a{5gpN2vyL*5+K zK^e)a$OfNI-jQe%aRN+#va_0RQQ9yh=I{)1sj%`FZ%LR?kHJ{(SVQD&~WolR#^ zuX7-hp+zHN1w{PRHIaZ-4p(t@aFmiinR`Q# zl6FA8B=$8sEvl!eoQw)}!Q58)+E=E#6ts%nvY-NYL>Q&{>cOQ@c{ngiw2Ik~LE#39 zttlhAFfL2Rsd_;#Z9pp4GdPN}7+Pdu$3Jg<~x;&eS>Q6oOsKnmnjRpc<10*9i%GMiyJI-JZD31nQ`NpKQBAyXunw9ep?OieFEkz9YTN5PzFJ`ng4gUz{<*P)R*CF_S)feM`J~%w=Dk~m~jTyiA~Oc-<77rhn5h^O)yrU3*#zOhwr2( zS0S*@=a+A-2R_=^?j;@%yq=!3`Qus}Jyl8ceMdV=)40kz`a0;Esa`(MS`sQNk8}@d zhwH)mm)&=^2rk^L?_OeV6c&ExOwZdtA=z*8B!0R_V1F)9P@Avn@73|#bbX}e z%H>bXN+6Oj732!miVS4ohS$DwoTS*O;Vu*EAN37)6H}C&>aP5{J;s@1;+qBNE*u{Y z#07i{Gs(a^0l;-Le3aVr@{wnL&qTKY;3-@cqw+a5L4iQ;g6sTc&6Fyp)AR?i!q!Wn zsgO(tunBO+7{!id`4*9Ax*OHBF>Jx77-rsO05jl%PqZlCRRGQM0nK`)>^=LIEs3mZ zh{9^Na7>ovlEl!i&#U*@6PHiX1>)XZqp=fcEj<}!zkew{9Z*UYj1@{4OzNuaqMyAH z&^+?m1!X0&qV7g{eIkkz`XaPmGaHuT642&Yzqco*q9#dC{t|P~Dj}?qY8u0VQJ_+O zy9pL!%$o-Ik=YCVH)TGCQDc^1423tXgK`>%m0@$#mjQVEAC)?k4D^WxSTo~KeoVmg zFscl?W4`~RXyaHU(ZQC8`mvq(vyD`G0|A}cgQ%-DW5k-Nu_FZ&CK-R*lCNK)&=qQd z#P5R2F=jhN#pyX$S-_l#Twv+Y^I^q`>Y#lclmfBlT)Qoo+t3~@dO8S6&sx(N9q11^ zld_r5mg>u{MY3jr#4{Fbr)3tpb@MSCLq$etB;c{oi*$W#v(Y+O-lO+6fvYHC(@dE^ z9I_(N&IzSf=LnS#=02L$~N`g4+}OVeSYy#rf2M{4jUs6v?)o6Rq1moSa?$Arr7zikEc8Ef|xA%V04*KqETW zmO^}$VYme0sc3LZx-0~AZwM`xk#_CRt`e(f=QP}FB!K$?Jh-w3Z#Qf2z`s_L2%HGS zkTC!N1P=iJVKwPrwJePs^?ofSMaIe4tm47w7F&=5g%~rf-aTz%V`xtQ%Hsf}dGNnDWVjDYF-zbqaMgM!N-__b318 zXg7t_WMjM66<8NItX(t*|M;?!(S1aV<yH^m-;fNe0FP7g@JHGB_aA1x-Vz}2B z1SYt0k+@$Z(l1cMFMW!FY@6Jx!YHaD<?m?ST4 zA#Ko;wua!|RD$tQzX=jfJmk@^1;Yz<+wjU@q%-ku0!)Xd>mu&Gz>iP%8j$CRq8%K% zX2tEQzy+t1Ce==@I*)vDEWUB)>PdCY7Y|H50%BiygE##>TV?h6P2 z01Et1Yk)AnLb{9`9nGvwehcbS*KjBmh45T(!|H!Mr8P2(e_TmV91gnfCXPRcjPYy3@)$H5yu9Q<^Swj3J8yVGscM`l3= zlj5U2`4&f7?`~jkKt0UyT})9)0@G7McMMxuh;|IXxzdgYqQ_Z>#~HVv+I zE2dgVE~8b7UlXEQagWirE!Jh#^Sy6TWJa8sQA>OX1^m)+xZ`SUW`i@6O{Vzv&ZmFo zV1x6O$&KC2HPIipwMlu%jsO0g|3YLYS}5!!&6hYy^T4d*8QZw3hjLmLvG8C9I7sd& z3CTYCyAdRvOK2Kdf#Ds%=?~~d(MnipVuIQd1iCqj~uPfR{3%E+R6!a37kKT zwHz+RF=_SOH}x_DtspE#Z{+k@;qAMb*>YSKFMQh|1Pr!P`;5T8X1Ar1($QW%3xa>` zedIT(shj_f-DrltSHu@0S?*&0xl*&hFo*SJT<}HJ^$V@l?N`;jCv?9HW^Sv)@~s9o z{U1OYutaE7TL(rU>GxabkTBQ19DOI4fS^-AJ_Smvmy)I|Ld-GE@6iyaux&v|7BGM> z@VXJ;x+2(dL3w^Qz_96xyI7MqT2~w%c`aYQ&x0Z$o>JRi&E4&9fcFA2vOz@4=7MI- z`dNUgv5~fbuM^G+Hh!|x;)JPob#%vGLFhtEGzNI1WM)yL>mzGJA`HDH$$|ZvfXtdh zaJ+M{2G-E5(+Oq*u@P3sz{C%{axsiMqV{~t10jUqB7oBeZGcGY>a8w-2GOOmRsQN? zS2B{LZBMdOS?+ZuQ^k1tDM_$VgMk~HDcSjYHnwQZVt3;tYCZ$D?;(C^hOt*6cHMj! zQ6PQ6&I(Z23l7157USIT;@ls4SDf#P9gQ_eI)RYj+L;yU|mK{ z?pw?$8)K+)v#wn!;hVksgPWm8FzC_Zp+Oooqf8N2IIqaHh2G0^b&Zh(o2;UpG?>ag zA(a~eiPW}67DN5@in-|yGmCG&atmHlHEBF|B*VNN3oq+AxjO0MhD|f@%d7+9|$$0e!l1B$> zN6hc(50X|+`fkV!<-C*J#PD9k2rb+IOA(@6@VVu7)osLLaDO}pWg zX(#BOW3>~7`B5JQ0f`gIZ+5rqiNQ9$4QZ?*=XjD6;hAXa zes19IG}C{z@g+ewgZ^CRxQ)Z@{osvy;vIdvs@_@9l6O4cSR0V>)F%%XMOBHTYWe( z|6}E7V`FKnXKiHp4|adm+Aq6*ZtbtEj4f^S9F6{|kGOx)M>8uu6C(#2eltfaJzIx= z=;&YlgAR^*j!q8$+||Fbv$e5xvi&ut|Ii-RUv$>i-p0h<$l*^d{#86V$zNFN85kH@ z8hxBk8+#f9Cwu$<8=M6E;dz4paxnpZl`8)we;?O`{BcxDNRf|9Oj?-c-}l#tTUwzg zVcm-dzjlCxvNAkrtdz^Drg#iF4J^y)9HYxS3QyET6O|yNR9HH9y8paxfTn5%+s)!J zxJ%%?EyNuGHu+q;6`KyC7mEEKW{DAc`>!pkV1Z1>o6K z0>JRKLpwW#dOgy&P?aW)Vq=rDzJUj~YEIy)Q!3acQ}%(katok?feIJVv8P}+lSy-A z7RSTwGo_7_xH!6`3c>H!98Df-6L9?!~tb#WR^wkvb5c?QMGYq)0 zf;&oiVHT1{kr*cNZYS{2!BY`N74H~b|w6`-Jd_a{0eyc zL%dc0BmC{Z4gc&v|Lem0x8(6Te&j9VhEMVbIg1bAx9TMCjI5h5-g}Y180KwfoxVyW%Z@%w8{~3FX zebar{-8HLv)_mV*R(mi^aS;rvk}ND74ipq36qH5=?k`kjwD+4dg(`Mvx*il%y528q zR$v(l3hE6J=KcB!idg{)O3y@EO!%8y`my&1FXG;tbL&PQnhcNHaL|HdT!i)TO5Z2z zaC!fO45~mVXc(lNdLdd0vIiOpBxzq9f23Y#6#T8#<{!g}Y2hF(y(WE^yR?;BPTi=*~w<+%J3#W$g5YSih2+lXK?m&IBJ< zbQ?zKuWdaM*3iH}DA^~tzqPf(ldy7sD?3Uw3@!PmrUVf zLHd8f?R@u{IIwFd`WXIV4&jJvmCr6O>tb1M6WO&)OiVyO;Qo=KmBtucfSAL20l8BL z3RwaxGc#&hT1W*QRxRm7gZMc8%M~kSfRGvb(-*MxD)ADaZJYcGSaiyf@rptS zu&4K7Vy@{#poYojxZ4b|M9`g(krj$QA4abVZI7|n$tdY zc-&~)$s;tp8%3Aj4+Z5v;Vur4(E5izKR~ez!XkA|RVBlYLd(MUgPh7!%ARQ8`ZdfvlB z+2!C?YY(xs@~>#ON{e<;VIo=%{w9eW(&E#7gc_K>D&p>{t`Uw^KyK~n!#FHeA7sye`ir+D$n$Eo;Swu^@m{d)h^I>E931{ zA2-W)-=Vnna75pK#8XC)64|;E%YAyP!(?z1(>^<0%ha|VzVTJP(~`QZ;R9aaU?N;+ z7y^YXn1M2|HHj18)IO-G67V&$Gt_ar$rl?O7h%X84E%fcWh;tg=_GxGrbPV(O2KrYxlf-hODJC*xWalv~!zFr?!fs5EwHuR`9(xfTR+nSHleE4_vij4sQ5NvzAxD7;4Ny>MrYe9=jDxWN%P|&?LTO3KB>@+2 zX&gB~#bYeW5{9Vx#L;11+@B~-?y@#fcIl%jW6P?7V$pOwDQkIW!`nud#$(cV`6&rD zyvcr{z?AW;=R~iD>pq=C0+(Et&NuZ#gO+6D(DyGrCbO#~_3qrE$CY>DB>hXsscJUH z3=EB?dUtMKe>;Ll#?7ZH`j;hWC)OLEwdOumk@|R%Foh_@{yD8+Ak&LHT(PY?QN@}W zdsgLRBz%os1)2@m{R!O)5hkWjMTGaD-`SP_{EJ+3Op1! z!RFG^qo=uLnFnuGdU_Hroy*WLML}NFML;8M#bJffwIiCcqNQ{1CX&8+@a#@Kl4R$? zO(H4AW&R{WL)R1YpY~?cBSLbos3V8?1*{43GP;$rmJ9>NTTL*@d60mnOvi~kJSmJ& zsE@pOsIt@-szQ=t{2U=Xx1DO9i^|`jInpDIzHzyM%>;89m#gZmqSbzsR z9wjTmIJZ@>u>h{+Y4-4%2UR+R%MVtZ+A9XfU%_q~lDx8kQ^}WIO@42ym*J-Yy6-eR zmPO(JqQIi>>e(DeF4bI)r{1_?OKVfPwBy3VqEKMH>=d(tK-0N((?vrXgC}rs@*}HT zKUOwFeb^2k8l|2}hc)=Hc0t}0892FXSQcn-gYjRllL{#55b$e-QQ76ng<*qEjvYqM z3;uQh~~_>+L~u-FuiLhPVA{mxa>GO29|Y~J1v7(hg&3yyKS z;KQ(ezET%yrJ^oT+Xz(sR0y)khUbRw?oWv#?vAexJn8TD{GS3Zo_7w#hL<3d`s7p)6#UV|O zI!b+5$V5ump&Q%}ZqNF}2Nb#gVXP#B0iLhVvG6LAiy3sQ)^Vv7ETz|V?rt=i@<&pp z=(xRZs=&=&efgJlw`eMP>}fYjbFDZg9EV1d;f3;qQNsu4%6|vH2eY4DJ$==K6A-kK zmbq+5k5vuO-@6mFz#znO<;loRAgF8OBpeOveoEuPg!vuPj)qIJbC*lJcLr!ESxy9;pa?QYDOrsQ*}1s&T;w%R@_L0~4A0XMUJ z;;5XC6szg14vve9TlIdaFA^FYj#+iDtL-kPbg@}VPvzzd1J3Jsi*uK!DJ2!qhldv% zO>}0Kduh;ZUhf`vWtsFh|1LR1s(~52*wwA;b6N<7TrYbrYDPzQBU>irwgS)vx$Z7? zm^0WrPn~7uqQx5F1C8+tiyv81$H)I=t=;75%Lmu&d^;H;`%C)V68FWF>i4t1NK(1e zUG(BJSGK0BNBmDkc*+@soIt)g5%ic@Fr`R48Aga%!JlV_>n$JV1i zrU`0?o_97YCwr$6_KUAnJ>i(kJRM!1h56X-jnxgfk)1lq=inmi$%$H@qhNgFa-U;;sn$TJ zI-EM1Nx)$LYvk7LEL+^IwiEoB9R4 zD_o|<<{zW8apWtOw50lnPP|?X6EjnFKlWZ(w*Y$8xN9Fl5bGX><{hdAzVwEE<(xIw z>r)JK#exG)KPR%O{JdlFfuzQW&rappr|%%{`nvY3!AH>4WqD$z;r9;vC4rBEz6hph z>)Px0y)Ujm(1{#!+ja^J`2X6SwN)V> z4*;vv*(r{km#rMdvFRBBueTL$f%7BlwFBUFFHe8*@|<7eZNF~GuI(x*|7xU=ap{VV zNs|kF^rrvz$5~8Tza-~TiZN3C^?s6_H@g1tTKW@Z`I%4MQ(XdHTkB)ZMO>k|oW)ds zl86mP7r1jph|=RC+-gL-ANg(dvWq>Y1$e%HF|b?xF;w2*BE1j;yiT^dKCefN&7m+8 z)HwkwS-@K<4XFp*)L|Pfp_ZPi72yK3JtW_g)cZPz6y4<6`K4iO3c#b0=<^jR#pZL&f31y+4%CCu=L!dsv)ZB%{j(%*^ zW~KV&frd2wX>{ zNsclq^hwI6`uu%lu57ve*Ya?+`m{$B(GD;QPjNNtg;Keh%;()mb<-nx6QV! zFZOy`(t{%wapr>81`W$hx7{i*3>ZiE{L!Ro-K&e!JD_aJ=6z4@C(CvoZB_4Cr^4)f z>#fHXy_MU|3GB*N<3ZIJer0VH=cb&CrPI{#hvBToEzd0A()RWISDeJo%jU3Mk*=d& zbda^>7rwKuVcUj_@IqkrU*c@tRFs+Nw?x?WT}TQ`MRqFBNvqyU>$n+LX2ptzlVwln zqU0&@2MMc4gy!b>{zT#N)!vYo5eO3vBNyS$MJ%)jkYrnv%h@i9b6+*ZM-Bisr) zd#Nu=nU6&xmd74KJ*w@v!>Q5!(#s_|IZy?BN}vfb^SbWqd-J@RIaQsZ ziJ2KmcQvB(P9^v7H^m)@)Mx;0<$C4)LQUrIz|T*)PByyoXV#o# zT_8=lXxuaKZJfp)dtOYp_P&15@E{fl=?2~1hr3Oa zhX3v;*sz}x^n4Bqc)GI~{oRf1q(hzfiwC=yt_UgPU=ul2ySZ%8G}&6;YXda<_gVvZ zyO%5?n_7=tNab>PcH3%nf~G!g4ZD1Qj(ji_a*>>8Gd&|X|*ySh}Z4krZ; z4^+(-gOlQ;z4udYZ9`zG;l8+!F_Qu1{61{F?NSj@C4rbAU#$<)%<0iq*S}{kdbdo4 z;D<*4qBZYiE|Bo!Mz}Ktn-vzst9?sRadMoBJ8gIa2Mn@R>_e;AQSDzYR~M4uB_!Vj z_V3}r_1bMK>(^6op1r-^hrE11FWOt?W8k)W?78eqjNXK(KyHc4`7<-wM}&ZH@rpT- zJfq#9Ep~SHEce44QoQ%l-n4!RGFfv9Y!f6uSzY7SFBvIFgPBGwZFtjC;&i?kUrxpB zQWu%EK8CC{dXe+;ZjB1^CGeLW{|-6hB1DN^eb1v8ohcVM*Td6tx@MDpaX;n7yJep% z^bx{ibNmH?4w$2mg*(?tHlUi5CqTXQ=E;k+vfj%YUjAW2WV#@~pm2j_#=q26W#@b4 z9Bcv)e7~VR-2x+|*^Xr76RJnjaBNc2hbgAgnUCyBajZ0y$X8W>OL4+>@ViAsbcs?U zQl1#-_pi8WD~CN~C@Ceb-=2v0`N^KL+~WBkalVxDYveKvez?ewT;RDaKUW3jgKfR5 zrZSRzw{^0=#Pr!shb>EhUp=KnRDiK#H`Sau&J#QZ!emu8Cn2gjafrLQjD&(EHKlcN zr4ZPb@iuJ4P~Gdg@lcY; z8Kw{TU-0Zzo(uDFtN=>{V#MRktA`E44X0%Cz1^Lfr)p{G<4EKC_z~hS^5b?yoZ1_b z;+iZSgDm*8GI{K~@|h6S{!@o1Iq9HJ1V5}d6_fiF2ZAhkab&Tsi#SZ=Dfmbg^)~`x zXvt#m*y1uXQg~6Ckc&6t=Z@w;T8T`03(E(|JT}3J(|bEK55P;?NXrDvhdJ|~N#)N6 zx|P(-ML5((76b=M_#9b){gQ-|cV7(swHtq@8MF?Xq$CFQthejWpC`+Wu#2)j%o1(2 z$Sc_G^>`y&zFx?x74b@Xsi~>!9oZb~pX`@-BKPYx!amaNvLY6s9FxR;mz0#0m5tzB zICN|dB4NAG&jT8e?M`}NagvQ-E+j(JrC^dj$DK=yii*m}ga@)tgu@^>9nQ0+=$R4e zj445fV|}&PYIVa57pF5-8%h3?MF){E1VK%_)+r$;oesto*&Aa^n|4JeF1h#k50Uk` ze0j5;j)wNbpiD&?NwpXmq2$qa$>bM}*g=}h#nq;QEC9`OjDh@}hB25v*t_l+U{G_E zwh9|TcLg;kp}eCbTX(B9{jVU+N60fRp)ArT5(P~M2iCWcKFBSWP2A9Do`S#;Q6ebh zAaU`{cGLI~S|ym?c!OW8VHiEwP;;Wf!hVZ{P<{rVg&eCt%}VuE4CHT?!)-$}(wY4* zT13E&x`1uK#tj5Z{cA&%J2?wI&1WhUFCqsmsD|qGt7B^m$+~c-f#p~=BqTMM%Trq= z1Hj$kLM?y6npD8cZRD*7mkzWZUszlm`cz9DErT?ybhDCPKx|kLn205Vo)eFCfSRZx zB70!}1r@<~o<-b<3I+oVjvqDkv+h1l^PfWE2N)Q7l)w~1+|ZRQR8d(fBAFC>`e+E^ z;GhY=9(9@971-M%?eI4vPU=Jzj>$jK&)8KIk^fZKY`KYU*`IYp{IV{)0J;Pe6_wb( zNq3t7-*#!wk6I?jk<029{cqVF>CTE{bTcG@>4s<>U=f({&Wz-fDPMIUf`M7mW0R}p3yDS)fzbW(H}vBk z^BbWI%^^G0IbGR=3oxhtkl~Rfgo{s=`-HIk%J&-wvmXJ$x(i`0APcW!Fa$s!@x^EY zB|qWLn{>VQsJqeVKX-Frw7M``1Lusr8V;Tt+=Jw;fB4xkDk>+aK3?junvzaCMhg`1 zDi@Mlx=V2QP!m$;ZG6o&!^YkO!R4Np@Sy}hp;0m7w}Yq@@QnA{KJ>+LoriS zMMav!hazM-H8 zP#!EuDDafJPwt54E-AJ}`+>ecAe**gbDL6GcgOJ>6{FTtD>}bG8z98Fv&$SF4tLv@ z8n3tfHhupM7A_%F4QqlTIsW<(n~^gRb5AxJ8@X*ObY1|33yqJs!&@ekA9u6Xoyj7g z*G5)Km()!*jMU%wq(evMm)-}t%~RFj1`vo+tcRAIZ}yiY*JqE&p4!2sVbS{6-gsl} zaPKWb)ysTr!NT}0X@F05uoCw+7$+FhAvT*?1}qWbBC!WE(R}ds1JUEkT$KNz8#)Xu zij9BgEwh?P+qOr)Q9#C85E#4pR42;%pL_vJ1W`lT($W!AQLQCG zV72eQ89Mhs>RjxDA*@rwREPOUtbR$SJ57qOip(Vz(lzB1ET-W9OjsN^26vF6OY!gVXYnO6g z)fei&ABk1TawJOp3Ma1lTq3YAOly<;9neK=gkFHkGh!pmoC&N{2mJxNZiY{P<`SBg zkfv>c?gN|K%SsXd=^yf@yo|xumO)COko`R`)I(>GkZe={DTr*i^qt(4Ad=+_WQ7YW zCp5%oX!3^F7uf`!`*qx6%~ZdTx((E0Chd@)GX33SMbzj`6oGxk#cq$q6vT(R0~ndm zTb?Aw6I_XP?@)aM10BCUB7`o!Hrj2t(>_Hr7Q1r?z8!{8#)%<@7e8+I|UhS&&4)}3A_Vu=lod?d5vqZYgcV; zJx(z@)`5XDs?+3SDjHKndZY{0R1l8>MO)Mny@maB`^j^xpG9UiR@HTlKq+m=R1|^s z$jC`|i3w&S2Y>jp5x8LPi>#bSal%E^OR_OQk_?kXZOcLEP0B4^iU9B-I!>S%l&3U6 z7p3}9bA0+z&z+6BJb{iVOU<086P`6Nnx9{gS{1n<06LTS$#b&Hy!K>%vTDG0iHKtK z)5FR8#Bc8HhAC=;eUCXq(1~s5P8dYR@|Fg?dJj&6oM@6fTlL$~>1YaWdC*g7{|*uo zmpyHEpFBlFP1?UiaRWQ~sjWWRmCy!WkDXP)nP&#>KZB8qSWvUQp9w=ZVi&F?TcTZ8 zf#y@W2v*LwLZV96O{GsrS^asO$GmM;YV9v`uZxGUcLPRw|sqFw6V+PT%4n}Lf{KKZ%m)V{)|ZFCKw zc$ilI+t=r{Gq3YJqrA2IVeQJkrsrx|IqloXHv7W7#U@IrjMmoEv$u!mSII1ASB|C= zJun%oaU+)IGfE#zd@av~|H z)og?=ZTUCwbY$Ztzt+5SdV-~iJ}dNO;rdgQMP0?iAE0=hW|X)7(}+5*1yhBwYJ zyi!+XX3B~tz^p2uxW;a1Phkk`d7jzgy6Qp6S$h5i53kpXX7+S2lT|q^KP)wR4XZJi z%4xtwvebS*L>GK%blB*=R%5qpahBrKnX9vCWA}W+97IWjHZ1MnQQUOcngygdKHJ}v z*Xym_1CwX)DX8jl;K<-xzdD8WBluDQs4X8fT0!4eN~7iaSUq1BS$C!d?soi~RSmLv z*F)!*gQmw^6j24`-*!jMS#|NY;9{sIhW~97$T2F|3F@v^{99X8=uSwO=Hoof{QBIL z*IihSuaP>nw8EI~2kI}o*Nq~m7}>uG-kNz5cD#VSWAfV03AwdXB;>S^G4)W<@x;SH zIJvvIzGh?Fo>&?P?bfiOw##N`p}F&gE$WD8JAcq;N*D4m*Nfh2CA;xk&X>8^RFAV(*O!~o`ubms$jnc##s1T%p3zZhY$SPn z1$i_KM9R-Mds$fr(~1hxSZMNR^hyGNqos$*N?4NqYN{U3O}JKE`{SllIcGf;((s6! zJ_mC_{$Gw>OA>xl;_MdX6U%G(^~oV%giiD7X6ebri+8Il0b7>6{od5cc92%o(&zB} zTFtJV*J9TiHs}8JUMQd+^>VmF3h&&qqsP(k3Mv9O6s=1Z$8d#Pj@i&(4sz`7w)Z^o ztvI=G>C9VG6pPAFgG}J~${l%#TJUD%OIu>DgnVjaEaL=DQW`tu83FvlxjR1~AXpB1 zY60~)L}T*|*VZrptAS4N+*Xvlgxo%XAyjofFTyelkw2WMJ@^zd)Ju11g@I-6G(pW8 zWZ-taTeylPkB-`@Asf_{q&tMWVvE;p9@^=&hT{3HFt|fXm9F6e_iG!R!DkYZ+3Qe*60ArjAiZU=%|uK znR@B)&c~C(-fJ_7R~#KFT7G`L6MVjOa&soGjxG?+`)eXLwg=-bDQI1Y#R>wsa&vO@ zi;%Y$k=!=%)02!!f|}46cjCdSE&6up@mQN7gh)u86!tU-=+_I8 z<2?T|`FSrAuogq(>1~b~DAr7rzk@=`c&jop(q;CNYpi!miLOQ0G`p80Zw40C*;Yc2d^*&Qk@|NSrr~ zP|yJ;^0KH#J@&G=Sm$av z0W z`Lr+LoOiiso*Wsx7kc#xNjmq(T29mWUO`oIrZKXZbXKbFKN1WkYsB$b-vw?A8lJ8$ zNPjfnfQfwV3yDK{i|2^&h}Uy*`Wd+!!=RutY_C4%dq|#=_7T|$ZfWYO;KR<;r%WEG zv)qgTsnoDvltZxZi?d6^+W1uUBXLl@GLz&Ql=2d@EP)#WRsVV~%Tsi+hh)la$?y0t zSD*TkUj8*d_2YlOQ9s82HAp*UFZQ`|mi(UTocY371_fIo#ITEEUdw$=*0B^#3K9Ke`66KE9OR61#N(Hr$mKS1)Zxd2jfT60^Qy;uhOCr zg7J~hP$nh2rRpTH=@EuW2n(B+J#8@AX$#_l>~I*K>WsK|HQBN5Vf7PQGvh`3Z>j5mH%~>L zLIbJ(@efc8TSf}P7tewePIaPf2E2!fSUO+WEJwIo~b5p)B|g^4_Hd&F}*w$2aZ{vU&UQ zGHf17Il^eN)L7OXmb9xHI16PB-&SWugcWAQyp|}taO6+?uob!hxuU$yfk>;QvmgcA zW9N<}+tVAwVS(Zdur=G8T`T+1INuXKYT39Tnqb$;YDL41%9g+I!_Ku@(zK(sX&_g=$`tzgd?+*r5!)L ztH)Rab4uuU32LvUU)q-}hKQvFMiZ{eGj{omSN5z9<2XqFFDV%Vccd+)=S7^R$90R|%o!4M?Lo=1B z{pTC(+=U|zDl5m~ z%w;bAXL9!IjbSlik?d&v2D~Fkdq=!w<0UG7RXMIXIu&Rpipnt*CS%(|Lg9pvs)N2qgOAO`0 z25TktquZc!6M@fdbanCE>!a>1ul6V#vnI0eHfSNfAAb2Gyh4Lu!M;i$TkO&AGCZFV zrAz#E9iHMfS$=+^#-|0xZ39R=+3|cyI5?hzd1{;rI7OXz(VBC5xoMKnm;hrWhojjHF9`e183t_dT) zY|J+X7&^nkpCZXmJa0tch$HbzKI3eB@iZ-tWkO6J70pNpMVr zDIUD21EDmWLBsY<9JzhgSPZ=H%4l!Hjm%a5cgKET)-KQ7>TAqSIuC~BItQu2?r5U! zh8+mYX&RpVzK)3HRt(mc!7YU4x5+s4Qy!0ZV5Tva!Zw>d1=t$g9h4}}9wAza?X(=V zx0jW+s#(c!{c58j^|t=EBbfe`X>1q+fpO#Z5piOo?(^UMWC;Iv;yhiuw%dNR=A!cQ zqgme5e>IjK#HYt{M}R)?^X%eQ2XK>=R)F;PXI3B!?VtE2_r$~fuC3>MK03K$@!xkb zx?A?d+dcS!2#^qe;aJeu89sW`YQ0?4Df8gEs$I&E9+V#cspL~RH8M zLpx?EC{Fv8!AL(!$SpL|kaJVhE$TbTC)jjbL$=^nj7)qCLJ{*UmBIB8HRu#(WLIo^ zeS1&Ez2cJIt-eg3i!-~C%o{un&(V^ZwdI$2Cj!mAHehjAmGf!U+pbnIsm^#K{zWo6 zDZS)%st}z-;Bx55mR5>e6q%-8&0KcyV=6g+-EHNpP18lRC;O;2`-abxft^~GdDC-= zl}FQ3S}LKzQ}@~F^TOCI@KTY}VA(nC*gO?gYF&6eqat!in9){{<8qbRjV^eJA*@f)auwfUH{*Cms&=OHy2i17PcZt6u=|a3UI(^B!k2{CFD4PeD*!cD zEF_}b;n&FqnC#~HIedi&6lY3J`xXv(JV19&MU-$2%AS|88y-%5mUE!3{-_gmm$mkjYpJ()9FIJK+SqZ*6Igj9>zxC%?JNS zYB_;m%NA1wI#sH7cj!mx@~qs7mRDTOY+B9yi*+v3YZ;_C+q_Od$lRHIPoKrAQ=CDe z{Vlw-60d&wcC_EbOwUpH2Z3`aHU373$rs6jX-m$&{OL1zunLFM3o17vB8}Q;uttbC zPSh{lIEQc{L~7pb&<ZyY+2md9E0wbs(_<)ZlyXOZFsoqbwzr_nSX}r~j znwpvpMMJ0n`QbhME<;XjMh=*9Pbq9L$}kgg)tB}_s`FOB^HwM`u6np67}N9aG{Jgb zCYwwK>Ez$VR{xF1`i1>1k{Nde4yqw@)i${f-4$FT!^mYL?oEJThP0L*c}X;VCIiTGpz6`{wNI z+|#4-v36XAFiOH4+#gMHbmP&x(3&cI7*Y^;pTi#<*cV0g;rDuHhC=PLcJu}@;}1o} zvHOdy9Z{v@;&>d{1*Mh*p|@pj;n;CO-2Z5POeiOy5o&LY1&c|B=Gu!%!}{G6K&f9s z{ngF+@uFW5{QnC7jZYsx+LI}4)Q`@zSYmW{(C|b~5!?sLu6xVqUW4((!BQVZw91q4 z%i$W0@xGe6YprJXGZcRMA4D1KlpU&6^?yN1mQLXA7e!GE4|TrECE3-E3VhGN7<|v= zriLY78;C0TW9G+4c7CkT!TLZqQml8}DqMgv9exuW(xW8PhUtl#F^WGy66zu0vNm+w zPdILC3uX9c|Mt^;29(6L2Q#h*vXJl+oIIqokho_h=oYK>n>AQ{f!vNE&ktQ!dK3L2 zmI{E+DVvpbBb~E|;C?-$3YFLR{7!Lsfx4Vo-<>l!8D@_WcsvivWoF zb=SRJvJ}B_tHf06)2Mxq&PL8>ZSiMw0ZNx|*H{p98bU#A)VPUr;)$BU^ANdT_a!1C z|37-bg2$Je??>DuRP8g7mHHJ3#soA<=TbQbJw^3Gd;N9nzP%t+ByG9x-^#Em5F_Bf!$$t6(vJROfc4Cm)+Lyo*$`U#E&%1guK$H1IAVW6I6AUmNg{OF zK{(+{ua5V6elUK3PM872OryL{jT@Bf^_fFxx&%J{zecCu=(ydQW?B2Y%kUGEv+UJB z7YoG?&L6S?n0@V1Kt-Q7v_`s+g~zT7bn^@t#3Ia5pRlH_kisBE?;%2n9CwB4Axw1Z zKy(n_zna8cfkt$tccj4*Av=Zs1Up0<@Rc(tPEZ3SAA>CvK~$`-$0VlEM2tEQg$Z-x zH&4zVksd?VM{^bZcQ-;tJDB_3cG+*SbFeSOryQ~aCZG$xtVg)Yfj7K5jAbxRC9t>( z`N)jZFk&tii*PEZ8~Wn+A0oKZ@qUTgLnGG*3`X*tL%ydVzn9{SI+p3Db?hCtTH!@D!CAq~nc#fl7Q*ULVM5iG#nQD^$T#$PO;uy(4@fobxb; zp&LybP_`X>pYMP-eXa;Sy}wKk-Pf<;+K~SmMrNE5yILmkiWLfX=2d$0S0*? zYk^n{=p$j98m7Cf_^J>=N=MFT%as}1cG*M~Lu}b&JT^C`tu}WizRt}*?o59eH*}XZZ2W=$g|>FANV%YetQSo!{#)S0nIRZr<>8!oz+k|x z%m}-4U4zM*e<+U^9k&bJPs(#$es3jMW3>3ayKGVz@4uF_z??iu7)5=be(OY51wzC^ zCPjrsUZzo7U@mYVd9tyC0i#iU{C!^Z73p7u;A|KG&|Z|YLR0Vi6|X-iKkwnd*80AX z;Hh`|-9hUtlmFWvBZ1Ko5vo>1`X)v>Xw&;+gAR^+m#S1Z)r(acKOtT zRzyD^#G(UvCQTaN`qWk!lAYU!RkT*tW%08KryMtt#3e0;H|B$7OQ#`~^zwoIpFM8} z68r5XKsOvq$s~9~u&ytfxL$K)d9Tc(dF3A&Q=bnQn+XSjJxxwOCe64#OZH+}g!KLl zwZ`DAz#=^7Ap?Wd#_+{{jT47j-1}ZR3gjE|G(;+1^(MuJV^Ff5ge&B1>5aw;5=_fI z*1<>mY>!YKsp1$U@2u^(QuMa+cW=KyLpP%g03oT&%`Bde0p*u0J@65U>XCF5NdAS- z!7hYqkMgs}GxWO29bP&5v=_9#W&3WrhnCwD#Z zYX7|d)P<5m0T|)u2j8;uIp)cqNATab!(>P&QdH^FapFn{YT(|2nSs}7Bh7VNG+vfx zjdACYQPXMpQr%v`f#BP=s_g23RvsH*;Io77%71xu_f|7nq}Xmg!AE>`C;N($GqIqp z`1+c`eCE0TYj-PzhiCEDcG0`6%53LT(#dL_?MquL8! zKS7^i-Sn~k-QrT#%VC%S>3nnLjrG*OsI_@;_vLV+n%#rPU7L5>Z3aHCyKOZWOX zr2@9Ua8xfox2kfjB?jicfIg9I?Q!(qVVpQ{F5Z9@tNJhs zHS77Y6KcAze9iwb&Tv9SDYo|jM9tgJ0Zo3sRR6tM*n5Tf7zv%=9|t@TUP`CLij&rxvD~eYS{Z$)5LfBNC6b>j~NOUD98%*bDTl z>}6s`73_U|Edu8UAZE)+=9-jbhUmcJ0Elfv)o7jI!TCF|`Wu;^$q{!E& zm%W;H7sfSnk7)WJ{~Jdf!J-&QlO8(n+xerI!uph(LbLYVFGwxl?0XF?q-ih4sFO|N z|K;>_VSJa1uZKMQ;nM!UE3Nje`LAY|&i>(>*c?*~tMHYszS`*wr@&tow!Yj-zQfSH z&9sdES>UBS=V3fiD`_p96$h%s!9M;9bhEQP-#Q%L^q`}-$R>jCDw(h5YI+{$~#y2 zu!-Cb0!AQU?6#L%ovaz-90C)W(+0=4eFtm?kK8|HKsqAv3ohe3TUf>fM+z(YJkAQv zr7~`r>2cK2+H~K(6izb)aMq5?wB2{v$WgGef%a7UX6%|Vk!esvaIQBE2b(N5ABaRfqJa8P% z79k^=d1aRVZ;CtX=jCr4H=YqiPu{k9!j-p6K$x47aX6yvVBw_iKgM#m-Nuhe!#`N? z4QHlPuKsG1jI?a*CwjIj{rTA+GyeE@^G5`qewIS%#S{2e$lf$vJpgDZiMFt)Oy{~v zB>F#Wy>oD7UEA#)b?l^LI~{jy+h)g3$L?TtY}2+qO^c`+3g$y{GE?@0vAh z?W(=>F?!Q@j+(vckC-%>FYa4S#?dkRz_l19rc&ZqT1a@R0zfl`l7YdvtKk-!zyTM<$zu2x^*| ze7Cv0yu3PROT;vA;pEG!`#r7Pd^1=i6OuvmV{SkMWz-Y3RIcx4dIIY6QJy#=xD61r z5s5&+6)32-m^VZ%Rm2Wrrta*F>uQOHuw(iTEk)(Ojba2i&8O#X=cba1>&tfz{JM&L zx}%I#!h?Jgkti0gjJNA%VPiX7ZZdEX)4-b{7rP&Kas{tUT0!-7T$-DkTUnvu3p>Y) z|E>T>2hj(U07(+0=8vRL-VU(~CX~Y+tVO-$@R@TZ`f8r}b#i*z+=)QEa#oDS@KaCw z8~Nw&2}1*M2J%P@Uc-w^lpHZJi8IqKiS^B=c1|{CmAwPd(yW0}OS7}!vWn_vzL|Nw zgnolrbz+5ug>soZocolKM|aQh@#y9ZG&D3jzqU1`rQygRf*7eNBfIa|J_&Q3QfL8Y zMf^{r^*^OwzA`y(Xq9*C<8j%C2Xf?yhdqQC@!SByl#wN@$d+jDeN4z)_V5r5!C+yk z_OJo}0#kPe87BxVq$`e9$u3$vYqqW+ZWt#)Fv4>PF!9Jee3|{93ox9ZOlQ*p19;zTNq-X z&P)aGFK3Hw{XbZn_JQuCf-1@%B$Hj~d)_BTLVX&TSCVS_WCZ^@9J4i~5dA|WblP_{ z5nmjk`xh0jB>R3V`Kk#C5u}yID6}T1gh&>D?rY2b+1ARcucHv}9zwy9!01(n9xzoK zNo5LS1g`TC`uh7Nw?`A(Ob!78Zip)BVW|wtj>g+6!Z8^C&|iM6xG2Om6qI%m$FhOX zBW=T_ON12vS2XQN8jPOzbL|RGA;^l&-zZ%2g6MqY=X~Tqf8PcV6jOct zw&|8oh?h)=*w=+*)S;P=U>$@{vXj0V4H-NtAo(9nirQQiNd)L@{e_lPQDbz42i9Fh zsMFzwez?G|HKvb%wA%Iy^$mcZcKCe}n*yO#{OC*Ek`EGCWCTSH(=NxvKonTpfossL zso4Yd!|I$81H109Jdyo}K-tkm>Gt)Sp*m-M_as37ZYbgsZvMMCB38i-TxJmintPRc zdn9i4TPg-oEdT-k5d=u^s@<4aej`tT_j&T>KKEwEkbTVA@BWv)i-282I$bjSo_-q9 z7Y-IyVq~nYkH7A?0_pn$?}Oz#%d+U1P1I*_DBV$AK*W}4KZnKo!3O~bK^`~df(pS_ zszI?+@6M1 zgBv_}l|@W-tePZqGv0W*G6OW{mV{d5V7HZUPf(y*vJ4*$9Ub4UonmOrhZ zi1UdG(d;>FPG9+B6ho#yS#~8|4suXV7PgNBhNU)qtaklhZN3bLhg(S!Q^8Bl=oDa|Hne=x`#z-p`cHz5 zRRqzA7ljA&tR^HX0|+5>k`NV({JT=(!+E8&ETKs!k|6jeDcElL$b-o?h_NJslbw*h z`7gqfapmxWcj%gg2|Ca`G=eF@gJ?2s7;rbfX14qyIrZ3iw&uE<5xe`K91 zM%Zm?UK2KSi@Log;(xd@uC?gu3BF%MAB40^u)qve;e9~TJ(F()C1}HPHad)Om0|Jc@LXk9 z*`J@HUl6lo-z9}Uwy-8|rhpBvk4eGpmC>qB({QLh))zH6N^~@BPNUf63zvJk4N@5y zX_)PY{pR2Xmax#A7d7d&sejxl-N}q~RNRQ6FNl0GR2Ba*&3cn_@l?qSEO-r0f>1)I z>w)tSChwu*Khd*uGR8 z-ONDerhiJ#TiMG|nx_*fH-QVc6MUXHqCl88XuRqodG>fACPi~3@u8S3+WtCGQg*(a zw>h~}epTi3HH+*i4xuq}C<5D`Vl zKb)Uu&4Y;Nx;JNu@nf%H^g8LW#-?=DlkK*X^M!~9e>iW#{RxqO;XU%|0=G;$ecu9k z6CuncevYzdUGO`2Y8)+!-k7D;PZ2Y^v+aq44+sW_vb1vM``nEy|GUyk2(P#BzVFhkInWtoaM^L7{i*j>wB_HA<|+Y)4e%nQ zi%*L4%iP1+I_}JahJLy2y!P$|=hI8MpyR$&;@3)LG*BGtiR7B>NLW$~GL9uHDemfh z^TN*Wew*}gx?WvYfA`sJ1sA`o?zZR4;KR?QR$!wAKPbz?TPl`-yUt^D{dh;U7E=Qn zc$vqCdWG+}-8Nxe>0I-AG`6>B27O@5JdXW9{EO?d2sb;XgJx#Y1lXc zgLk?P8#(XuB}~v%YO_lr+iBoh-*`iChk`HdD&0uR{6u1Gm>t>`aZffd+XS=Q|aAJLfIk9nb7UVti*ZY~2(4fupJ)a=W51Ri` zI|l8_G5qsK7^_(I+M{_<{>$Ta_o&$-Fv7|*ilD@PMng-?R1HzJ6*vhrXdZ0v;(Xf1 zg@M1LQPDG!qT*{DRpc=l2@id{czt-YaKx~DYuKSYi6r3kMbWs6q^mucoIB?LdhpA| zvN*ik&P8Npf2`CwNIUbY8s3#_-Pt(44O7R_tiMeH;gFUixmKxB*1ADQCl8*=&cm{J zWu=I1l}sl3zElbxczApua_L{9RuRlYd9iSc+hX`GnaH67JDFaP`DF!>O#bj!Sq-lD zibY&FncejiNY`9n<89osOGEqcr1W>M2idMn((6=_f{| zt{eunM;p%}mMgVeul?VW6S>WSQGW z_wG5XW^f{d%e1L?AhV~WNsx*lJ~{VMO*b$_)N)v^@;JU{m~T5^Y|fXi84+qCS@@OR$UNk+-H>uXA0Re2TR%sAd~ zEo+UHaMBdXt<-t%V1IX&GUdw3A^n-3I{n)|Sy#N`$u0qEA;od@Ad;}%Q@**WwUGME?iQph#viC;= zy_INlO8zx*nZ|F`>|0GZa+>000gw@-SS3(*uv@p^nsC3iRP%zptA3jWu*xvKhcT0~qZb1|O=jIr#I!k|*@SXfvDJ)Gss zri@~YpB+1=%KoZcKvH%NrLLSoyLdaO8{MPF|6xB$*9g|>I@p+{0ZU1jrd=}hP3zhc z!R`0Zt1fLj$g3yfrSOB0{~KomDx`&=Qe;iu%zcv(ct_XU&MLdi_$%J;><_RvX(!*! z8~2>MZF@@=AbE7}cA9axh?~0=#5FZ{)?jdHhqc|NF^x!~Sk1OyM_rPduDN){3s8R=Uy4KM0^x13Yy%CTrP?di=1MApvkjy2!hNUw;6)4DGG$}X!m z#r(KzkU}`SS772}{CjZI>+Zt&JB!8f}ONd0`+aW~rT(~;aV*L8LeW;%uSN2{`B|2_KDcm=aQ_mPZrl6b$Bs+WH2PKq95 zV`JBvj{7R(uGC`ZpnrDGLr`^mzCChj{n56UZyNn1r5y`z9fZM|K#dbj`6;4EL;#=*|-#D~{;Ha2faQsLV9PC^z2sP+%h=_TvYo zh8k9@Ckff#JoS<6Yz^j=ste`oQ=cYp*l4INwX1J7gw?dR3^}MK|BWk>{u5UO{%>9} zI5CK%2Eqr~G2cy>6j8~cj3aEDQsxrr_irT`YMWJlY3f2MEF;N{PqOfjRE!V*5i{(j zF>*Iqr#o-(Ok_UG24t3pNR9+~9Ev5I97NVnt zVpULof9gb54lPm--X(zyn--6|)~f%lH1oYpFnFm6(ET;!MH&8rc(X~=z2?e8OAc0U z$N;eF6`uQ%u?a}QO=I#7bbK$QU=cBbh&$9~Rhu?Kkq+e4yQdDKE!ehZNsL8#iwxCQ znkkd7`-^~g_JCQFXF|RA+OHBGEgc)(uslPGrefpqfXlA zpXEJBwz}_Le9rB_1@Q|bNFYC(m{(v(rWgoa525)_(rP>OvDMZdmtuaYan)<|rL-LO zAF$I?o==NS6j!{|=oZ*tNJG5F4&7{t015KS|bMZ6Y&o^3@9uJR2U~AT7s(&`8Np){5J|}D&8BZ zrsgyoDo^rkW3PEWIES1&YVN&j{&|k^kL8jzhZu)6S!AQM8poctTU*o^28=lH^AfRE za3Y-iRs66O=|4%U?)~##aQ;)7ieOCk8>l(tg)bN{HYgs%_z7s;kkl{xPk6yzQd^Q^ z2}(p{6rhl5)L(&rEynRmWK_R1Fkj+dNev|XN0dTAhjxpLKU+a&v+-^pgo?op)k#sH zTYTP*ZDQ#_BxA0kp`2u1mbk-$Vk%E}W=`UY=**3zd z{)4te+mybT1V1;SCK9(_VktQIjB zY!qW|mV0ahvh8LZvKXyj1EONHOOl9kMo#u$?IXZG5{Z)X-@B13w3+Jmxl*)O5nX$H zaYu=GEW?{$FrY4nUN8=&0{^j5uQo3F6pMO%|1^F7$MYQzCiN%}+_G~q1-T{b zihctZHiw3UU0i}!{#=d%4rPZI=vkliH*Peq6u*!<(#IcMcrw6)*uk*L7OGbCK0E}Z>=0bo2!a+4 zUK|E3s~XEV%!8oU5#wdlkkK4G_s#MDn(_0<$SjCXvj&0FpJL}D$3^7SDP(r%%GWw= z0ch0J)Kxw!d$(?0zc)g1E#j7hUU=n_peUi`*k9N!;EO!N)yiZ|qR35tvf+~BVU@BA z$m!5imLbdN)cHltMwr5pi9C79CDD9}Izh(xEJMb)>CKy8LZ5&6q+ju|a31$Lp|QZ^ z`ctq7u|@ztF@_IRkHjW4pj>ha$jkz_JGXL{RvArNdkA?kYFtD{~pe zalID&)MuY0D@NQp{lQ4y0#b1+J1sND<2szA*=~LRr7?o0ld6?1;l;Is8!&h?dBg(q zqTY%vMED5Svftu?I|5}USr8qbh+t45BP&QvN`aQb=eRi;p_p>2NHYuY>JCD$B>)34fHps;!3D5QRiPz;7&`s|T94!bgocLJ}-3X!ms&+W0Ry0C;af;*BkwES=D#qvp%dQwtS z`%~k>xY$RNW)k~_)vg-#=G(~B`rXX|P2t#(th}o;A;;YL zSXT=oz7->pL*9N$IV1b%g+ zK)p~#p*qg?B)x~vwuj!&M+Y^52^fiqh>foEbDv^Ol15GEc^p#@TbkE0UtKaf99>jI7}hFK#b692~UGb8wS|5^(D%H42gPVQJ%!)%bfBwqt!&iGLZ zyvioUn@-SU^VT>$0PAl@!;o2dX_ED(`5FW2UNsF0>Mb=s!s)j zdi42q7wd&AeeKRaDGHbz8UOWdSO#x9=Mty56{1J`J6vLUx&rG$Z}bwj#njxy>7kUA zTp$d!pgBC`o4@^6mYiCOlvi)5vi>i-V4wX;X}~TgiB}*XWsZ6Y+^=7pot>#IQY1}# z(>0l4G9>+`0dGhh=kl$^^$26R{yk|^H(uQgxv;Q6BEgj?Jty@h(sX#-pL#5Gqdp;2DLI$dn<$hnP_Kn>X>~1*j5G<(2jYYSBSe0N z^=Yx6GGCnST0Cv7udgpJr!yVgv_B&{ZfThNY0@$m4Fp2g;X);K@2x%gT14tOlo%n~7s4*J0M6y|WV4%p~C z1`}=q{N^Ja#Gdt=uoOxqjE1n~8eE(BKS{u@P$XNg{^&D5kUi*pKd5T5qmatWWg*Gs zkpBMXB#*Yx`^e0S@>MZDUg;uJ-)GWgQ$93y1`WXtIqg8dIOGHoF;a12$QZUVs#~Wk zxm{c+*UsTri+ESZ1&u%k3(hrkV#m26G6!(p`b3Q=dStg5r%2LZR|-I0{4 z>S#+jq_-+XOf{zFE1qt5tvS{}!_%vgQPVn`&jsl0BV^eg zcJIMMH^*nkco0cZIgtt>ec%7|nTLjk{-@7;T$>y%pet+TVTfsM%B}oU8(orH4GkFH^(qttbxu*G&9=Eygr=DJclPvZ>u|g6 z+23SatB==D<(y2qbno_3Nz&UpngVam;b=CduWUU_(2$f%GR_kbtkS0FCS+;kR0B)l zFU+aifCy4*!Zb5yf@MrkAY#(ywRxoN<5V}8Hg;G=;3RGL-ZZFiT8`fr3=TRZy_p<8 z+nOOArP(6;P>vD{d$gF{lT{7-%n4en+NE&z_2ilLzFeOGW zHDJ=FV9d$01j=oLQc6Q0zLxXt7b}ag;1ui^XCAm74Bvq^3b&j5*wN>wEG<-(3B7i3 zycN`B5{TdH)-@&u)%dhWTret&)ZZ^G_R(WJSk(cfITY5R?(a(aMp zWJUEZzaIV)wLGt_a3(8H)%WhnfM)b`L)pGTQ~tzVQ5R6!gdaj$6n2h$0RaCAyg%CC8CGxwlr&T zqciwsglVY#fp}YKaw(!8p??c7ep~>2QoVJI9UQ8AQo8jjW7!-}=D4g=R6wYtVXXfS z^1v4|G9Eu#5=zS$%^{F$ub4rByTsx2iKVU(iEt9;h^_*C3JJH?+o6|$B}{${t>MA# zk_tJd5j|$eAfFW%x~$K(Z7T~bU`Z$mEM`_e>!6z=D$Voj%b(*05L%+^C2)M~+iE!W>4|QCd#W{aRJNC-=A(&SlK%k2URNNR!Z@cB zFZJx%1Hc&7hAY$!q{{BYQY9gq{zcU?zf0#mLAlaBqB^w)RB3#Brj%xcrP5kp5AqgWS$}>-0=U1RY=@h>8^hk>>d&E|8{vra5K`!qxB5 zTB|qy{KpMDG^5!loPRDS=?2KjA~i$(QL|=v?#QVCl&MlcsQ0f;$T++H6H;8v=I6D6 z2KG+$i&IRkW8DI)W@RqX^_B1SGo?^O1(**l_}5|Y=h(;5XLrD5BoW8pQZ|;tx_Ss0 zK0hzLdIR;=W~q0lEHh3_Hh2xOkDGJ&e7Xyf15{GnNLJc8Xn;Z$1Lt|hVhQSs4Dq(_ z+;QeJdw8E2O%Ried}J*82%e#-(|DhDB6;(454IfBx4mNW(3+Wc^OF_aY)={|o8T3< zv0rG)oVab~!;rwTIQYFK=$vr{R$xTyj z+cl8g{0U+f;`CbX_x6V83O-W|Iv6t&kgOZ!ngNmTMjsD8PR^9=Ei+BGhId`GoZMyv zLNQ&uWc)LU+g7ktON;7M(|eP@Zp@r&ecgur3z2PVDuL2X3}%PFtyWiegSfD`$-Fh< z6};C!=Lp6kkPz!c3rTU*E8faqm9&oQy)fz6g0Ori*`0%QO@d{UGb`K!rjRU7vFNf* z6~dxii9nN?5cGBWWW4L^<28mG>?-T%s8ziFBwqHpnw% zDvh8WR0p(G*C_iIXNMi{Tvm_gzo{QEvFx!}tJapr&Qy$GndY4I+Ab(KawT;XVHfpOjbwAUIA-l+t(vxS)!RT)TAp>YWo37qRCFSSyH=~u;j{A@Jde! zX_O8I*vE>gFTU0YlmEW2KmYoMl-_nxkyCS%DYVQ>u zx(kHH_m{j1@r{3st6|aHZqJpe7pb)~*-Bbb6r<3Ym(A-$D93CUCcnjLNWWLzEe7Nr z(A2Z4Oyz#de<;;wbzA)m=)JSrA=TRWExqRZ3ENMgz@ACoAC~mk_vC#S;pMkNR!k|G zA^Xw5#vT8ssrFaEjwh&+B*;D7io3xw+AI)|u-riYvFK6}07C!HIRiV`Dhd~kah5*T zhRM7_&}r9x3>;CE2df8X|T1p*@;+?ta!Lijep!@u3Z=u_Jp^uQ= zYvYjf0H}U1#d3Pi%R2684g5M2PI^- zz0l+CbuaUYN^9hOEr;Z@T%UMgziPbcd@Ij3Bm}rW%AV8ayjN`NKL~lkKEM^Ep`dZE z^@EdPKtOuMK|ucBy<3M$8CIGBP;;#STCcwkYlaqc?9e77umr(L7J=cegj_u9Bcu5{(V7Nbw#EQNpn(RARuv4V9 z1c>S)TQ)z&&e6U0csULW91Dt|UV!w6T?AwiUd&6P^4`9rjTbgz-)poTYu`MzF(|`z z$;B{88R_jiWboaU0OsKj<(YGnH;^pE&Q~FACK3iXwB|b6EM&yuc{u&?!Wf4Y3S+;% zCXAjQE!>ngxbRkf>pT4;siQqdPlcO700R!Wsk(duu71ew?I+#c`Y$o4{U51`^C)o`2Da`(ByhSYAx zPZ4F!1>sS6VSLk*At8p=RqQ%1+a+Okr73&Qit6Oyw8Dx0VR~^lR)~4+@6SElXNdgG zgX?^d#n4^}+EjDnvB!MrOM^4&<6NO|{=TMq^-1O!tX-fNBPnCZxLK5qccYp1F4Dln6Q+p zR%$Rg>qAQ`phQJcoIs*Z1w>V=`UrwPkK9B`Gsx3WkSLb+RHW6NQMDc>P zK<=|oBD=k_0qgvFkK)@NVT;^~Jo4@ZzIr?w6}y`_@?^V|-=29xus(JK`R*WxC~@3cyTq+PQdaQrs`vK-w%4G#xoH z+Ctj=#o%C!m;J(SgR-K}^*J#^z%h!$nvOIJ+;=S{&cUzz;r*FsKdYG0SnR;;u?(Ni zYVKHrXo@gh-yjsOFD8S7_-AezmFX1Wv`}r)(mo=2Jk{6AIs)rftkzw*-pV=w>vsQU zz;a>xJ5$vGf^7BNG8bqK?vm_XMSVigIYOa}a=VpvNtMTZm*p{XW0T6*K%RlHQ#svN zz{Qrs`q*Xpv&_p}o61})r$c9~@GJDSVoFYy!^yK^O0NpsQABgHS9Qa$9S8mHrW9BZ z+tZnmql=ow4yMZ#M-RqF zh%|UcT$yZ&Nt`8D{ZIcgf;F*@oVnS4raaYnceK|Wj6u^nWf;Q z6@#Q%Kbg}tX$*~4;M7gKbj-V>#*L@*;fhQ)s^Hju15YIQcIK*hqF(&+AriEaIKz5l zp3FZbmH0WG+;gUKdZp7R%QSv2!3HJwN4a)79&AUcRLjbclt>z)DZl2TNA*{5)O|lh zpinfl+xMurYV~<#7`&xTwG#k7f-lOmFMY{j;9Ooz4D)U^76P_y+NQo_B;aac=htwU z2yVcYr2GMWuq=OmbHl8;lqodE#ovuVI6W&7TG$mne*-6&$j!f{vL!_f?p%PCh#aUX z;l*?BmSwxc7;}x%VuN{*Xj++FD}aS|FNIK&i`z*jKPUahnqtObsi6kY$(=vm7L;C- zG`BpHg#FC+5mFCWZ&W{=DV8i};gmMA$Fn_Z-z{N^prVdE+pFldJ^*t{`jg#rC#dcs zL3m+)xV_swtlW=ql*)5Z*R1p=2k*5u&4FqLuBoD^Xg##uql#EM9x>xBYU^Z}Id$1k zxE})FLTQ#*;3K7W71ao6uOtdzL1nP2oZyx^&mkTpg$gQIO-pkXRjPWdv$OPGW>poL z2TsFpjD5Z-Cb6kRYbDapGQ!eJxW0nR#8LXf7uB_i-NiN%c^f7qK@E~j*FEQGwqS8Z z!3c)t$2Zb?&SV~MEX|*PB@NdJtWX)s8p|EnMnOTp_2a0mV3{ve(Fu|OS!#V$J==N zO%RNtwW76EvGohU&2fOFm$RI^Jg=~F&u%_JSn&&7*UCQa5L4yvT14T6xa!J=V{<@h zZh)5=#=Z2Y0a{*Lna~N|Cfy>?@n1vG=8p8FW^Q~bpzX6`3ZO#B8)6OJk5zk(!wvGFg zfkMwM*&#a9G}cAM_cGqcuitn_YFnc{88bpI7}AHf6|ORhwx=S;s%||$uH*<*X?T}b zOza>vLz}E8b@129U6YfEm+%o_+w5FZHmW@3%|&#s=p>m9V*L5iqY#z;`j<+Kt-o_8 zpdU06>;o34WO``o-pUh>KUzMfj7Q0fLY|ZqspK~Z3b;jZAmbb!LsBa+mBlq--FXeH zt)E6?YS6{=V4XSLHA-1zMZrgFz-URK^gXzG*g33Lw#(QA~pTS zqMyig3Z{_kEl=ePU)KU~}JZFR_JmhhP)nBd%f63PSYhx{=>A|iv?ol#fmRMaNJICDKP_JVC z0o@bK5v-$-f{S8dE9?+YqC_vnx+r~8Fdo`poluUpY+M(!!in`~>ll8Ry)tF3b3J#$ zn*e^L-*KFD-u~fF-OL4{J>xl2i;5|erx9=?@vstOlX~>j4eQHzw zfopNH?8$PhwlI*|VtRLK|Grce>13T-ZUyWd6>I-UvpiY;IbW@qW^tUlRtuG9QPje= zFkfHIdZD)9g1Mrz*0{v@F>`GFq9#?blec)7mmTHH2m4I*KZV|a_)+cNjAQX?57=vF z9_nL|ZLR+o$HsmJp3r`=)zldX2y%WOvX2SApnRRQZEgkEr5*U*b6jQM*dE=Zb>T3K z!XHmGXeZm>VmLiMsbNmU5dBGewm_$?ywa`P7ml8odCrVSp2%Lqr%#o8&(hYD0STY2 zcyeB6L&4FDbo?%Yb%A~OMEN+?WUsKr&8KB%V`h0<#nFp-{BE3efl?-M9oBi0c+^`J z#!0?*`h+ghsjP+7vwj*l+Rc5!sT~i&=pPJ5>Jt1>lpS(0X;S!@@<=_-R_F#c|I68o zqtCSu$AN@Sn=22gGdO`#+lr+65RYXACz?NKQg^K9cc3KZ=4%rLlRYZV4Sn{L$o5xR zcpOH1ePQtY;koEcJRGIo?=^mcYd*%VNHbL)(PGa;KPuN#=$)Z}&^3Ij-g5zLXRy#< zjTVsIyaSt|`9~|MX2ubGe`5|>471nscE*B7Wy_&afrt$*RvLLy4`HR#zMs4iGEwL>!dp+C6+`1noWYJKUpidYx)%7T(+egg zYO+WkjO1aY_vnm)0>QJv1*i;T$=2UHA&(u9gwjyR55KTx%M6g|h;&mp2IEy!e=zfc z|D-j*30x%#iTP6^D2!Q}!2e9EgT3uk6jL?k?Uo}5pOY1+X2rx)f03ORw0|eewm@b| zDT|ojVqCpdlv6ula`9W=11B+D5J9M+B2lx*`e)qx2Tig&kfdm1{47>t8~p{`{b#Ue zhIny8^pGpgvp26!72AUqs_2Tx^R^oiJwmbWLF8n_&`Dd13$YH1{Wtr_Z+4Mffj8eO zP29jM`z!Y9&Fz?{t(pTC9OQcv5K~KOIz|(SFswNg?vffB4+PdAfpoCv;LE;XsxZ$G zwXPuV0Lf`g;81MxaU)lRYW^(P-qJt5Z(t@Yr)`cf^tEObmqBwh*0NEG54}jlGs{|J ziB-a{a&A%+11y(jUHtqlOKn!STE$zHem~G|Cq<~LQ!ZewnVkb1rB^`#Yw)K+KveS$ zkB3;NE#701;botMUCr=UA82r;w~?`q$2B zgx5|3x(|8q@rZZAXTY}aqZQRZFb{Ed{)bG|#k~G4Gxw(1I#n!DB!?|xMhNu=S_raI zRos00m4$30WC(SWMa8KCj(w>C<>?xCpZw)dgzo{yTlXa?PQG>J4jEh2zb0*aJ;V;0 znI~=@0mzA);>4RjTO7DtlZQNcH>)_%2n{%I4!5HlX?nWmw1?T~-hHt;7X4m59&4yW zC3YFuuV5x_n@=f7l-K4no9z1s0OFC@{7Kbc*WL}ZsSkb%$VYxL9EwbaSvA_FGy&8x z`*fnRHw$_9dq{xI7d2AX%g{PGSsoQa7#|R zU>vT6ST@(R0kwEKA&(O(A!3F1z>j#7hIq3{4VvRzsLzj>DDd~E{T@2_|0_1tQNfP+ z^6t)!x@>}eFAN$3+J3C|yzIE(Sk(1(Blak0>3!7?GHW&D^GtjTqzBu+05sfK#^b`o zJHF}1aTELYbMdxDlW|ILLG(f63P|`4;3> zLq@=oSY_V>yC_Z22m0%=LA-QoG-M__O%CXVAT&squW~|bMta7 z`iuW`kNr9mNq#6|4hhlfj<5@ZR#$F);d#mtsX3|u6jG0e=JwxmeM*fOe&cm>nUIS& zVU}vOj6G3^Rv;(a5k-;*hXNap>>C6I{tLL@1*wYF zjg9@)#3;EhUpAByU$4xVT$MLe8AgKDe9M1s<0fRPrU(?yaW$)}XD3jn>PVS5Qk&c! z=$wcop9tc|`o#WFARI*g290pE=~nTPtRzPMQxg`e{St7L>w%eO;3F(S0yrJOWYaoI?vR}TG?+q-YRfF^f|=1)*iBT z&PDsrpgpD3Fogq8o-Z|?)X}p#SL3>RG_l?J$n z6KA%?4?~;VV+5!hX+Al?6hZ&CQUFj(pVw!>jXL;&di;ehG3#_LO`TfXOG=}S6?i?` zU06p|;(??jmm#=F>^nf$H@!i8eG=$9w!ajZ@&AIX#<3sENj+I>8lt#sE1TqC`!yBR z(IV56R@2TYvtzvr zGF_zI$*o$2G)~vo zJq+%ZF$FLVxtKOw&AI5y<&yL9QzNCz(9|4*3e#p{3t70KZ?ua{_biNUB|J~B);=Lw_$;GFx31zO+ObrW-7>NF^ zZxd}Lf`mZDMr+{JC3Q&eE4&u5afO_%6sV(7w?0R6lk?g`CbjxOXJ7MocQ+eBd<+Gq z;(lbhhD(*q-0|b4pE`)o-1BZotzK_1GVUUMjsZ$Zk!*W!^!_;pNI$?APva~K{A7{{ z8%oeWiVa(IkUnycX0Q(K;Hq@f)j5T0wK;@c=*$2Kzn5ocPT_|Eo9b8Sf%t)kfh>u( zsjW0mS>4#cMnjhDDFMrc%Q>7ss(M4}qhWY%COv*}W!eHgnMym?++TXwFL~`QcQwCX zBGvVQaf#SDZq!Bx$Y+-dVTF6$|HT#k4NBG-&WqD>t?|(Sbx#z{K!7r+Y^w?N8+jyT zV}=UAESq=U+hxkF94-I4Uh>P`sQMyx1iWB-E-Kl^-?Q6;oVw^3P1uA`y-k!vSI2Ic zEsH+VO4@*({kj4Nq-)m$0~Y537}9GR(i@GwbfA2q4sy@p1&c!H zBheGn*t;+kxZnWoWS)h0Dxa=Dj~sob*Es>0gX9=o%Y*85tPr@-4z)VP7qoh53=TIh za?Izv*yqQR=!wnfV=9S9T)7H*aQ$f>0s@bXf=85(Ol}0-l5nrn-FHTw#@aBBnvBKA zxwrF;(y!0A<11NU6=&Ylg4w+T?fgRQcGfvAU&DxB_S0;oEWZ0HT$t=F)9r*P!Gi#` zU6i^YHz0Li_J=lRiG20^^TraDlsO)`zCSAL&_Sh@TEQEG0-qhhg^Z zT7#0@#*`M<9j563p$`LTeKa`hmjjFiqy{77swdCCBQlEk021(M@m_Xg`1l`-ViX1|2OhXMvlt z_WOO_+c*hadxq6#jUO6fdL+TRpNjVZd~{BFZNCAuQqN-kJ3?=H(*Z3gGdN%)U;P70 z^CqQfg^y-i3BHBt@LY*8SnweYw#Amz!ai*=XNHURQfkon1{^8_)zT#-na^F9&|2yx z6NY7B07Q*wYSk=tBi4deq9zi1*8U^|a?E2vO_S>!#%c_K_~826!S{;(=I|It!HF-N zn7caB|8D^#$pw@TH4`29ro%bV|Kdza3uV>I>F-HqxO6CpwQ{;@@w$@vQ;&zSf(YZ` zM0C0>^oocgUVkBOErcg)i6V|h78%VUv<)3X`oD?}scotg?3LBgN}lJH3#lgPk3W3I zqHIL_J8YXwW}t5aZld3@(Nqny1lE9J8g7-y>HHGRQ225#TAjjJ^kD{)p`ysgLxZ_; zn2ocC6PS|9Mp3U4hAwBY?FI4QF(&Y(Q6d^}BFPm|X(af8P8u{l-i=O+br|?;qFkqR z(+1?7Bp)`F74kzQqJikxO2!f~#Q(cklI_BXq=_{yclr{@55#@HHXe#2mJj4v(%u_B z)xI>Y)#6!#cmKXS98D>l`9H;}?3Hml=>m{~=l+gO9>>IA6Zqo$<~t&=9P3?@Khv?2 z#0T>fD1;n8cO^Vf`}Q*$1olG`QTvFs)9<48FY)RjuF>jyc8~*&RD9516sHAWjx2H+ z)dWCwOaw`UXN?>E?!h4O>*fJO8mbdpt`b!)il(((m6zW5=PN1=FP@{=Xi$Y+ zB!0jCf#(KOd)D7EAC+&V}fR@{3kJ%9MMthR|EP(&1{w>B={6zmd^Ph2l(gAz9Af4|l4pDCi zJmvTFAv==)-{PPQ-fRGox|w8j338M=0dsglJ2t~)%O&`6Y$S%!IK;^V;ggrNL#i?L zgO#9jxnR0&f9?2fmhB{H%K8S&*yl-hQxZ4R15uW<6GC?WN zHt-=pB3*yjzPKB*i3Dn`JaEU8UkCdS9%T{n=FfC0hTYjx1A9t8DQFi!dwhw6Ld*Pe`;u|;+<6}~hw zlD@VOMr4_^NMY<-8Dnh2of%8X-Y8`$(iEkVeQlH^MSfWlLSzYLrm+)#GcBI#`JO-S zx%b`9=e+OpocBJ@dG9?y>k*>`oZV_(uIY9uFUG@o-WyV(%# zY~%7ir$6zzD?GK(W;H%_O8~OQZ6cwFx1^8XXHQ0AlZPU{1ku^!vSfbxp{Zvx^2xzx zY4MHZh2XJ_2BTpnlG;2yM6T-VKl=ef<%qcqv(MySTV<@X_tIpKnRU-^2LNG!^RKR0XMfY|CLgJi?ebHQUz>4*OQWbWW~aYOQTD z2nzPJWOC?vdibPya4R`c?RuxZXN#9Ei+!>>b#(fRrNxJr%@24aJH%Wlko#JUDs4@q5tiOkPtSSdao}VdJ{fc(9Ryy+j z@r?V;v}aCExJ}w^*xDk@({({)@px)h;LbeQ4WlFYG-&Mj%t3ms?N~bxFFxjcw54zR zR>4&yDyduFLEFToXIk6{jbi3MZ29X=m?_y?yX-|z7kO{qifS4H zn{B(6;9Dq-9kF^v%&Mp9uLf&I?r1`dm$pWvdEoiPV%^6Z>juI?vJ@~^-W0z0)LK{G zKQw1uNGX||8mJkR+u@#8Ix^;QClNLAW`tfFx;EhxqiqbadF9%UDM6;7x_gfAxae*} zM#Miy;nDqgoCA(rl+UQ>8nzfbPT-JcM+g}s8$jX!0B)|ocRQ1Da$s<#HZ*eZOlrf_ z_0FredWMB-`0NII6@%*Gaf95u4ke}>Hy1xZi;Pk4e-`tuF{SmoFwH4tw$)Q}S#$5X zwNRPiJPB7Rn|Ac1EP8a4Q1bGH!5&TyJtmyPqK1sc~VW?0*OD?onK>tz$ z|JlNfOjL_m6lYRM6cL0nV;S2u$*y|~zm^7LcKgn`Cc#2Il}S*WJbQ~vBSu&zso3lO zgQ+mTp6ZE78*K9Jm5V3mB9$-NF56k_I8j?{uin0Ur>v_ETWZgY^U|c2J!)`YzCTk& z>>3s47r$M;|D?XvQ6FVnS4^xEv%?$pwsIUJ^(-|$_lcbN8#Ay+^P-8OBn*bK(0#bd zI^V2*8{;%OiV#JBxf+5x`@1Ij8W!X;c(2{{>203qdA!&pe<2C>bx3Ntuh{8Q9qJXL zo%YT}HqlvwQPDXPf*W zj>uHDYJ@ndl!F;zxS+a znpcj{EXYs$0D&CIkvMhJfp|EuCD0Z^$UEEozKm4HyfE6~TsewezsqDdvooH+5N;m%AZej0#Rx?paxRKId7J7$2)6bpEsnpB{&-2 zuZy^;W@bz`UbX6^3J2MEe0z8`jP5y?U2rD7U?wBodGT?kk3`P6E1=K6=Ct-zyzw26 z>b@^JD&GCU`@)Ca3!=*p>&wPMZcjK|f7Q>`5=iiuMF_w6gysCU5D_N(O-1=GS*;uv zEPp2--k1jZ8DcX0^Q!Uqn_jX4%ugjj_v@7->Zy|fW~v#p@(lTVhb~AP>{9GQ6ZJj} zVn{I3yaI0bnAUr}5DS=#Ff=irr;^ww;Q0LYNsg_H)d}cp+ydCD0rlu1P}DWYZ}GGB zH-nG^_y|p-e~+zRsxP^$KB=P;f=Q~ZlRI6%%P<*a%qftA_z^W+=Uvv7zfKyq7*yBO z?9`^C)n;onw!i3K=hL1if>8bte2^Wg>*+@{w}~uX zQFK8uZVEX>oS>14xayhN3;=dWHJ`s|Te@4$H=U4ljcrSMinp^M4CAmc>64_FGug%2L3}x4nM4NqU&3y=MKoR|(w3 zLwzr3QkyMaLu@GN&{8nytrHbr)%UrZ-2cB5#LQO{mFq}sTufcTS2&32q9>i}!{dYN z5D69^>EJkGPc`LHtzKn!tn+!D;xNa#*PzxI-0M{ph~8Pj)!&{Kf?YvYJCr`GgEmFB zow%B-)+fD0yGNGPmC^Zf&9tzwU(mjl;XO9DXI`eOO5!b-`s|KSb!C5Ya=^5)C_-Vyj4w{VRKOBr|t<&%}<)RC1H`85zB zQ4kl`*KI%UedPh{^KsJP6hU&K+I9Gp$sVa2iD5|@ld9uhlG*o$UU*exKNtwenx~F( zFXTNMc=VvaIVgL|rm3pt=-hRrpVpvvNI8B)C6r<$q-!Q;H1MW+JQLFKQUH$^nv7*y zZa)NJYWk|wiC71;oC;-q5INyU%?$;+nu9KPC7?PsOSa^cEH8gNfIy5MO}PX!2|4&=`_^Q`ueMI3!OETqcC*q@Ved5r07Kp(YFy>@OIQe#&kG=H%n_pXH%L4#e@V}N`b9VVKyOoHBsdMoCDjkMISn~m!fbV6*?S#D> z_oQ%niY^%B{Nl4|Q3Me#cFD04LFao<58wNZuo$!S$M;35L3O}eV$0q$=G_&~!_@7t!X^Jx_4zh)Qe;W|^LulRu z05E6$ac=%NwGcmFYYPre5kLg^iNO5#B*k*TZ%ru*0Q}r=&i|$LyMH|(zbOI0`)B|N zbi?7i{5=ByOSN>tvz>>P6{A>L@lQ?9i>v{i{k`1X0&!|sSNFf5QgXbDLztpR7X9vcHtAdfC4eCFmux6Wk~2Sn za)tEIceyh}NzTy(iZuzo@hF){NeLID$dRBMmTcH08OjIZ<_)R1L<)hZp!mz*B}nm8 aa;)i_a07toABbs*6rre{yb8%|>i+?%BK?s7 From f3b81b432ab8cac7c8976262b5ced1775e9b37db Mon Sep 17 00:00:00 2001 From: jakory Date: Tue, 20 Sep 2016 11:44:21 -0400 Subject: [PATCH 15/30] Add unit test for database exceptions --- src/test_db_manager.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/test_db_manager.py b/src/test_db_manager.py index 490ae85..da2016c 100644 --- a/src/test_db_manager.py +++ b/src/test_db_manager.py @@ -35,7 +35,6 @@ def setUp(self): self.dbm = ss_db_manager("ss_test_no_participant_data.db") #self.dbm2 = ss_db_manager("ss_test_with_participant_data.db") #TODO add second database with participant data - #TODO add mocks to test exceptions for all the functions def test_get_most_recent_level(self): @@ -51,6 +50,7 @@ def test_get_most_recent_level(self): self.assertEqual(self.dbm.get_most_recent_level("", 2), None) self.assertEqual(self.dbm.get_most_recent_level("0xa7", 2), None) + def test_get_percent_correct_responses(self): # If there is no participant data, we should always get None. self.assertEqual(self.dbm.get_percent_correct_responses("p234", 2), @@ -70,6 +70,7 @@ def test_get_percent_correct_responses(self): self.assertEqual(self.dbm.get_percent_correct_responses("p234", 2, "ToM"), None) + def test_get_most_recent_incorrect_emotions(self): # If there is no participant data, we should get an empty list. self.assertEqual( @@ -87,6 +88,7 @@ def test_get_most_recent_incorrect_emotions(self): self.assertEqual( self.dbm.get_most_recent_incorrect_emotions("", 4), []) + def test_get_next_new_story(self): # If there is no participant data, we should get the name of an # unplayed story with the relevant emotions at that level. Some @@ -276,6 +278,7 @@ def test_get_graphics(self): ["story-fo1-B-a.png", "story-fo1-B-b.png", "story-fo1-B-c.png", "story-fo1-B-d.png"]) + def test_record_story_played(self): # Add a record to the stories_played table. # args: participant, session, level, story @@ -284,6 +287,7 @@ def test_record_story_played(self): # Reset database: remove all the data we added. pass + def test_record_response(self): # Add a record to the responses table. # args: participant, session, level, story, q_num, q_type, response @@ -292,3 +296,33 @@ def test_record_response(self): # Check that it was inserted correctly. # Reset database: remove all the data we added. pass + + + def test_exceptions(self): + # For each function, test that we handle exceptions properly. + m = Mock() + self.dbm._cursor = m + m.execute.side_effect = Exception + + with self.assertRaises(Exception): + self.dbm.get_most_recent_level("p023", 1) + + with self.assertRaises(Exception): + self.dbm.get_percent_correct_responses("p023", 1) + with self.assertRaises(Exception): + self.dbm.get_percent_correct_responses("p023", 1, "emotion") + + with self.assertRaises(Exception): + self.dbm.get_most_recent_incorrect_emotions("p023", 1) + + with self.assertRaises(Exception): + self.dbm.get_next_new_story("p391", 1, ["happy", "frustrated"], 1) + + with self.assertRaises(Exception): + self.dbm.get_next_review_story("40", 5, ["bored"], 8) + + with self.assertRaises(Exception): + self.dbm.get_level_info(2) + + with self.assertRaises(Exception): + self.dbm.get_graphics("story-ki2", 4) From b4eee68d95ba77110b46d650dbd508e93d7f0f91 Mon Sep 17 00:00:00 2001 From: jakory Date: Tue, 20 Sep 2016 12:38:54 -0400 Subject: [PATCH 16/30] Fix tests for db_manager to use assertIsNone --- src/test_db_manager.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/test_db_manager.py b/src/test_db_manager.py index da2016c..aa6c94b 100644 --- a/src/test_db_manager.py +++ b/src/test_db_manager.py @@ -53,22 +53,17 @@ def test_get_most_recent_level(self): def test_get_percent_correct_responses(self): # If there is no participant data, we should always get None. - self.assertEqual(self.dbm.get_percent_correct_responses("p234", 2), - None) - self.assertEqual(self.dbm.get_percent_correct_responses("93", 1), - None) - self.assertEqual(self.dbm.get_percent_correct_responses("93", 0.1), - None) - self.assertEqual(self.dbm.get_percent_correct_responses("1", 0), - None) - self.assertEqual(self.dbm.get_percent_correct_responses("", 0), - None) - self.assertEqual(self.dbm.get_percent_correct_responses("p234", 2, - "order"), None) - self.assertEqual(self.dbm.get_percent_correct_responses("p234", 2, - "emotion"), None) - self.assertEqual(self.dbm.get_percent_correct_responses("p234", 2, - "ToM"), None) + self.assertIsNone(self.dbm.get_percent_correct_responses("p234", 2)) + self.assertIsNone(self.dbm.get_percent_correct_responses("93", 1)) + self.assertIsNone(self.dbm.get_percent_correct_responses("93", 0.1)) + self.assertIsNone(self.dbm.get_percent_correct_responses("1", 0)) + self.assertIsNone(self.dbm.get_percent_correct_responses("", 0)) + self.assertIsNone(self.dbm.get_percent_correct_responses("p234", 2, + "order")) + self.assertIsNone(self.dbm.get_percent_correct_responses("p234", 2, + "emotion")) + self.assertIsNone(self.dbm.get_percent_correct_responses("p234", 2, + "ToM")) def test_get_most_recent_incorrect_emotions(self): From a3b454eb99ff0f910328f10dd06e8a029c6ac6d4 Mon Sep 17 00:00:00 2001 From: jakory Date: Tue, 20 Sep 2016 12:39:51 -0400 Subject: [PATCH 17/30] Add first tests for personalization manager --- src/ss_personalization_manager.py | 17 +-- src/test_personalization_manager.py | 186 ++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 src/test_personalization_manager.py diff --git a/src/ss_personalization_manager.py b/src/ss_personalization_manager.py index 84be9c9..69854a2 100644 --- a/src/ss_personalization_manager.py +++ b/src/ss_personalization_manager.py @@ -91,6 +91,7 @@ def get_level_for_session(self): # If there is no previous data, start at level 1. if (level is None): return 1 + # If participant got 75%-80% questions correct last time, level # up. If no responses were found or not enough were answered # correctly, do not level up. @@ -102,17 +103,17 @@ def get_level_for_session(self): + "time, so we will not level up. Level will be " + str(level) + ".") return level - elif (past_performance > percent_correct_to_level): + elif (past_performance >= self._percent_correct_to_level): self._logger.info("Participant got more than " + - (percent_correct_to_level*100) + "% questions correct last " - + "time, so we can level up! Level will be " + str(level+1) - + ".") - return level + 1 + str(self._percent_correct_to_level*100) + + "% questions correct last time, so we can level up! Level will" + " be " + str(level+1) + ".") + return level + 1 if level < 10 else level else: self._logger.info("Participant got less than " + - (percent_correct_to_level*100) + "% questions correct last " - + "time, so we don't level up. Level will be " + str(level) - + ".") + str(self._percent_correct_to_level*100) + + "% questions correct last time, so we don't level up. Level" + " will be " + str(level) + ".") return level diff --git a/src/test_personalization_manager.py b/src/test_personalization_manager.py new file mode 100644 index 0000000..fba7842 --- /dev/null +++ b/src/test_personalization_manager.py @@ -0,0 +1,186 @@ +# Jacqueline Kory Westlund +# September 2016 +# +# The MIT License (MIT) +# +# Copyright (c) 2016 Personal Robots Group +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import unittest +import mock +import random +from mock import Mock +from ss_personalization_manager import ss_personalization_manager + +class test_personalization_manager(unittest.TestCase): + # TODO test for different participants, sessions, including DEMO! + + def setup_demo(self): + # Set up to test a demo session, with no participant data in + # the database yet. + #args: session, participant, database, percent_correct_to_level + self.pm = ss_personalization_manager(-1, "DEMO", + "ss_test_no_participant_data.db", 0.75) + + # Mock the database manager for a participant who has no data + # in the database yet. + m = Mock() + self.pm._db_man = m + m.get_most_recent_level.return_value = None + m.get_percent_correct_responses.return_value = None + m.get_most_recent_incorrect_emotions.return_value = [] + m.get_next_new_story.return_value = "story-fo1" + m.get_next_review_story.return_value = None + m.get_level_info.return_value = (3, 1) + m.get_graphics.return_value = ["story-fo1-P-a.png"] + + + def setup_no_participant_data(self, participant, session): + # Set up test for a participant on their first session, with no + # participant data in the database yet. + + #args: session, participant, database, percent_correct_to_level + self.pm = ss_personalization_manager(session, participant, + "ss_test_no_participant_data.db", 0.75) + + # Mock the database manager for a participant who has no data + # in the database yet. + m = Mock() + self.pm._db_man = m + m.get_most_recent_level.return_value = None + m.get_percent_correct_responses.return_value = None + m.get_most_recent_incorrect_emotions.return_value = [] + m.get_next_new_story.return_value = "story-fo1" + m.get_next_review_story.return_value = None + m.get_level_info.return_value = (3, 1) + m.get_graphics.return_value = ["story-fo1-P-a.png"] + return m + + + def test_get_level_for_session(self): + # Test demo session. + self.setup_demo() + self.assertEqual(self.pm.get_level_for_session(), 1) + + # Test a participant with no data on their first session. + dbm = self.setup_no_participant_data("P001", 1) + + # Mock past play and performance data so we can test different + # values. + + # Last level = exception + #dbm.get_most_recent_level.return_value = Exception + #with self.assertRaises(Exception): + #self.pm.get_level_for_session() + # TODO add exception handling to get_level_for_session? + + # Last level = None (never played before) + dbm.get_most_recent_level.return_value = None + self.assertEqual(self.pm.get_level_for_session(), 1) + + # Last level = 1 + dbm.get_most_recent_level.return_value = 1 + # No questions answered, play at level 1. + dbm.get_percent_correct_responses.return_value = None + self.assertEqual(self.pm.get_level_for_session(), 1) + # All questions correct, play at level 2. + dbm.get_percent_correct_responses.return_value = 1 + self.assertEqual(self.pm.get_level_for_session(), 2) + # 75% questions correct, play at level 2. + dbm.get_percent_correct_responses.return_value = 0.75 + self.assertEqual(self.pm.get_level_for_session(), 2) + # 74% questions correct, play at level 1. + dbm.get_percent_correct_responses.return_value = 0.74 + self.assertEqual(self.pm.get_level_for_session(), 1) + # -3% questions correct, play at # level 1. + dbm.get_percent_correct_responses.return_value = -0.03 + self.assertEqual(self.pm.get_level_for_session(), 1) + + # Last level = 4 + dbm.get_most_recent_level.return_value = 4 + # No questions answered ever, play at level 1. + dbm.get_percent_correct_responses.return_value = None + self.assertEqual(self.pm.get_level_for_session(), 4) + # All questions correct, play at level 5. + dbm.get_percent_correct_responses.return_value = 1 + self.assertEqual(self.pm.get_level_for_session(), 5) + # 75% questions correct, play at level 5. + dbm.get_percent_correct_responses.return_value = 0.75 + self.assertEqual(self.pm.get_level_for_session(), 5) + # 74% questions correct, play at level 4. + dbm.get_percent_correct_responses.return_value = 0.74 + self.assertEqual(self.pm.get_level_for_session(), 4) + # -3% questions correct, play at # level 4. + dbm.get_percent_correct_responses.return_value = -0.03 + self.assertEqual(self.pm.get_level_for_session(), 4) + + # Last level = 10 + dbm.get_most_recent_level.return_value = 10 + # No questions answered ever, play at level 10. + dbm.get_percent_correct_responses.return_value = None + self.assertEqual(self.pm.get_level_for_session(), 10) + # All questions correct, play at level 10. + dbm.get_percent_correct_responses.return_value = 1 + self.assertEqual(self.pm.get_level_for_session(), 10) + # 75% questions correct, play at level 10. + dbm.get_percent_correct_responses.return_value = 0.75 + self.assertEqual(self.pm.get_level_for_session(), 10) + # 74% questions correct, play at level 10. + dbm.get_percent_correct_responses.return_value = 0.74 + self.assertEqual(self.pm.get_level_for_session(), 10) + # -3% questions correct, play at # level 10. + dbm.get_percent_correct_responses.return_value = -0.03 + self.assertEqual(self.pm.get_level_for_session(), 10) + + + + def test_get_performance_this_session(self): + pass + + + def test_get_performance_this_session(self): + pass + + + def test_get_next_story_script(self): + pass + + + def test_get_next_story_details(self): + pass + + + def test_record_story_loaded(self): + pass + + + def test_record_user_response(self): + pass + + + def test_set_start_level(self): + pass + + + def test_get_joint_attention_level(self): + pass + + + From bbf59d0a4b8668afe995f803f25bb826d4ef9c8b Mon Sep 17 00:00:00 2001 From: jakory Date: Sun, 2 Oct 2016 11:28:44 -0400 Subject: [PATCH 18/30] Send GameState.READY msg when ready to start game --- src/ss_game_node.py | 3 +++ src/ss_ros.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/ss_game_node.py b/src/ss_game_node.py index c3bf096..e5881e7 100755 --- a/src/ss_game_node.py +++ b/src/ss_game_node.py @@ -211,6 +211,9 @@ def launch_game(self, session, participant): # Set up signal handler to catch SIGINT (e.g., ctrl-c). signal.signal(signal.SIGINT, self._signal_handler) + # Ready to start the game. Send a "READY" message. + self._ros_ss.send_game_state("READY") + while (not self._stop): try: try: diff --git a/src/ss_ros.py b/src/ss_ros.py index 92b9ff3..7212e9e 100644 --- a/src/ss_ros.py +++ b/src/ss_ros.py @@ -249,6 +249,8 @@ def send_game_state(self, state, performance=None): msg.state = GameState.PAUSED if "TIMEOUT" in state: msg.state = GameState.USER_TIMEOUT + if "READY" in state: + msg.state = GameState.READY if "END" in state: msg.state = GameState.END if performance is not None: From be36a8bf9abeff5328ab879c2e39091ecbcdf342 Mon Sep 17 00:00:00 2001 From: jakory Date: Mon, 3 Oct 2016 13:34:48 -0400 Subject: [PATCH 19/30] Fetch one matching row from query results, not all The database access functions generally just need to return one result, not all the results, so for all the functions that need just one, we ensure they use the "fetchone" function instead of "fetchall". --- src/ss_db_manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ss_db_manager.py b/src/ss_db_manager.py index 26fd422..289b81e 100644 --- a/src/ss_db_manager.py +++ b/src/ss_db_manager.py @@ -240,7 +240,7 @@ def get_next_new_story(self, participant, current_session, emotions, AND questions.level = (?) ORDER BY stories.id LIMIT 1 - """ % ",".join("?"*len(emotions)), params).fetchall() + """ % ",".join("?"*len(emotions)), params).fetchone() if result is None or result == []: self._logger.warn("Could not find any unplayed stories for " @@ -263,7 +263,7 @@ def get_next_new_story(self, participant, current_session, emotions, AND stories_played.session = (?)) ORDER BY stories.id LIMIT 1 - """ , (participant, current_session)).fetchall() + """ , (participant, current_session)).fetchone() if result is None or result == []: self._logger.warn("Could not find unplayed stories for " @@ -275,8 +275,8 @@ def get_next_new_story(self, participant, current_session, emotions, # or didn't, and found an unplayed story without them. # Return the name of a new story to play. The DB gives # us the name of the story in a tuple. - self._logger.info("Found a story to play: " + str(result[0][0])) - return result[0][0] + self._logger.info("Found a story to play: " + str(result[0])) + return result[0] except Exception as e: self._logger.exception("Failed when trying to find unplayed " From 35ab95520190ed81da209a8b3518ed18a76c9b86 Mon Sep 17 00:00:00 2001 From: jakory Date: Mon, 3 Oct 2016 13:46:26 -0400 Subject: [PATCH 20/30] Add comments regarding whether tables can be empty --- src/ss_db_manager.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/ss_db_manager.py b/src/ss_db_manager.py index 289b81e..f2ad97e 100644 --- a/src/ss_db_manager.py +++ b/src/ss_db_manager.py @@ -95,6 +95,9 @@ def get_percent_correct_responses(self, participant, session, # Get the number of correct responses (i.e., the questions # from the participant's last session where their response # was equal to the target response). + # The responses table can be empty if no responses from the + # participant have been recorded yet. The questions table + # should never be empty (filled when stories are imported). total_correct = self._cursor.execute(""" SELECT COUNT(responses.response) FROM responses @@ -116,6 +119,8 @@ def get_percent_correct_responses(self, participant, session, (participant, session, question_type)) ).fetchone() + # The participant may not have responded to any questions + # correctly. if total_correct is None: self._logger.warn("Could not find any correct responses for " + participant + " for session " + str(session) @@ -144,6 +149,7 @@ def get_percent_correct_responses(self, participant, session, (participant, session, question_type)) ).fetchone() + # The participant may not have responded to any questions. if total_responses is None or total_responses[0] == 0: self._logger.warn("Could not find any responses for " + participant + " for session " + str(session) @@ -169,6 +175,8 @@ def get_most_recent_incorrect_emotions(self, participant, current_session): """ # May not be able to use ORDER BY in the subquery - if this is # a problem, fix later. + # The responses table may be empty if no responses have been + # recorded yet. try: result = self._cursor.execute(""" SELECT DISTINCT responses.response, questions.target_response @@ -183,6 +191,8 @@ def get_most_recent_incorrect_emotions(self, participant, current_session): AND session = (?) ORDER BY time DESC) """, (participant, current_session)).fetchall() + # The user may not have responded incorrectly to any + # questions, in which case we get no results from the query. if result is None or result == []: self._logger.warn("Could not find any incorrect responses for " + participant + " for session " + str(current_session-1) @@ -223,6 +233,8 @@ def get_next_new_story(self, participant, current_session, emotions, params.append(current_session) params.append(level) + # The stories and questions tables should not be empty. + # The stories_played table may be empty. result = self._cursor.execute(""" SELECT DISTINCT stories.story_name FROM stories @@ -312,6 +324,8 @@ def get_next_review_story(self, participant, current_session, emotions, # This gives us a randomly picked story from a list of # stories played not this session with at least one of the # desired emotions. + # The stories and questions tables should not be empty. + # The stories_played table may be empty. result = self._cursor.execute(""" SELECT DISTINCT stories.story_name FROM stories From b9938c005cb54853ae8c79ad45501e5d333f9bf9 Mon Sep 17 00:00:00 2001 From: jakory Date: Mon, 3 Oct 2016 16:03:22 -0400 Subject: [PATCH 21/30] Fix pick next story script so one is always picked In the scripts, the opal command LOAD_STORY was called before the STORY command, which meant that the personalization manager had not necessarily selected the next story when we wanted to load the story. We have updated how the scripts are written to include a STORY SETUP command that will just pick the next story, and added checks when getting the story script or story details to ensure that a story has been selected. --- README.md | 9 +++++++ game_scripts/session_scripts/demo-story.txt | 1 + src/ss_personalization_manager.py | 30 ++++++++++++++++----- src/ss_script_handler.py | 12 +++++++-- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7a89728..bff3ab8 100644 --- a/README.md +++ b/README.md @@ -326,6 +326,15 @@ will look like this: `STORY` +That said: Depending on your script, you may want to specify that a new story +should be selected before attempting to load the story or play back the story. +A `STORY` line may optionally take a string argument "SETUP", which indicates +that the next story should be selected. You will then need to use the usual +script lines for loading and playing back the story. + +`STORY SETUP` + + ### Story scripts The story scripts follow the same format as the main session scripts. See the diff --git a/game_scripts/session_scripts/demo-story.txt b/game_scripts/session_scripts/demo-story.txt index 408bd10..8d9c73d 100644 --- a/game_scripts/session_scripts/demo-story.txt +++ b/game_scripts/session_scripts/demo-story.txt @@ -5,6 +5,7 @@ ROBOT DO "Touch start when you are ready to hear the story!" WAIT START 300 OPAL CLEAR PAUSE 1 +STORY SETUP OPAL LOAD_STORY ROBOT STORY_INTRO ROBOT DO "Let's look at the story together." diff --git a/src/ss_personalization_manager.py b/src/ss_personalization_manager.py index 69854a2..073cd55 100644 --- a/src/ss_personalization_manager.py +++ b/src/ss_personalization_manager.py @@ -119,7 +119,7 @@ def get_level_for_session(self): def get_performance_this_session(self): """ Get the user's performance on all questions asked this - session, by question type, and format as a json object. + session, by question type. """ # Only get the user's performance if this isn't a DEMO session. if (self._session != -1): @@ -136,6 +136,23 @@ def get_performance_this_session(self): def get_next_story_script(self): + """ Return the name of the next story script to load. """ + # If this is a demo session, use the demo script. + if (self._session == -1): + return "demo-story-1.txt" + + # If no story has been picked yet, print error and pick a story. + elif (self._current_story is None): + self._logger.error("We were asked for the story script, but we " + "haven't picked a story yet! Picking a story...") + self.pick_next_story() + + # Return name of story script: story name + level + file + # extension. + return (self._current_story + "-" + str(self._level) + ".txt").lower() + + + def pick_next_story(self): """ Determine which story should be heard next. We have 40 stories. Alternate telling new stories and telling review stories. Earlier sessions will use more new stories since there @@ -147,6 +164,8 @@ def get_next_story_script(self): # If this is a demo session, use the demo script. if (self._session == -1): self._logger.debug("Using DEMO script.") + # Save that we are using the demo story. + self._current_story = "demo-story-1" return "demo-story-1.txt" # If we should tell a new story, get the next new story that @@ -189,7 +208,7 @@ def get_next_story_script(self): # Save current story so we can provide story details later. self._current_story = story - # Return name of story script: story name + level + file extension. + # Return name of script: story name + level + file extension. return (story + "-" + str(self._level) + ".txt").lower() @@ -213,12 +232,11 @@ def get_next_story_details(self): + "\nIn order: " + str(in_order) + "\nNum answers: " + str(num_answers)) - # If the current story isn't set, throw exception. + # If the current story isn't set, print error, and pick a story. elif (self._current_story is None): self._logger.error("We were asked for story details, but we \ - haven't picked the next story yet!") - raise NoStoryFound("No current story is set.", self._participant, - self._session) + haven't picked a story yet! Picking a story...") + self.pick_next_story() # Otherwise, we have the current story. else: diff --git a/src/ss_script_handler.py b/src/ss_script_handler.py index c4353ff..de8665e 100644 --- a/src/ss_script_handler.py +++ b/src/ss_script_handler.py @@ -257,7 +257,7 @@ def iterate_once(self): # Do different stuff depending on what the first element is. ######################################################### - # only STORY lines have only one part to the command. + # Some STORY lines have only one part to the command. elif len(elements) == 1: # For STORY lines, play back the next story for this # participant. @@ -266,7 +266,7 @@ def iterate_once(self): # If line indicates we need to start a story, do so. self._doing_story = True # Create a script parser for the filename provided, - # assume it is in the session_scripts directory. + # assuming it is in the story scripts directory. self._story_parser = ss_script_parser() try: self._story_parser.load_script(self._script_path @@ -290,6 +290,14 @@ def iterate_once(self): self._doing_story = False # Line has 2+ elements, so check the other commands. + ######################################################### + # For STORY SETUP lines, pick the next story to play so + # we can load its graphics and play back the story. + if "STORY" in elements[0] and "SETUP" in elements[1]: + self._logger.debug("STORY SETUP") + # Pick the next story to play. + self._personalization_man.pick_next_story() + ######################################################### # For ROBOT lines, send command to the robot. elif "ROBOT" in elements[0]: From 70fe1cd6ee32c893c0f473836cfc8dc6c2547e2c Mon Sep 17 00:00:00 2001 From: jakory Date: Mon, 3 Oct 2016 16:54:19 -0400 Subject: [PATCH 22/30] Add additional personalization manager tests --- src/test_personalization_manager.py | 140 +++++++++++++++++----------- 1 file changed, 83 insertions(+), 57 deletions(-) diff --git a/src/test_personalization_manager.py b/src/test_personalization_manager.py index fba7842..b081be1 100644 --- a/src/test_personalization_manager.py +++ b/src/test_personalization_manager.py @@ -28,6 +28,7 @@ import random from mock import Mock from ss_personalization_manager import ss_personalization_manager +from SS_Errors import NoStoryFound class test_personalization_manager(unittest.TestCase): # TODO test for different participants, sessions, including DEMO! @@ -95,74 +96,99 @@ def test_get_level_for_session(self): dbm.get_most_recent_level.return_value = None self.assertEqual(self.pm.get_level_for_session(), 1) - # Last level = 1 - dbm.get_most_recent_level.return_value = 1 - # No questions answered, play at level 1. - dbm.get_percent_correct_responses.return_value = None - self.assertEqual(self.pm.get_level_for_session(), 1) - # All questions correct, play at level 2. - dbm.get_percent_correct_responses.return_value = 1 - self.assertEqual(self.pm.get_level_for_session(), 2) - # 75% questions correct, play at level 2. - dbm.get_percent_correct_responses.return_value = 0.75 - self.assertEqual(self.pm.get_level_for_session(), 2) - # 74% questions correct, play at level 1. - dbm.get_percent_correct_responses.return_value = 0.74 - self.assertEqual(self.pm.get_level_for_session(), 1) - # -3% questions correct, play at # level 1. - dbm.get_percent_correct_responses.return_value = -0.03 - self.assertEqual(self.pm.get_level_for_session(), 1) - - # Last level = 4 - dbm.get_most_recent_level.return_value = 4 - # No questions answered ever, play at level 1. - dbm.get_percent_correct_responses.return_value = None - self.assertEqual(self.pm.get_level_for_session(), 4) - # All questions correct, play at level 5. - dbm.get_percent_correct_responses.return_value = 1 - self.assertEqual(self.pm.get_level_for_session(), 5) - # 75% questions correct, play at level 5. - dbm.get_percent_correct_responses.return_value = 0.75 - self.assertEqual(self.pm.get_level_for_session(), 5) - # 74% questions correct, play at level 4. - dbm.get_percent_correct_responses.return_value = 0.74 - self.assertEqual(self.pm.get_level_for_session(), 4) - # -3% questions correct, play at # level 4. - dbm.get_percent_correct_responses.return_value = -0.03 - self.assertEqual(self.pm.get_level_for_session(), 4) - - # Last level = 10 - dbm.get_most_recent_level.return_value = 10 - # No questions answered ever, play at level 10. - dbm.get_percent_correct_responses.return_value = None - self.assertEqual(self.pm.get_level_for_session(), 10) - # All questions correct, play at level 10. - dbm.get_percent_correct_responses.return_value = 1 - self.assertEqual(self.pm.get_level_for_session(), 10) - # 75% questions correct, play at level 10. - dbm.get_percent_correct_responses.return_value = 0.75 - self.assertEqual(self.pm.get_level_for_session(), 10) - # 74% questions correct, play at level 10. - dbm.get_percent_correct_responses.return_value = 0.74 - self.assertEqual(self.pm.get_level_for_session(), 10) - # -3% questions correct, play at # level 10. - dbm.get_percent_correct_responses.return_value = -0.03 - self.assertEqual(self.pm.get_level_for_session(), 10) - + # Last level played was... + last_level = [1,4,10] + # Percent questions correct were... None = no questions + # answered, 1 = all questions correct, 0.75 = 75% correct, etc. + percent_questions_correct = [None, 1, 0.75, 0.74, -0.03] + # Then we expect to play at level... + expected_level = [ + (1, 2, 2, 1, 1), + (4, 5, 5, 4, 4), + (10, 10, 10, 10, 10) + ] + + # Test at each level. + for i in range(0, len(last_level)): + dbm.get_most_recent_level.return_value = last_level[i] + + # Test for each percentage of questions correct. + for j in range(0, len(percent_questions_correct)): + dbm.get_percent_correct_responses.return_value = \ + percent_questions_correct[j] + self.assertEqual(self.pm.get_level_for_session(), + expected_level[i][j]) def test_get_performance_this_session(self): - pass + # Test demo session. + self.setup_demo() + self.assertEqual(self.pm.get_performance_this_session(), None) + # Test a participant with no data on their first session. + dbm = self.setup_no_participant_data("P001", 1) - def test_get_performance_this_session(self): - pass + # Mock past play and performance data so we can test different + # values. Returns (emotion, ToM, order) performance. + performance_data = [ + # No questions answered. + (None, None, None), + # All correct. + (1, 1, 1), + # All incorrect. + (0, 0, 0), + # Some correct. + (0.5, 0.75, 0.5), + # Didn't answer some, answered others. + (None, 0, 0), + (0, None, 0), + (0, 0, None), + (1, None, None), + (None, None, 1), + (None, 1, None), + (0.4, -1, 5), + ] + + for pd in performance_data: + dbm.get_percent_correct_responses.side_effect = [ pd[0], pd[1], + pd[2] ] + self.assertEqual(self.pm.get_performance_this_session(), pd) def test_get_next_story_script(self): pass + def test_pick_next_story(self): + # Test demo session. + self.setup_demo() + self.assertEqual(self.pm.pick_next_story(), "demo-story-1.txt") + + # Test a participant with no data on their first session. + dbm = self.setup_no_participant_data("P001", 1) + + # Tell new story starts out True, toggles after each call to + # pick_new_story. + self.assertTrue(self.pm._tell_new_story) + + # Mock relevant story data. + dbm.get_next_new_story.side_effect = [ None, None ] + dbm.get_next_review_story.side_effect = [ None ] + with self.assertRaises(NoStoryFound): + self.pm.pick_next_story() + + # Mock relevant story data. + dbm.get_next_new_story.side_effect = [ None, None ] + dbm.get_next_review_story.side_effect = [ "story-cf1" ] + self.assertEquals(self.pm.pick_next_story(), "story-cf1-1.txt") + + # Tell new story starts out True, toggles after each call to + # pick_new_story. + self.assertFalse(self.pm._tell_new_story) + + #TODO ADD MORE + + def test_get_next_story_details(self): pass From f53dbe242759e9750d8d32e1707c774e6187c697 Mon Sep 17 00:00:00 2001 From: jakory Date: Mon, 3 Oct 2016 20:37:51 -0400 Subject: [PATCH 23/30] Pick new stories only from never played stories Remove session as a parameter, since a new story shouldn't have been played in any prior session, either. --- src/ss_db_manager.py | 25 +++++--------- src/ss_personalization_manager.py | 4 +-- src/test_db_manager.py | 56 +++++++++++++++---------------- 3 files changed, 39 insertions(+), 46 deletions(-) diff --git a/src/ss_db_manager.py b/src/ss_db_manager.py index f2ad97e..e6fcf96 100644 --- a/src/ss_db_manager.py +++ b/src/ss_db_manager.py @@ -210,8 +210,7 @@ def get_most_recent_incorrect_emotions(self, participant, current_session): raise - def get_next_new_story(self, participant, current_session, emotions, - level): + def get_next_new_story(self, participant, emotions, level): """ Get the next unplayed story for the desired level from the story table with at least one of the listed emotions present in the story. If no unplayed story has the desired emotions or if @@ -230,7 +229,6 @@ def get_next_new_story(self, participant, current_session, emotions, # parameters. params = list(emotions) params.append(participant) - params.append(current_session) params.append(level) # The stories and questions tables should not be empty. @@ -247,8 +245,7 @@ def get_next_new_story(self, participant, current_session, emotions, NOT IN ( SELECT stories_played.story_id FROM stories_played - WHERE stories_played.participant = (?) - AND stories_played.session = (?)) + WHERE stories_played.participant = (?)) AND questions.level = (?) ORDER BY stories.id LIMIT 1 @@ -256,9 +253,8 @@ def get_next_new_story(self, participant, current_session, emotions, if result is None or result == []: self._logger.warn("Could not find any unplayed stories for " - + participant + " for session " + str(current_session) - + " with emotions " + str(emotions) + " in the database!" + - " Will try to find any unplayed story...") + + participant + " with emotions " + str(emotions) + + " in the database! Will try to find any unplayed story...") # Query again, but look for any unplayed stories, not # just unplayed stories with particular emotions. @@ -271,16 +267,14 @@ def get_next_new_story(self, participant, current_session, emotions, NOT IN ( SELECT stories_played.story_id FROM stories_played - WHERE stories_played.participant = (?) - AND stories_played.session = (?)) + WHERE stories_played.participant = (?)) ORDER BY stories.id LIMIT 1 - """ , (participant, current_session)).fetchone() + """ , (participant,)).fetchone() if result is None or result == []: self._logger.warn("Could not find unplayed stories for " - + participant + " for session " + str(current_session) - + " in the database!") + + participant + " in the database!") return None # We either found an unplayed story with the right emotions @@ -292,9 +286,8 @@ def get_next_new_story(self, participant, current_session, emotions, except Exception as e: self._logger.exception("Failed when trying to find unplayed " - "stories for " + participant + " for session " + - str(current_session) + " with emotions " + str(emotions) + - " in the database!") + "stories for " + participant + " with emotions " + + str(emotions) + " in the database!") # Pass on exception for now. raise diff --git a/src/ss_personalization_manager.py b/src/ss_personalization_manager.py index 073cd55..4adaac6 100644 --- a/src/ss_personalization_manager.py +++ b/src/ss_personalization_manager.py @@ -174,7 +174,7 @@ def pick_next_story(self): # story. elif self._tell_new_story: story = self._db_man.get_next_new_story(self._participant, - self._session, self._emotion_list, self._level) + self._emotion_list, self._level) # If there are no more new stories to tell, or if we need to # tell a review story next, get a review story that has one of @@ -189,7 +189,7 @@ def pick_next_story(self): # story but haven't told very many stories yet). if (story is None): story = self._db_man.get_next_new_story(self._participant, - self._session, self._emotion_list, self._level) + self._emotion_list, self._level) # If we still don't have a story, then for some reason there # are no new stories and no review stories we can tell. This is diff --git a/src/test_db_manager.py b/src/test_db_manager.py index aa6c94b..a60926b 100644 --- a/src/test_db_manager.py +++ b/src/test_db_manager.py @@ -100,60 +100,60 @@ def test_get_next_new_story(self): # that would not test whether the story included the correct # emotions for the story's level, since not all stories have # the same emotions present at every level. - self.assertEqual(self.dbm.get_next_new_story("p391", 1, ["angry"], 1), + self.assertEqual(self.dbm.get_next_new_story("p391", ["angry"], 1), "story-fo1") - self.assertEqual(self.dbm.get_next_new_story("", 2, ["angry"], 10), + self.assertEqual(self.dbm.get_next_new_story("", ["angry"], 10), "story-fo1") - self.assertEqual(self.dbm.get_next_new_story("", 2, ["angry"], 22), + self.assertEqual(self.dbm.get_next_new_story("", ["angry"], 22), "story-fo1") - self.assertEqual(self.dbm.get_next_new_story("p391", 1, ["sad"], 2), + self.assertEqual(self.dbm.get_next_new_story("p391", ["sad"], 2), "story-cr1") - self.assertEqual(self.dbm.get_next_new_story("33", 10, ["sad"], 10), + self.assertEqual(self.dbm.get_next_new_story("33", ["sad"], 10), "story-fo1") - self.assertEqual(self.dbm.get_next_new_story("p391", 1, ["happy"], 3), + self.assertEqual(self.dbm.get_next_new_story("p391", ["happy"], 3), "story-am1") - self.assertEqual(self.dbm.get_next_new_story("33", 2, ["happy"], 8), + self.assertEqual(self.dbm.get_next_new_story("33", ["happy"], 8), "story-fo1") - self.assertEqual(self.dbm.get_next_new_story("3811", 13, ["nervous"], + self.assertEqual(self.dbm.get_next_new_story("3811", ["nervous"], 8), "story-am2") - self.assertEqual(self.dbm.get_next_new_story("0x7a", -0.1, ["nervous"], + self.assertEqual(self.dbm.get_next_new_story("0x7a", ["nervous"], 3), "story-fo1") - self.assertEqual(self.dbm.get_next_new_story("", 55, ["excited"], 9), + self.assertEqual(self.dbm.get_next_new_story("", ["excited"], 9), "story-fo2") - self.assertEqual(self.dbm.get_next_new_story("P001", -22, ["excited"], + self.assertEqual(self.dbm.get_next_new_story("P001", ["excited"], 4), "story-fo1") - self.assertEqual(self.dbm.get_next_new_story("002", 0, ["guilty"], 10), + self.assertEqual(self.dbm.get_next_new_story("002", ["guilty"], 10), "story-am1") - self.assertEqual(self.dbm.get_next_new_story("0.03a", 9, ["guilty"], 1), + self.assertEqual(self.dbm.get_next_new_story("0.03a", ["guilty"], 1), "story-fo1") - self.assertEqual(self.dbm.get_next_new_story("0", 8, ["surprised"], 7), + self.assertEqual(self.dbm.get_next_new_story("0", ["surprised"], 7), "story-fo1") - self.assertEqual(self.dbm.get_next_new_story("P01", 2, ["surprised"], + self.assertEqual(self.dbm.get_next_new_story("P01", ["surprised"], 6), "story-sr1") - self.assertEqual(self.dbm.get_next_new_story("0", 5, ["afraid"], 5), + self.assertEqual(self.dbm.get_next_new_story("0", ["afraid"], 5), "story-fo2") - self.assertEqual(self.dbm.get_next_new_story("P01", 2, ["afraid"], 10), + self.assertEqual(self.dbm.get_next_new_story("P01", ["afraid"], 10), "story-fo2") - self.assertEqual(self.dbm.get_next_new_story("0", 9, ["frustrated"], + self.assertEqual(self.dbm.get_next_new_story("0", ["frustrated"], 8), "story-cr1") - self.assertEqual(self.dbm.get_next_new_story("P01", 2, ["frustrated"], + self.assertEqual(self.dbm.get_next_new_story("P01", ["frustrated"], 3), "story-fo1") - self.assertEqual(self.dbm.get_next_new_story("0", 5, ["calm"], 10), + self.assertEqual(self.dbm.get_next_new_story("0", ["calm"], 10), "story-st1") - self.assertEqual(self.dbm.get_next_new_story("P01", 2, ["calm"], 1), + self.assertEqual(self.dbm.get_next_new_story("P01", ["calm"], 1), "story-fo1") - self.assertEqual(self.dbm.get_next_new_story("0", 5, ["bored"], 8), + self.assertEqual(self.dbm.get_next_new_story("0", ["bored"], 8), "story-sr2") - self.assertEqual(self.dbm.get_next_new_story("P01", 2, ["bored"], 3), + self.assertEqual(self.dbm.get_next_new_story("P01", ["bored"], 3), "story-fo1") - self.assertEqual(self.dbm.get_next_new_story("0", 9, ["frustrated", + self.assertEqual(self.dbm.get_next_new_story("0", ["frustrated", "bored", "happy"], 8), "story-fo1") - self.assertEqual(self.dbm.get_next_new_story("0", 9, ["frustrated", + self.assertEqual(self.dbm.get_next_new_story("0", ["frustrated", "surprised", "sad"], 8), "story-fo1") - self.assertEqual(self.dbm.get_next_new_story("p391", 1, ["happy", + self.assertEqual(self.dbm.get_next_new_story("p391", ["happy", "frustrated", "afraid", "sad"], 1), "story-fo2") - self.assertEqual(self.dbm.get_next_new_story("p391", 2, [""], 1), + self.assertEqual(self.dbm.get_next_new_story("p391", [""], 1), "story-fo1") @@ -311,7 +311,7 @@ def test_exceptions(self): self.dbm.get_most_recent_incorrect_emotions("p023", 1) with self.assertRaises(Exception): - self.dbm.get_next_new_story("p391", 1, ["happy", "frustrated"], 1) + self.dbm.get_next_new_story("p391", ["happy", "frustrated"], 1) with self.assertRaises(Exception): self.dbm.get_next_review_story("40", 5, ["bored"], 8) From 1e21707ce22766d0d618bdcd2d843c68bef47f0d Mon Sep 17 00:00:00 2001 From: jakory Date: Mon, 3 Oct 2016 21:24:36 -0400 Subject: [PATCH 24/30] Clean up SQL - Refactor SQL queries for clarity and simplicity. - No meaningful change besides efficiency and readability. --- src/ss_db_manager.py | 90 ++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 50 deletions(-) diff --git a/src/ss_db_manager.py b/src/ss_db_manager.py index e6fcf96..0fb10cf 100644 --- a/src/ss_db_manager.py +++ b/src/ss_db_manager.py @@ -62,8 +62,8 @@ def get_most_recent_level(self, participant, current_session): result = self._cursor.execute(""" SELECT level_id FROM stories_played - WHERE participant=(?) - AND session=(?) + WHERE participant = (?) + AND session = (?) ORDER BY time DESC LIMIT 1""", (participant, (current_session-1))).fetchone() @@ -103,13 +103,11 @@ def get_percent_correct_responses(self, participant, session, FROM responses JOIN questions ON questions.id = responses.questions_id - WHERE questions.target_response = responses.response - AND responses.stories_played_id in ( - SELECT id - FROM stories_played - WHERE participant = (?) - AND session = (?) - ORDER BY time DESC) + JOIN stories_played + ON responses.stories_played_id = stories_played.id + WHERE questions.target_response = responses.response + AND stories_played.participant = (?) + AND stories_played.session = (?) """ # Only filter by question type if one was provided. + ("" if (question_type is None) else \ @@ -118,6 +116,7 @@ def get_percent_correct_responses(self, participant, session, ((participant, session) if (question_type is None) else \ (participant, session, question_type)) ).fetchone() + #TODO helper function, "correct" as parameter like question type # The participant may not have responded to any questions # correctly. @@ -182,14 +181,12 @@ def get_most_recent_incorrect_emotions(self, participant, current_session): SELECT DISTINCT responses.response, questions.target_response FROM responses JOIN questions - ON questions.id = responses.questions_id - WHERE questions.target_response <> responses.response - AND responses.stories_played_id in ( - SELECT id - FROM stories_played - WHERE participant = (?) - AND session = (?) - ORDER BY time DESC) + ON questions.id = responses.questions_id + JOIN stories_played + ON responses.stories_played_id = stories_played.id + WHERE questions.target_response <> responses.response + AND stories_played.participant = (?) + AND stories_played.session = (?) """, (participant, current_session)).fetchall() # The user may not have responded incorrectly to any # questions, in which case we get no results from the query. @@ -230,52 +227,45 @@ def get_next_new_story(self, participant, emotions, level): params = list(emotions) params.append(participant) params.append(level) + params.append(participant) # The stories and questions tables should not be empty. # The stories_played table may be empty. - result = self._cursor.execute(""" - SELECT DISTINCT stories.story_name + # The first half of the query looks for a story with the + # specified emotions; the second half looks for any unplayed + # story, not caring about emotions. + query1 = """ + SELECT stories.story_name, stories.id, 0 AS found_emotion FROM stories JOIN questions ON questions.story_id = stories.id LEFT JOIN stories_played ON stories_played.story_id = stories.id WHERE questions.target_response IN (%s) - AND stories.id - NOT IN ( - SELECT stories_played.story_id - FROM stories_played - WHERE stories_played.participant = (?)) + AND (stories_played.participant <> (?) + OR stories_played.participant IS NULL) AND questions.level = (?) - ORDER BY stories.id - LIMIT 1 - """ % ",".join("?"*len(emotions)), params).fetchone() + """ % ",".join("?"*len(emotions)) - if result is None or result == []: - self._logger.warn("Could not find any unplayed stories for " - + participant + " with emotions " + str(emotions) + - " in the database! Will try to find any unplayed story...") + query2 = """ + SELECT stories.story_name, stories.id, 1 AS found_emotion + FROM stories + LEFT JOIN stories_played + ON stories_played.story_id = stories.id + AND (stories_played.participant <> (?) + OR stories_played.participant IS NULL) + """ - # Query again, but look for any unplayed stories, not - # just unplayed stories with particular emotions. - result = self._cursor.execute(""" - SELECT DISTINCT stories.story_name - FROM stories - LEFT JOIN stories_played - ON stories_played.story_id = stories.id - WHERE stories.id - NOT IN ( - SELECT stories_played.story_id - FROM stories_played - WHERE stories_played.participant = (?)) - ORDER BY stories.id - LIMIT 1 - """ , (participant,)).fetchone() + query = query1 + " UNION " + query2 + """ + ORDER BY found_emotion, stories.id + LIMIT 1 """ - if result is None or result == []: - self._logger.warn("Could not find unplayed stories for " - + participant + " in the database!") - return None + result = self._cursor.execute(query, params).fetchone() + + if result is None or result == []: + self._logger.warn("Could not find any unplayed stories for " + + participant + " in the database!") + return None # We either found an unplayed story with the right emotions # or didn't, and found an unplayed story without them. From 3246f151cdef8fedaba049beac1dfd41eaa2b8de Mon Sep 17 00:00:00 2001 From: jakory Date: Tue, 4 Oct 2016 14:28:54 -0400 Subject: [PATCH 25/30] Add more personalization manager tests --- src/ss_personalization_manager.py | 34 ++++++------- src/test_personalization_manager.py | 74 +++++++++++++++++++++++++---- 2 files changed, 83 insertions(+), 25 deletions(-) diff --git a/src/ss_personalization_manager.py b/src/ss_personalization_manager.py index 4adaac6..e39f78b 100644 --- a/src/ss_personalization_manager.py +++ b/src/ss_personalization_manager.py @@ -143,12 +143,11 @@ def get_next_story_script(self): # If no story has been picked yet, print error and pick a story. elif (self._current_story is None): - self._logger.error("We were asked for the story script, but we " - "haven't picked a story yet! Picking a story...") - self.pick_next_story() + self._logger.error("We were asked for the story script, but we " + "haven't picked a story yet! Picking a story...") + self._current_story = self.pick_next_story() - # Return name of story script: story name + level + file - # extension. + # Return name of story script: story name + level + file extension. return (self._current_story + "-" + str(self._level) + ".txt").lower() @@ -166,13 +165,16 @@ def pick_next_story(self): self._logger.debug("Using DEMO script.") # Save that we are using the demo story. self._current_story = "demo-story-1" - return "demo-story-1.txt" + return "demo-story-1" + + # We start without having picked the next story. + story = None # If we should tell a new story, get the next new story that # has one of the emotions to practice in it. If there aren't # any stories with one of those emotions, just get the next new # story. - elif self._tell_new_story: + if self._tell_new_story: story = self._db_man.get_next_new_story(self._participant, self._emotion_list, self._level) @@ -208,8 +210,8 @@ def pick_next_story(self): # Save current story so we can provide story details later. self._current_story = story - # Return name of script: story name + level + file extension. - return (story + "-" + str(self._level) + ".txt").lower() + # Return name of the story. + return story def get_next_story_details(self): @@ -232,14 +234,14 @@ def get_next_story_details(self): + "\nIn order: " + str(in_order) + "\nNum answers: " + str(num_answers)) - # If the current story isn't set, print error, and pick a story. - elif (self._current_story is None): - self._logger.error("We were asked for story details, but we \ - haven't picked a story yet! Picking a story...") - self.pick_next_story() - - # Otherwise, we have the current story. + # Otherwise, we will get the details for the current story. else: + # If the current story isn't set, print error, and pick a story. + if (self._current_story is None): + self._logger.error("We were asked for story details, but we \ + haven't picked a story yet! Picking a story...") + self._current_story = self.pick_next_story() + # Get story information from the database: scene graphics # names, whether the scenes are shown in order, how many # answer options there are per question at this level. diff --git a/src/test_personalization_manager.py b/src/test_personalization_manager.py index b081be1..5a6d257 100644 --- a/src/test_personalization_manager.py +++ b/src/test_personalization_manager.py @@ -26,7 +26,7 @@ import unittest import mock import random -from mock import Mock +from mock import Mock, patch from ss_personalization_manager import ss_personalization_manager from SS_Errors import NoStoryFound @@ -155,14 +155,24 @@ def test_get_performance_this_session(self): self.assertEqual(self.pm.get_performance_this_session(), pd) - def test_get_next_story_script(self): - pass + @patch("ss_personalization_manager.ss_personalization_manager." + + "pick_next_story") + def test_get_next_story_script(self, mock): + # The get_next_story_script function is also tested in the next test, + # when testing the pick_next_story function. + # Test a participant with no data on their first session. + dbm = self.setup_no_participant_data("P001", 1) + mock.return_value = "story-cr1" + self.assertEqual(self.pm.get_next_story_script(), "story-cr1-1.txt") + self.assertTrue(mock.called) def test_pick_next_story(self): # Test demo session. self.setup_demo() - self.assertEqual(self.pm.pick_next_story(), "demo-story-1.txt") + self.assertEqual(self.pm.pick_next_story(), "demo-story-1") + self.assertEqual(self.pm._current_story, "demo-story-1") + self.assertEqual(self.pm.get_next_story_script(), "demo-story-1.txt") # Test a participant with no data on their first session. dbm = self.setup_no_participant_data("P001", 1) @@ -172,21 +182,67 @@ def test_pick_next_story(self): self.assertTrue(self.pm._tell_new_story) # Mock relevant story data. + # Need new story, no new or review story found. dbm.get_next_new_story.side_effect = [ None, None ] dbm.get_next_review_story.side_effect = [ None ] with self.assertRaises(NoStoryFound): self.pm.pick_next_story() - # Mock relevant story data. + # Need new story, no new story found. dbm.get_next_new_story.side_effect = [ None, None ] + dbm.get_next_review_story.side_effect = [ "story-cr1" ] + self.assertEqual(self.pm.pick_next_story(), "story-cr1") + self.assertEqual(self.pm._current_story, "story-cr1") + self.assertEqual(self.pm.get_next_story_script(), "story-cr1-1.txt") + + # Tell new story flag should toggle. + self.assertFalse(self.pm._tell_new_story) + + # Need review story, review story found on 1st try. dbm.get_next_review_story.side_effect = [ "story-cf1" ] - self.assertEquals(self.pm.pick_next_story(), "story-cf1-1.txt") + self.assertEqual(self.pm.pick_next_story(), "story-cf1") + self.assertEqual(self.pm._current_story, "story-cf1") + self.assertEqual(self.pm.get_next_story_script(), "story-cf1-1.txt") - # Tell new story starts out True, toggles after each call to - # pick_new_story. + # Tell new story flag should toggle. + self.assertTrue(self.pm._tell_new_story) + + # Need new story, new story found on 1st try. + dbm.get_next_new_story.side_effect = [ "story-cr1", None ] + dbm.get_next_review_story.side_effect = [ None ] + self.assertEqual(self.pm.pick_next_story(), "story-cr1") + self.assertEqual(self.pm._current_story, "story-cr1") + self.assertEqual(self.pm.get_next_story_script(), "story-cr1-1.txt") + + # Tell new story flag should toggle. + self.assertFalse(self.pm._tell_new_story) + + # Need review story, no review or new story found. + dbm.get_next_new_story.side_effect = [ None ] + dbm.get_next_review_story.side_effect = [ None ] + with self.assertRaises(NoStoryFound): + self.pm.pick_next_story() + + # Need review story, no review story found, get new story. + dbm.get_next_new_story.side_effect = [ "story-cf1" ] + dbm.get_next_review_story.side_effect = [ None ] + self.assertEqual(self.pm.pick_next_story(), "story-cf1") + self.assertEqual(self.pm._current_story, "story-cf1") + self.assertEqual(self.pm.get_next_story_script(), "story-cf1-1.txt") + + # Tell new story flag should toggle. + self.assertTrue(self.pm._tell_new_story) + + # Need new story, new story found on 2nd try. + dbm.get_next_new_story.side_effect = [ None, "story-cr1" ] + dbm.get_next_review_story.side_effect = [ None ] + self.assertEqual(self.pm.pick_next_story(), "story-cr1") + self.assertEqual(self.pm._current_story, "story-cr1") + self.assertEqual(self.pm.get_next_story_script(), "story-cr1-1.txt") + + # Tell new story flag should toggle. self.assertFalse(self.pm._tell_new_story) - #TODO ADD MORE def test_get_next_story_details(self): From ce5c1ba782e40e980c18064d984cda1da2638e30 Mon Sep 17 00:00:00 2001 From: jakory Date: Tue, 4 Oct 2016 14:39:38 -0400 Subject: [PATCH 26/30] Change emotion "angry" to "mad" in scripts --- game_scripts/story_scripts/demo-story-1.txt | 4 ++-- source_ods/story-test-set.ods | Bin 33599 -> 33572 bytes 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/game_scripts/story_scripts/demo-story-1.txt b/game_scripts/story_scripts/demo-story-1.txt index d3db9f6..fcb4202 100644 --- a/game_scripts/story_scripts/demo-story-1.txt +++ b/game_scripts/story_scripts/demo-story-1.txt @@ -23,8 +23,8 @@ WAIT CORRECT_INCORRECT 10 ROBOT DO "Lisa felt frustrated." OPAL CLEAR ANSWERS PAUSE 1 -OPAL LOAD_ANSWERS answers/lisa_angry.png, answers/lisa_afraid.png, answers/lisa_frustrated.png, answers/lisa_happy.png -OPAL SET_CORRECT {"correct":["lisa_happy"], "incorrect":["lisa_angry","lisa_afraid","lisa_frustrated"]} +OPAL LOAD_ANSWERS answers/lisa_mad.png, answers/lisa_afraid.png, answers/lisa_frustrated.png, answers/lisa_happy.png +OPAL SET_CORRECT {"correct":["lisa_happy"], "incorrect":["lisa_mad","lisa_afraid","lisa_frustrated"]} ROBOT DO "How did Lisa feel when she got her shoe back from her mom?" WAIT CORRECT_INCORRECT 10 ROBOT DO "Lisa felt happy." diff --git a/source_ods/story-test-set.ods b/source_ods/story-test-set.ods index f7de115526c5ff5c0dacbd72b12ca87ad13c58e9..e8d33d3969907ca03723c79959370e9b732edf82 100644 GIT binary patch delta 8323 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

F=HS0W-vKlrQ?z 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 Date: Wed, 5 Oct 2016 20:09:39 -0400 Subject: [PATCH 27/30] Clean up SQL, follow up to 1e21707 - level_id field renamed to level --- src/ss_db_manager.py | 61 ++++++++++++++++--------------------- src/ss_init_db.py | 8 ++--- src/ss_process_story_ods.py | 4 +-- 3 files changed, 32 insertions(+), 41 deletions(-) diff --git a/src/ss_db_manager.py b/src/ss_db_manager.py index 0fb10cf..83d07b7 100644 --- a/src/ss_db_manager.py +++ b/src/ss_db_manager.py @@ -60,7 +60,7 @@ def get_most_recent_level(self, participant, current_session): try: result = self._cursor.execute(""" - SELECT level_id + SELECT level FROM stories_played WHERE participant = (?) AND session = (?) @@ -234,6 +234,9 @@ def get_next_new_story(self, participant, emotions, level): # The first half of the query looks for a story with the # specified emotions; the second half looks for any unplayed # story, not caring about emotions. + # 0 for the first query and 1 for the second query lets us + # order the results by anything found from the first query (the + # first half of the union) before anything from the second half. query1 = """ SELECT stories.story_name, stories.id, 0 AS found_emotion FROM stories @@ -314,15 +317,11 @@ def get_next_review_story(self, participant, current_session, emotions, FROM stories JOIN questions ON questions.story_id = stories.id - LEFT JOIN stories_played + JOIN stories_played ON stories_played.story_id = stories.id WHERE questions.target_response IN (%s) - AND stories.id - IN ( - SELECT stories_played.story_id - FROM stories_played - WHERE stories_played.participant = (?) - AND stories_played.session <> (?)) + AND stories_played.participant = (?) + AND stories_played.session <> (?) AND questions.level = (?) ORDER BY RANDOM() LIMIT 1 @@ -342,13 +341,13 @@ def get_next_review_story(self, participant, current_session, emotions, result = self._cursor.execute(""" SELECT stories.story_name FROM stories_played - LEFT JOIN stories + JOIN stories ON stories_played.story_id = stories.id WHERE stories_played.participant = (?) AND stories_played.session <> (?) GROUP BY stories_played.story_id ORDER BY count(stories_played.story_id) ASC, - stories_played.time ASC + max(stories_played.time) ASC LIMIT 1 """, (participant, current_session)).fetchone() @@ -383,7 +382,7 @@ def get_level_info(self, level): result = self._cursor.execute(""" SELECT num_answers, in_order FROM levels - WHERE level=(?) + WHERE level = (?) """, (level,)).fetchone() if result is None: self._logger.warn("Could not find info for level " + str(level) @@ -409,11 +408,11 @@ def get_graphics(self, story, level): result = self._cursor.execute(""" SELECT graphic FROM graphics - WHERE level_id=(?) - AND story_id=( + WHERE level = (?) + AND story_id = ( SELECT id FROM stories - WHERE story_name=(?)) + WHERE story_name = (?)) """, (level, story)).fetchall() if result is None or result == []: self._logger.warn("Could not find graphics for story " + story @@ -439,16 +438,14 @@ def record_story_played(self, participant, session, level, story): try: self._cursor.execute(""" INSERT INTO stories_played (participant, session, - level_id, story_id) + level, story_id) VALUES ( (?), (?), (SELECT id FROM stories - WHERE story_name=(?)), - (SELECT level - FROM levels - WHERE level=(?))) + WHERE story_name = (?)), + (?)) """, (participant, session, story, level)) # Commit after recording the story. self._conn.commit() @@ -472,30 +469,24 @@ def record_response(self, participant, session, level, story, question_num, response VALUES ( (SELECT id from stories_played - WHERE participant=(?) - AND session=(?) - AND level_id=( - SELECT level - FROM levels - WHERE level=(?)) - AND story_id=( + WHERE participant = (?) + AND session = (?) + AND level = (?) + AND story_id = ( SELECT id FROM stories - WHERE story_name=(?)) + WHERE story_name = (?)) ORDER BY time DESC LIMIT 1), (SELECT id FROM questions - WHERE question_num=(?) - AND question_type=(?) - AND level=( - SELECT level - FROM levels - WHERE level=(?)) - AND story_id=( + WHERE question_num = (?) + AND question_type = (?) + AND level = (?) + AND story_id = ( SELECT id FROM stories - WHERE story_name=(?))), + WHERE story_name = (?))), (?)) """, (participant, session, level, story, question_num, question_type, level, story, response)) diff --git a/src/ss_init_db.py b/src/ss_init_db.py index 87e776d..3139520 100755 --- a/src/ss_init_db.py +++ b/src/ss_init_db.py @@ -76,11 +76,11 @@ def ss_init_db(): # each level. cursor.execute(""" CREATE TABLE graphics ( story_id integer NOT NULL, - level_id integer NOT NULL, + level integer NOT NULL, scene_num integer NOT NULL, graphic text NOT NULL, FOREIGN KEY(story_id) REFERENCES stories(id), - FOREIGN KEY(level_id) REFERENCES levels(level) + FOREIGN KEY(level) REFERENCES levels(level) )""") # The QUESTIONS table lists meta-information about questions that @@ -129,10 +129,10 @@ def ss_init_db(): time timestamp NOT NULL default current_timestamp, participant text NOT NULL, session integer NOT NULL, - level_id integer NOT NULL, + level integer NOT NULL, story_id text NOT NULL, FOREIGN KEY(story_id) REFERENCES stories(id), - FOREIGN KEY(level_id) REFERENCES levels(level) + FOREIGN KEY(level) REFERENCES levels(level) )""") conn.commit() diff --git a/src/ss_process_story_ods.py b/src/ss_process_story_ods.py index abc4595..cc009ad 100755 --- a/src/ss_process_story_ods.py +++ b/src/ss_process_story_ods.py @@ -238,7 +238,7 @@ def insert_to_stories_table(cursor, story_names): def insert_to_graphics_table(cursor, story_name, level, scene, graphic_tag): """ Add a list of graphics names to the graphics table.""" # story_id = The id from the stories table for this story. - # level_id = The level number from the levels table for this level. + # level = The level number from the levels table for this level. # scene = Scene number (1,2,3,4) to load this graphic into. # graphic_tag = Tag of graphic to load (lowercase letter). print("ADD GRAPHIC: " + story_name + "-" + str(level) + " scene" + @@ -251,7 +251,7 @@ def insert_to_graphics_table(cursor, story_name, level, scene, graphic_tag): graphic_name = story_name.replace("Story-","") + "-" + \ ("P" if level < 6 else "B") + "-" + graphic_tag + ".png" cursor.execute(""" - INSERT INTO graphics (story_id, level_id, scene_num, graphic) + INSERT INTO graphics (story_id, level, scene_num, graphic) VALUES ( (SELECT id FROM stories WHERE story_name=(?)), (SELECT level FROM levels WHERE level=(?)), From abac641eab28430c6d791e7e86bb042800ead443 Mon Sep 17 00:00:00 2001 From: jakory Date: Wed, 12 Oct 2016 14:10:52 -0400 Subject: [PATCH 28/30] Change "afraid" to "scared" in stories --- game_scripts/story_scripts/demo-story-1.txt | 4 ++-- source_ods/story-test-set.ods | Bin 33572 -> 46658 bytes 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/game_scripts/story_scripts/demo-story-1.txt b/game_scripts/story_scripts/demo-story-1.txt index fcb4202..584e59c 100644 --- a/game_scripts/story_scripts/demo-story-1.txt +++ b/game_scripts/story_scripts/demo-story-1.txt @@ -23,8 +23,8 @@ WAIT CORRECT_INCORRECT 10 ROBOT DO "Lisa felt frustrated." OPAL CLEAR ANSWERS PAUSE 1 -OPAL LOAD_ANSWERS answers/lisa_mad.png, answers/lisa_afraid.png, answers/lisa_frustrated.png, answers/lisa_happy.png -OPAL SET_CORRECT {"correct":["lisa_happy"], "incorrect":["lisa_mad","lisa_afraid","lisa_frustrated"]} +OPAL LOAD_ANSWERS answers/lisa_mad.png, answers/lisa_scared.png, answers/lisa_frustrated.png, answers/lisa_happy.png +OPAL SET_CORRECT {"correct":["lisa_happy"], "incorrect":["lisa_mad","lisa_scared","lisa_frustrated"]} ROBOT DO "How did Lisa feel when she got her shoe back from her mom?" WAIT CORRECT_INCORRECT 10 ROBOT DO "Lisa felt happy." diff --git a/source_ods/story-test-set.ods b/source_ods/story-test-set.ods index e8d33d3969907ca03723c79959370e9b732edf82..2f0354b1f52d293ee610b1ce0331723a2fd8b21b 100644 GIT binary patch delta 44932 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 Y>p!ED`0JfXt0wU-kOros_AkT#0f-p#uK)l5 delta 31768 zcmZ^~WmFv9qOOa(Gz5Y)4#6D)1b252Zo%EXaM$3$LU0J~0fM``ySuv`zO~lfd*35} zx<`-E)m=5mteUSpv-21#F9ZrzQ3e_Y3jzWG0wOX|G#*t6?fp$^4~_cICwT~nPxA5B zEIsj}oJYy=8Hzj3cf)p#?7i+7C``Os>|& zOv&v%!(A|rKh4L-4rh~?iDxiocvQtdIKzZLF&W@3dNxZ^an5mOjFqBd4RSh;(jB zHvlBuKvJ_;!?&F^N3HI0?|Knlt6?)qDE-Qc`TfZ*#Laur^3$m9xcr0rz%hLi-)3ih zq3554pq)KnP=y(EjV08GjJ*gNrXk7N*g1!I^KwFB$`)Dz1tI<93x3PV79hfJoY_}9NnC%3DECaempXR#d+Z!DoGLQCfWQx&SNV#ddItPzV%2Lh_UMd+uC2hhWl2GIQ{0osA+jHY|?( z-Wn|;%Ra;I<@|Op!zB32iF%ZMshXt$r6SgpIXi`vqAjM&MhJ-f)DbGFQp<+47evrU ziz?b~4XtWHw7u$%iIxS(Mv`Aq^`~JJBAe-9$5IcXQZT0I4>g}Z^_BsreB)(f?fgE} zG7~Pws=_bKCkri+Si#+MvHTv-MXR_aGwI(qJMT^wF;W(x+B`PPVv|Rn_qS>v4H+{) z>4a*z2^LKKK8y^iY8v-4j0KPZvIv8jHSPpr+ZEaC-<_so*2I9y6axu2Xj9*oMq$T;lV9pD4kMV5f^qJVN_n0lw!M!1HD6`?6lYh0^%d4izdUj z9S}WTlT&Hq@+!c^aI#piR*h5&a$mLci@e&UA{%Iob?#`#gHY@)}gpI>!g}d zL4HM~i1>+3&yEnG%a|fuoOn^s52{ty-Wb!Nj`#pG75~*eM5B2oYEw0JKoAo~2M?YF z>Q@mEA!bzGkwCk7g)kIopF-DOuLJibgIZBBmRoIs5yjy<#Z3sv5AF{cYe{1x%=ZCo z$Dl1aNFQ`6TS||s7=%Aob=;icJW4pY!M38H630(1IRt$mB&a^K1Q>O#L#P^93>XIO z_Q22pog4yYBONW%EDm;BM%v+1LBp_??`{$Rj*(*yBQx{n#z?cTpn<`_&k~eMCK3Vb zK%8y3K*N`@wH+)AeZa!o!=hL#S7p$!Q;yUCBFvFnsZ9MSzFfOt`m5dZ)pp)OHqhMo z7sE0BdYktbMICq7KwE?N>xy)=II6nm{ZvBe&EdMBlGYjvin~tKHS<7Hm1N zmyItL&*i@rrUNMnLBm6c7A#E_Xffl!P3Rb`4oP4YT%h9G3{8*7EXn>kK($m&kU%== zFgwu8(XN>Ye|*ZcZqUTQe6lT(;<^(qD9;K)6s(TRu{GFoyQ`eWVDE zAtQpmA?!L9f7WUM7cm`EB8tW|D#`+78VPwz=IqvAn+FNMk2iRMBWEH=90gVK)f2eT z++hJout76sp7yYD^@E|hpLhbPXBknS!%r9rvEoGO*pENIr`Zu?V8KDcLcqmt4@Y*{Bx=?!UY-GB$*^hF zd{5Ji=b}i^;hZBY^fha@snSSk`B<)A7$3)^bR^>l?qxykW{cmokQVB+Jk+_+m?w{5IMNq;@;#+JcZ$Ve3?#whL_UF+n} z0-T+s?n_X@El^ZhlL7Xm+w0Vbh-BEY=Z3QgeeaP$-^VX>zt6 zyk3~%zvqtlQ)jV6DUo)wS)fMcR)POIjn#TGICfMoM#TvpZ|&*qks$-N(%J2>n(fxs z3L1Nv_}O+tVb6%$>}xc}vf8}gn*KPr!RlT2_1aWLYq^Yu={8;$CiXHY8_D)5WT~Z- zWqa(>B;b=S+lH@ioqlHUjl*pG2)xLh&TPJlgy{36`#esfQc0U~D6C}rxE~JRB_&5@ zd)L9tipzRmPIx$(mf{1h)+^N9Il+g+IhonMH3SplEw_t_a&3$>wd_xrD?6xVT={UAXI)O%S7@Q|<*VJT^;DNJM}7#v*% zE{B6lVPQ?B3&r0aE_-*z&k-WXilxVfK#o&z3np|lsduklxFJBx#VlQOgDnw z{!@%jqxY%F?HX{#XU|K?e|sLDQE3{HfP+-Xn|;6Rq{6R#d8Kfr_v8xeXg~H!rRTw8 zw?UQedOWuJbeH=<@A))vK|vwi!+7kjt^8}v^-yr8*WK&mR4hJ4&{yn>z{Ns_uYXGv z#3MNjBev(6)0+T-ZZ?7y_$y_r&t@@1F*eJ$#eE@*($c+h?jFu>jEitKDsj&}k-ol{ zIqDl2H?F_mq=M(~&UkKTs*5=l&d?%@8qALDVm60xE@kw9Z^ExT0r%U}A2XM{A2yaN zDNht$s`sVkd*eT(P_|wlW~5}JSn|mxIQdRx&o(X_@ntr|9*BP%U#5cTl+){Fj~1`mce6LA7J;_wz}F9Z zSj6``xv*=COvDOfp6n(2gAv(tiF?P+5_z1~o%T>h7R6Vj#PuDQ_ z?|i?rX=L2`xbo+w<$TYwaK$t#>}0uWsD6GPjgTA2m~W^rY7F~Mv*diZQ06qJt35ky zxA<2Dy!F@$>*o`#UNKih|1D8x6#2#wFJ>d0hKBRfmp49vSZGkKfjyO4z%2W?D6bcGcK{M^LWs*uwjkTx&>#^K?xtj6mufs@1gx@!?NJ;R51-w0{Kt8PpNBdFUm za6-#@r?tt+?+%~K+}>R@Xv#1c^o2s@_F&~IbL?XqZy|bG^Vc$UIyEFtb@K=X@Pi_d z;((6cN%H`n5+BX{3@=8=+C(5qgddHhLpNi&K7qKrQtLvsIl6O_?rWE|z`{*CC=SsO z6DV2lkPu?^_x?e1)EMbK@P-Y?nIPfdxdT3Z*vMqzE5gm>xL7EW4wHxw~16hyQiGb zOLWrOa2Io85U5rBb#7GPER{t?l#g&Ed`UqbM##2G6E4cT+^J285eIFuH*Y)fmXKRZ z=UtEE(tq(jTRrs*)SENte-25b&JT-5v6?A~mS^gc>`$Nf$AzLmw(&&KovOwOYZb~R z?VfHD;Yrd;c{dQYNq>KD*b-5li|DmU$@#f7(4b^UeTvB4}7g^OAsv z5e?5)>&>VIW-?+H_n-2GFZ=0dGu$i3O~}*S#_GFX8c5}0eLc-p{4l{qSkR*4z;@2gQ!B-|;dC03XP?SI&E ze;D-~a1`&5AaX<7Jk>L(+0Qb3HpB9b>_llOYl_?lVW~c ziUzgARLlV%@|qP_uJ&wd;<>o96T(N$oGa`~$Zvu9Gq#A5q*)x~nT(PlJ_8B(16ZRY zs{af_uyn*UHY^6ZmEM%Ce~M>z$#{FHuq8V_*1uBiA^r><1;wt*vVk8$`8YDbB3qhYPAJt8yU&Q(o$qP z96u_@?~`T$B)y*xY}~P{m$MF8Bdk#hjjA7CT%G^h0^#WVqZG*8?Ux~mr&B`Q0CK0>?TW5Xem9vn!F!y~Gp zRqkg97o(t~o5hHp#%w%j!4F3;v=pkX#^^~^>7w%_-KFwBvdE2?2holf4c7hj%IhcX z3189dCf#ibNr>^>Q4>p}O*0`?E%~}Lhc=Hq*0kf)x$uKB!7Lz2qJV%vzFdca1_@-H?_mYt4@|&`uUFo&$%=oTr>ax4K+JX)P7;T^%~0~pUd7hW7bI0L>NJZa zmy=iQGz}jEr+<(X1-Bdxezlfox`hQ^xY%iCO};$We8rrC8-@9jKK~Rp^2LnXnMXnT zEt*3S@}Vrr4LZDuS}3-s4ci*0<>N6m2X&!X6g+^qiAkhk^pRiUIuw!x5=l9v+l)g9z^rZcV(QUn)_RBV*(JMg`4<`x&M4+r?(0lII%VP7U{^diwe z77V!GgY0S^KX4)-Me}`oC9i5aH#-ED+j)^vIV5q1NtWM0k~Zs&zkb@ByZ)xD_qRZd z;;7t$J77))MsmnQ*4Z*{&74_?o(-$DxcJQ|-03FziB&xrF=2X`52%&tWpD5joH~`@ zDE7mG>#RYdAH6B$gslI^0l@na)b#{L^irV2b`)9p4kB`M@N?S4izyfvp`CL-M>-Q^ zjoyoTtals}V0~5SZMO*r2X(GZ$C67J^as`+Vhw~F1%5Wv{u?{P!MGZe z@t^Y=1}HdiUS(n7vGeo|!LLslonA+5u2*ouu||9l(h>$AI~5$B+UhU}*? z_!zcc5maeI$8rfN@$S z6Hovsm6{co<(Y?s8ISVQ_ML4^@KStox+janu zQ3pvVY8pYp1Li{2My*_d5f@D22?{#Gl$bT=Uw#Rogdr-%u~P7>99W%Q!a7BeEC>5# za#q@rlE!xRVNiSuA=#aPndlbB+3FysEDhdr5)Wv=jK;)FcGoHsqOsGZ>oA*yuC#*{ zk*CJxoX%|YZ$g;o+$SZK`##S6S_T+2j}g+}*8W-Q0cy6<*^Dw&Qy|~u2-{W)aTijD zE#UyG9j5nr@;*&Ew8O>2$CWpW-vZS|tOCOZewSE3{S6CnE2&C;Vh0r4 z2MH#T8K{7A>3taH5A`FH}6>ti52V3%H!O!y90l-TqS8gp2~4uhDDF4qEtE0ji?ecJ4f&cX}~w?$g=U{yKgmJM6sX9x0InOi%$ML3O6zUYxK|47x4T zCX>L+f>~Ph2hTg(#-oN$PfjD@_)EXh?Yyrxt}ibg&$I^@*4wU+Yb_C%|BwOyI@F%mUZAE)8tqo_peZ?oRsfq@K~jY}r|B(zssnVF&ur zG$}{kBfs>93M@gpss|-{cyRMom3oKH<_B3SBEiy2J4cn6!w=DzM@A{lVIQ0 z<`J}v8Guw_HZ7*lR}&de!z?+g|~6ctM8l3F4K&{)rnoraHKgQ1({}WO<1v_QNoGw~6n(i$qrbg(H zSp65G$z$=_%ng_&K z==cUqKTj2>0WpG#(g6+DXja!~o(hKu)(+;@ml+;c1oAkCl@bM?-yA#ocwEn7-V`u};g#B7m7tIh|jYD`^Z4*-pFkH8EmU2}Jx=uw;A;D^p^o2tQva(iB?y@0Zbp`JWwafr+SK7OL z&b5VV(}f;Z&0=&fmqX;My%ETVZol~0JIe%DS1ZYE-%RW*jTdA}6-a+1g$}vlAfTGd zcNMBuJ0BUaUQWn@hDGXQDeU@)pZR>BAP2#(_?%2wvq-|M>E0xKDy&q9e8%GrL$X)7 zgitNKXE~=hvb!HjuaB92aBnQ0xmgp%s3p(;eZIAL`A_K>&8ZC4~Eue8v=4Lj@+V>a^5vEpQhAw`yV zHQD@MIyTw4KaSRG&}n@OBMw1MfQ`_?6z0oDE!)wcWYR?^#X<6rf8A{&-c=D(0JQ^- zv|2LxU7BY2CoKLCakNT#fC0h*8;7AjGlbeov-g>b7(tyimZi=rSRdL3KmKuhm zF5=y5f3HAYYG;RAXOj(Yn}mVgV+vV!(v}?S96KTFFsb`Ty?|<}FHS`&EdotA2F>er27QOD{}LAG@$COz<^^Es!ho}XWEheHDM!L#K+qFP=L^Qq|FFPMktL(oj*8dVH-ODAlbTnE#Q+)EKgoyk)aLRe$<=^ywhrxBe^yis-VS z%A^0N2XF;+zKex_)SCh%?BPhKo?k$H4y}w!KZhXXFin8soIjGJE*;v$PaA_8RSj!s zCvas$%$3+!c3!F|jE1Ux7Dz$m3&A5+cB)?!h*Qcl(0_&PUJ`kB(r0i6mPI3n4RAR>yz7**vsQ}ox`yRVO%1sb#v4%6HR zMoff4Uow6znmm%6LhMDwOnu906dLEXCvsSXwS!%Pt$^iG=j8jCI{;8I-b?=Y_eiEf zz|x3pvL*e!E!Qh&kbKc5%dYhjK**!_6%6^-epOijUB2Y3`I93Z&O+;zT7Etd$ zqO4nm8WoK$2!tHCP-EhE#bW^spW@!FlnMx}`)WG>7ruW0lf*^Z1%t2Wft{aA05)U)G z__}Ux8(yIx&>jcM^|AXes(AcrjLWA86D=r^n5WkR5VhjntMlxdU6nOIbqJ^a3Sark z{4Y0mE8Ho2lm+lQ} z^MurwhuHs0@*g{8#CzZM@7wU*M8pdeUF*CLnV~YI2M{>VfhwFC*q$K^pW6>Wws&}@ zPF_TJlUtZ}a}_fJ(66CF@0V|6V*DqQiEXn9Ck6rye4bU>Ij~p#qlABn9m5ee4$%^Ap|t-3xjet zZfY%qyTS*Pq$65N@rFy96`N;v5hBB;`wi2f)ru1Zy?E{B+=h6A`~4V6uX&C-28XsC zgGdm@7y#JC9YXvD(5Z!U_XzDC&n@e zSB2vG(slbH)uim)0Vuh%v6Yj$qs*qiRMShRd zJF7R%GukF8+UpH1^Oq18xX&#o>kAEfUIQw_5*xKY7`U+TY&)5UgMT>&D^oTrd~oFQ zTl3kZ!g>uF3~+w8mLXHig$7eYJ7`v4@=7>8F1KS%|9bD3lB&8p-Xx1uqIg(tCQdgh zP6M#6^~>sdzgT;rGtw$#|F{@83k43HSmrtn?noBAIVBM%eT! zrmlyLW-AI2k7-kdymYzD9f;B<#FI4NQvvH21+#hJt<~-^N2a7v%~Eb6`dv1R2>@La z6(0xLlgYK&>&tuPHdux@0M$Y%>0>@czQ)GK3&X?;MbQyI^ktid=bg%E^>)0oTS6}u$(;g!n!pw{=r(8n zRy!zbH`yp>I`r~sF58#YX~C>UH|{xYkLvXP+s11b73OAwpR-7mVHeTi+tQ$#cKvC; z^-77uR&P_N8^K7^#bx9e0GZ!}^R3kq=JPF9RtESuLK>!EC1FP+@ppNix{?IAyr@wp zdaS&=9G1sX!IUJdTT$0!QX}$j!{RZVajITvz(%heYcqal?x_y9%jqA~^Vpo2u)G4~ z6vpZWLO5gU`3}{sZuLx=TGAW@(GZv8gw$2I!flKk(svamYttDCF!@P&U-xh}4kfX> zwy2jLEjFdg7EhF5NinsZI&N6j;ghda$Y=dlEY{CVHgSqKr0jxbcD$OzgSY<83!5WG ztr;V3KX|u(9o|%yR(QU>fR)b{Wd519zyLe7P&!wQ4jWV$Q?Fb$L4p>VKtUrBOZ|d%wi*jC4CW(cGECnrO{(`4f=A@Emaa5n5%A%~Il>jMIpu(z%f0N;s)b zCc>81NCA_yu*fqL;;9c+^>OxuHbsZK+w}I)TX4HdBm7-Jo|SK@+F%!8!)H&{aOW4w zs{COZ-)40ocgfYP^k%~c9z`-N+yLn-_Q!4S7VD9FGyV0siCb#}NE^TIx~#E660M$M zkgmj1%e8>a@6;tMjHmUVkZ_;-pRjDdL!@iw&ggJ8ooV_T53m1izxluwf*Hipz3)8V zPUw8s9iA5qs5Qm5x=z#xJM#$o(JF8(tbHhG9?vh~?RQQ9_xaCLCy6JxJ&sp-U(Fjr z>&o+ZG{n@)6wU}|aW69eQl#A;_6RAA;mDgSX?3Z6S$GUk#%#HH4YqQ9gfT5sJwRe2 zep{ciTWT0fWw*GDS@E*nCUAc_8O!={{~TQtn*3@8%vAoK^|~cK{qcGIKBTAljnVz$ zQ%{IKKwmu+isB_1D2S~cr6Iu+R}YyAng3^dvg-4ZYwN>l?eU0}=}W@XARGES*2nNy zYA9slkBS-7;*hOL_8-|F$TM0tj=9%Slu|tPHWsr5Dn;t@%QybmEHpYd<3IQaR9&uw zlHhZ&fLLwjZw8^izq!?0FQWCz4IN|UyV|S@%}v-&Fk3Ed{8Pa2+zdN@#L3{vE<}$F zN1FMRSC5de)Ii*SG+XzXQ7xldqE zRKQzt4Zc#3Zzs`e;rlmIxD$I|)W z5S(LRcZeK!{n>c2Pw&u}f=-QxStIH8n2W%RvevKIA>4_YjX%1DM#ki;8IO*h*?j_8 zkLP1;wwmL)vEYWQlUV>-b>|Bwxo0qe1NT5b^GK)I8v5uVl=lHKUCK`0dv2P1x10(I ztTEjGJ|De-4~c1Zb{>l@bd(Sf;KO5_cWJ3zgKiyEs#TAapF0O%w+D#zc0X5ncXXmk zXjEwBq!=;>a2=!dt4K7jJL)NY^>Egq58lebh|hW45149RRuolrEQw+Nh1**ZqN&}E z>uT@6)efX2C5z#@utK>F&5#p$v7_+;>8pRO&VIjYFaCB00p~?&%SJ)TWc*1ru?3f zt3vJC;Y6j;j4PZWhFNMnU-!G)x*P16@kAx`JO%HU8UfdqAa0v5l$`bEULmq%OydG1 zDteeP($M$E(TptMt*lplG}fvB@ZMqhe?>0WtI?;?%CC+$ta_~0MoxLnNNDxycdE!< zJ@0uPifEWhIH`XhjDW<4lbJyp>f1u~0HHw5eL*^y7>mviyG)wvm0#_p5kHjoi{t5g zzfRohK9MJ|Wu;ypJ{Ws3;WX(1PH?&s%|cq1jwonv4nxO>D~to$63z916Vh_*CPWzv>PG|<+3gL+L}cgvJh`j z>MvRG2zxcS0lhF1T+VY?RJ9e=;oSH0lv|97wA>9*`=IBpR$)*4LgjWDwV(}X`&JPo z(}?JKJOnfGryWsV{6z<1PgXcyo^RveA6=D65>ct$$}+{&2WU#Zfd!HMo5{AmOK`|u zZR=9(I{GW}@|Jg~jO7Ius+WGMk;!(=iao<}CbXeC?1V@0f%$c0zJ&O_#m%L5=1-`E zKrg!=3LAgj%)O}%3(C;2bu{-eg~@&rHacs=_ahvBy(XK^#5pM-?qjFq$ptK%OD*}4 zL=UMv&g>sCI4L1H?b;lhw9hh59jr#(_MBRJOq34iUAdbgYEKPit%~J8J~vgYm==YAt3RL9gOX-g~}OWk3pzb`tl;y$Ib6RtUEp}H4NLj^6@v@u^s zOf|I9Pl;WK_bDsi2l!4LTmqZ)gEV0(Irzo~N8#H^#B^5zt2-M;1fxN8ds6CMCx(Q~49G-QK z8l#~1S^@ZXuTWJ7GL{PX31fPGho#IRyZQe`6#v~sh_xRYO6MYCZi|d_gngc^w=rOZ zG6k=%H=B5D?mBgzQuqH)w%_gr&OaaTlQ9eQSuRbv={WX>QP7di}0R3$j`&=PPE;5#S0ZtDt5RHz;z(| zUtkH{&j~{KDs(h~x;Lh_*O2Ow^^je$z^YLI%Mb5OOxF0+;4zFBLqS=;fJD-ZG--XW zIYxuCpbrU>phF~>tq>Ab|8_v0R46pT@1jQ37x=#>GykIq{R`oa6ajneik7+V0Ui3U zG-ECO>fN+jfVqs%doZTq_I+nUke1q0aLn+ZZTUSgr)Xm;@975Z{z<1ab=8u0zvA~% z{X6mb@87jSoPU3Yqy_*uadB#Kg8xS^`A;oLY8YNPgP(}h@0Lx$AptrBwXafNwuOU9 zvYJtQQU9^=wg&jnUk4fTA;l-7pRL~PjV5rT(CDp2-)*|l(P=^}EG%gw%F--jb zBU%9d$rhx?5j8)N%cH!L;?D<8MTqb1*=1`LDt6X8?EP$V33PHWzqP-K$&IWHeNv;)JZN`kG1il%KIi^;miKHZ9+hy& zy56?6^-sD0fD`=Nrw36x+n}Kgz%8d#?;Y7nqyN{xewku$e8IRfE(0o(R0z)!-rh{^ zORzR5!$YwT35Iu@m5jSRTpzZ;%p71NUP)$}u=l_TIaX5&Ak_B zP&MBEzpb!U!~P&g`J?|``jo1&{Q0CK;ebRH0=s=p3zki`$Z~%qoBuzvVhSZeuKbVG zk97up=3OtmbyDI4^Kb)(dix2O__f-KxDx=?6mh3I|wd#hu@?y zDGqMKehWhj<}RsR0w8DgQdO>)c#0kr*|u$+;z`;aj^?gX7B!r}@&+`aUlPig(MCh0 zl-h^^M$YRHzVf8zt#1PUtCa@9VNP!Zbc{9PJ)iOmrro%8Jp=u_b>*>)SDEE-iYTh4 z`oxHiC7{LXvcnaLiA}#9zGP+po}Eig*Qc z)Aox!=U><#+KDyderr}l2$4$WOk(MCzwSnTG474kt8zAcb6|=+p5pL2(|g}XVChH@ z@dqMV+N%HErtgR0~oGFRhrke?d@4{|J?Km!&#IB|ux;PjZ_^n14l*j_^0`qTflb zNiovW%3_X}44KWNlg}srZ^cMYK6-u}e{!lrWY}Ne-vIV=(;(#_~~I_Un?Co>yuD-mW*v*mX%eu7{4t z<5lNCQPD3e4L6%5O-13T(>33f=7oL#ZQb8*E%GQ!7Ezc=j)`B7L{v%Th0ttHHL7AG;dB~+R&~K=7&w| zroh5Em5D2GT++Il4zCO3g~&JzfZGZuu}&Lc= zR{jV(C*EMQ`DlFQPt5t7@zJYmQy|85;4oz(uj7GEtU^AW!>2c@&dRr8P$a?lrl|?1 z)*)Ko^TL8~r?R>6c}+lxf*@{;`9az(dme6mmu0$~ttSR7;F~-A+;<9XnDe78Hd_El zcatl#(Nt9~NgNX54PQJd=+C>@B9^c|J6Q&namfTc`I)HR#I5_#&~!K~uTyPGKlV{D=GuGqBNLl1UB=U6rLZ&?$(3cp)f})^p^O@gYYnU^BaGa<$9#2kY zr}P%oAA*D!bY6#|ia*$JNVEpQO^_~`j(CEdhV-*vr5}>E+UWJM;uF7k8_apu-1})V6yY@xwp||-zHEtC|KGl!Q+-?SE#ie%gz<{!zo|4v&q(z58h3dj#pxf4L z2(?6O;5(DJ#iPAkX|8%I&i8TvMrO~LDD&s($Eu4G48jqQ?~a2bEVgxMf;@<_-y*V} z7QCVQu%$m8Rt}LZ4W})shxFTh)IpK?HRRy$+zv{*9!SyOz-?E8$na!n9<^8@yjA6q zww$$f9YW)pA0-jVw-V|i7mtN&%}QfmFswl(VueAOA|wAWylUvi%j*jT^lJ-cAz~&| z^~Jbs+~;YSLcn})#|XY0t!s&!XCF@~L%LHX@fAcZ&G1JinPSdG)Y(jply6^A44V** zhapQzfHAF`_`+$fUOMfX4l9mD(sMbhW7f>V57VonYU&vEIJoa`7xiJUQ%oPw81h9Jn(PDs%D-n{xY?D3V3!}nBEDx3Q&;Upxn z@2G~vo%Oui!0qK!>>s{#;$w?eA$0a$xdD>L{qO2%c}UsJHC1$Dw6s4r@dDf{(xltN z3iBuN6Bz&Sa^_C}V0kmUmn+$g+C5@3Y11w&T{x63`)F}FV%H`_!AO3ht9_(hVJW29 zBCPk7tsa1a{mkw2i#k}?gZ#}pTxFHJ?3L<0dUL!k`rtLopx3-NrLfxU`Pe>S9D2;@ z6qCW7GEqJ>b9}=YX5= zBsrDYBNWZ?BLeC9%>~oB5)@MHaZ!%O@OP)FDyiAC1zy$h%lBrks>)k2c;W9&Z^)nX zTRx)7)qY@Lpt+b+EC@Vp3}svJYjbdhZ#) zp9$EVMN_h%jbuw)ylO(D@Uk(9aR|TEh9Z4!@>Y_T?p(}%oyxZv5`xZkd;YV1%+MI$ zar;WO_Oh4WR8$(ZVA6RFy-AZ=JQ!E$iQ#E{9|+`T5AZ)P$2)nT;(z9DH0w*|5ld^8 zsJAFt^Jp;`PQ_3_BTm-d9u3pT=5pH!uLKR-hI|XVZk!yp=PI6rx_3;#H$L9qfL?oY zJm@D4{QmvJ-PxiemB6Vo2LDr!gn;|o)Y8YR?E@#I>N%Tv`zY4lKBc(`8~i%u4QD|| zJbFMfO~A;74_2op!GalFr=IQRcot5#%Y@^7xjs(iwCYPv%I|qswY3F*m$)%2m(D@V z>+sjDLcX22=rgTy8FicKgyq)cVyz_vp5h;NF6AMP*xm2y2%uuYFf{*Tw@bTKMy9^8 zp1Z^HAR4)_D&rJ1+P;PPZE?hKR5q=8l{8>E+qbFqt+H=A3=FFU16%w1QMbUn9HEmi z`}^`wL+I#?^YJLQVy{@x1&HGWG&bLY{hi{pGbP?b*y4FNM?roWj2$ zxoD#5&H4LQj;n%uh@d>>cA1YY4g9RMWx5*CQO9i7$F~}VZ8xI?wZ}2NxEqz4zg+<; zn5NNP9rIMYxf=c1%NwsN#?J5&?sq9ii6?*6x1)pXy3Km#FfU!VYk3-O*Y>izzqY^! z7d^;Ry(jO`Bwfl2JpYDvp}~sC5NQ^8sZ?F6Jeglb73bx#n(ad-Ab}7yIcA}_q-Qbh z?$)%(_+}d*sPyMgOVjV#Wut^r6CA+FDbwUt;**D=oL2lpp@8RIpti>sl|A3vyI7|f zEi8-h_??|^B$e1YOdS~iZoW|YU_-c#z;&-KHzhI;oL~-lm1e?9M$^~1H6j6DX`A13 zB_0V!8jQ>IVqU;xO{jaX(V#~ME%u)cC-O)Ku&yev>RjqcN zL{P9?W}SpV^`MKpJ{Ib5zQw~S4!4<9%vttjJ5ju$JcG}n={XHROxR~bjHRtifJI#G zwx{r%AdW1((IS+MJ3UuSSdw!wCq|^0{OiPY@_xUY%IVfm-e&%37#NO1o;qEs7s6TF z>>#o^ab2f0nJn!BXh(?5=^=AuPM0kv0-$eTS6S3&w{3S$a>MU5Y*F>BqaTxv&}vHU2Z z=3Ol^`6dh!0{!ecZ5J}u>H0`x5w>6*V$iccv87l@$hE|=^5k|>TCF)OScL6ZN?25h z`Z)~zzF9;nQp3U^S>2&J6DhiAN1--nkQ@@-7^Jo{+q@0b=0FMgHtn^i`YhA&*^X?6 z&=R&1xW{ZuW-!*Bei^V2lBN@f;JbW z#Zuj^dne5PT~Bv>`-Ts#e#Q6?UhwXPg-k30yyKYL_@na#ay5qF!?oN6{$V_{%%8=p zcHXm+MXB%!#CMK!L8uXzFdKubP4`@@(%2;pC-*=>Z$07v018&x9>CWG0Y(nc*<2Eq zcOK#y z6OLN(hGrfJbbwCJ|AWcN92)@ay>Bx|_V-TtEua?&jKIB()Um??vZV?&XH$7H>;C~T zf;>{(58r+cU&0>y#AjFm0py6>{32g0kY0my`9*}YH?m;w_prM5nHkcCLK4fjSHK2m%40jrkH<4G7>Ysqxy>~d-iuc7$`9Iu29HXVCR%O!sE5eX%p$p>PbYbgDLy5uTLUqe z$n*t|0oOEkgf>jHd&zHMu}lmBKW&OW`~$mhAAHbHpkR*z=;r@_%>5_Vux@(qn0<6; zBPgsql<+at$Hh`k|7o}iN=L@~lYoYRlYqGZ!XCJt+OP`}A)WtXj{au?wOwHa14OUB z#S|JaVSA0uASz%;E|C~Ayb=4S<=nZn4nOh0<`Tfc>-7Jxw%#$kl4xlgj%`~L+t$Rk zJ+W==IGNaXCbpeSG_h^lcJgM9uJfGlTR--$uC=Om_v*g7diT2PuKk;KX2EFA}wEWXx4?8~TN>$45kc|j& zQL!=dRfc-L@w_uz;2;IhcorLMWDbOB6~IQ|zb+jq1j@d3;czh~v|f z^qYfm?>Td;Skg_ww{gPgoL-7nV5_*p07&?u`+(E~3^qR}DdY3eq*S-h-x9c*G3h?e z3Yv>5RcoAkN~ zD;=W4(t{s5K8-`yTYcYf`$Z|yhxD}pG+e%3f{&9Awwm=<#WpdKIIUO3`-xP( zbAHjNIe=2}&kruMNnh4#&RU04NH0_RS6c@vYC>jbgNFr|!bFQVqc6gT?XDa^fkaPc z|0L9fMCn4&a*%9WYUhdOM?xzcf($n#!)$*uBSKXR-fJuYeW$PAoi@> zKF?V+(#iC#wxZ-5KcBDGo5i}{{zMnvX7Js0)ICk5XHn$f^T3fDFaFvs;VbZYEKnbD zsz%dkQ)F)eOv-O&uo%oanH;(T-Vd7vv>R-57ATMBO4ocl+kHCs4&yt%nb~p96vVo1 zcud+-`8eKWSYj*7)sDokQ7DqmKG}UV!KPA#6k)&WLoNg4Z%`iqz;PX1FX{ zNsL`0<>Mu%ddv#Jr2+N=<-1#wy^e>~|70$8*XK^z zDk!yjJPUie0gV{3YeU4Gw0S<~i)VXW|6VB9<>tuv+z?OBKV64F5NI&D&v~Uq`sK)V zW=gZtEI)Y`_?d`-8Ja4j0`q(v_-eOCkN;*MP+a_H__NtLDDR*fFcM5ni|Dfpk55h5 zV?R(HNVDsjcUI*J90Hv(tI~Zv)~rv%v2%N}RM?P8 z$5dKr+V!l|I?i=cI()v;&~Kgf!T*KD`|WQr95*pEf$)gzHhYI)qdmQEZm@R@jd~&~ z%J4QRWn`|Ad0g89XjwuH{Yb`sX|##kpd~(&YyHN}9-W)WAd?!KIYUHD{$QF`?o$2* ze{rLy;Z#?#g!oRgXD>*wJ!IVYv~l#sa1g*Y#)#brg#DLtfyH)EIZmT`QQ5Xvu3Kh> zMb!_&kvGAA_=2}H^`z&~&=*Y?RcyIbTfQi-qC$mU{ox3(J}J9{Vf5!)6f+ZM(QHX3 z}#pUCte@{$(YZZ<9X@3QaSL9UVK?w}Gs8IrEe( z?mX8k10vL=rynZQS8dp*tMKbo~9CNW6t zQ(!3)fzTX7*MULl)TsSi?Bl_Tl7ff|g@9QR%NoIwmZ#auJ;XD(CCJs4CPG8+7n|@J z9G=wy^tY89W*z9uM4e|V14E|k`gpHf7rKxu&L?$%^%z(+)9P~;N`^y~pUMkuCdxIZ z)B~g7Kk;VF90z=04vJet17o~?cbbkV@H)ETU=UxE!$(}cOpH}EdjG#BO* z{3qE;;#kTUR)8+jnYo~6GTo%ep^CBwaB-TDb z?(4M%8uQJ1Xy&qeM`*X)uV{Rgt-Eu%>&oOFeEYVXLW|{WGxLeziae>?q<`Xq~ zz^6vWjPF7Q!_r6u!y)Ed!<#sy+}TnBKCm`!VNuqz*JzQh_#KohZ}EGOIHYE!t2}h; zN>KPo$wZJ?rJ&q4?TNtgyruvrY!FsAQv4c7b%4vVEf|Zf}MB1#`>YNanI9jXOk6oa~dMR(;&0i zVq-QuU;k1fX6xmkDES;_rf)^r-DN(S=5Z-gpFW!QV$le!64gZ}{RPEN{yx_8Fir*xXJBz*o1O|1#O?;Lfbsc1=k` zr6feCm!!9ktq4JvnVAybevDD)HRnRbJ5eMVBgJpa^zcAG!n$sh$Bhrb=P{CYCNMgN z!6CwpA5D`&kD>Dx3L`r*BJ=b#YNAi8C1*f+rWm|31VWQe_{n-#7juH=uG@sm2%+cUacGoEYb8dB56R zPFt@Zdqo%s%IJ-Qs8pVydv=(gq9AYhS@TwREI2E6kH5g2CNlvb6pH1r7^LlQiu`?D zQxepKO^il+xlo=Xhd{WNv)*{=R}lK3ZGRfBdKwz#<$V}}j=T6)F)JeQVBejj{)m2@ zFYHoQ$6Tx<=aKOIl~JS+C12z(P0qM@Z>5fF>})dwQ>Qt?Ms4!Bz59yzP4EMZe62|p z;ye@yOazO_sZSDc(#T=zpPNMj9XdMz2LIMZbXH4R^?r{AR)i4sY~O=PbgVp+lz@LN zOmHsSiro5E>mZ*eMB)l%X4=wt6@?9(oqd1U@%N=zBkusfg?StcI#}p?u^46P?6Dw{4O5D+sv9W2+$F+-n)h#&){(3%xW}?%4^RKpjQSZHL71u zt<~XN$OVW#4$cY+S%Et&C#LAZDk7<6$X3Do>^B}$#SzoD?J>U>hHDwZ89Ijl`b9cH zMf^Dx3xFB61(S|;7o@vOgZTuBf13-Bp&dfsB$t-|ZhtcdzO`u}5g9EFMlMq{=ahP< zgpm=JE7O6$+_@xEXNQd!$Pd=w(d7mjiY1%9qRy*e=?Sq%1t?Uf=6VVM$txqkb}Awo z;(fjR1a3+0SPVZ-Jo)0L1CkV#6R8q12>2(X9TXJQUq-ux3x8UGo&&CzA*PKPkFvEk znk0`tMucCSd78E)8YQ>f%jsi0nn@=e#l$a`>?ZSXH&-_|Ymf|7d1efDJLb+&nqub6 z9GSH?k@h>Yex%!LPdCq%9+n+CZ@ZE7DgA@>elMq36l-HoHbM2saB_8NCz-fb$zzlw z6713nk@eV*W~>8%p9HKl1!j)a^CZFW`1C#d(|DU#d4b?f%ow`xIhp~Z1)w49!jQ1E z_F4p8zl0$-jpJH>70K_SIVjEUkdq8Aw$|?OW@#>%)??dsFq!1-%M>e1KVZPTg465P zOXP!W_99~>m?X2f(nduwi3{JOF4aj5b9ePxGqQyhgs3V5LRkEMI|l0`psElkUss>n z*SP71;W}&Sv)ww7vn@JiZ=Yt zAeQs93Tw0$uB!-;fPN&#)%~WCW?zV3_8#yV>Z|`)uG$Nyc+9@b*%c#~Gade?L^LaZ zx2s-T7r>v`Tz(>Hg}p_lCGH_xNOnj)?9S$cCmIq`9F`ZG=WJ30N0RIS5}bMh!YYwh ziX9tSATnqC;4TzT_pSFfWA|4!epkb9pH{{b5}Bc&l%)x! zqDLEcH0ueYGx&D=>}28aT)iAw%n={xrP6e58vvig5Q`t)4n-qL9hRl3-yLqI*)4Mw zKhP5TNudXcBVi>Z+#xhYitpUutZI9{H1$jVy|3};~rt_ z8GLCQ;$sGsb+Zf;yG`K`K%39Cvc2orRu@@8Qjn8a&8(~&pjx7;&3`pkz9jrf_O`qh z&I9Pk_6wS>;g20{#7?_V$8alM$8VQe5la1>MmJ+NCxKzRPS~Fb(RbNRW!ru8z*Yh4 zr=$~Nf)1nJtyXOKBcJn*c9U}$e4EP}<;XuC?zBFObMb+|?Z;-3rPyA3fmyj8`5@~f zACehrcyzq0cz0HV)`}<+*IG=Oo+TkRM2uO#rCRHb`{?^E)d-~ z-X^)c6O40fW4ZBEt~~VCf4yI)$qikp_RqVt>gI)s|aU&d8aP~R@Ew#R|2cl?~I#C!F&-4oanjF82Cs%X->px&|Fhk4cy zo+S5A)33J=iKi8fO@+0o9+2S|0R@ChK+)vF*X}dWNq~HdH%O>9n6WQZ z9qQQ06wtx@K<0p+ru@$=h-i0=i`V_R7t}>`YltYFfEU`W`I)V z?qj@5qTao_^l*@}ll3knyuyJ{S=e`alTR4Gt`k78fed!Y ziHin0@=C3kyIVZCKf+Fir!#-qY$5gy#aWP@6sbCTI(-&FDg(PFBb{sfo!l7 zl}FVugGxlFMr&R9GbHZGdY>`mcvvxBC{1`QvMSAew2qWRi?fePtPrnJL{Y5`)5gaO zG|?`20^R{PM$^ua+AQ#pZW$oxHSr#q#&WXuQ^_WpI8i7&ssdaDXIy^;~o@RSs7RJv+-* zjr2ksg62YGmnzZSzU2MY8tbC-2d%F zm$e34NIxhgzj$TH(HqdHI`1PC$;Zd1Xg64CTj}URAgnAmtHxGNtqHFUSCjXOS)a!X zu7D9XiLib>%o>ayF=DPVcL5}!n9FY+{Nn*u9MKAoAp5E5NmoHd=(huRKYXYK?c?OO z;wFs+tX#?ro`FQI9^mFy6oX#@s&KrD*` zM$*TPZJ*J6c^H6B6~Xcx?4ACZQOT%wG%%XNFj?Vgv=P4`qwq~@O0BXLr$xX)Ubyq`IY5Nv^1>#HDJF%I%xg7J=zHME$cQWav)jH?)<6vuWkv-5_z3 zFi+nFqI_oMLdiC38&^tA2eW&a!DolH>1^743L(mLkSwZ zOAIN0H{EEMu&Px zfXf?G{UhL|pkN70Ob^s|ZLRgy;qYTT>)>LC!>qc*W+x_tZ(B+jzdD-V^g!64M^<>uucJ$Gz7_e)D! zLYg(D1Ns%y?ZZbBOJ+09Q;WxH-=^hO!NIvVAv=KQb9EDj`r*3yw!5rb^St?8?e(*# z{z*VV8WO5xxkw=e8VIOQ90=(DKDxC>hyX~VB?EvZM$`4Xfu4}=b7jMk;8)$4=bL^m z_6&gAU-q1?UJ`(8whShEkaiqvzWfpwb$A&*~o%6eCnGIf?RSrRE|d{pS~HQ`_nMDX+^EX@Gs)6eI- zTQjpL(1wUJKK$i7A&dfh3T$eeyt<+U-~r@32jWzd_nwsIWL*R8L4SQd=ddZ>lkm2S z;S#(F^WUM!8$ZXr5bZu{IGXsY5F`8}RS4t>WewmVN#v%t%yp|@t0E&eP%SeV0ku;#{o+!F0N|%$k ztnMW#SqjcO^oUNpM|TKD1y3rgzR%VEP%Hxt!xSMEzv5dB9yj)zzH?*Le82uNYy;-> z6VJMB>3X@{ZkDO*$@&)nc8#O+vNW`%>Ux_a2Fd+{{gdCF_Zo5N(Y6%eM&dzmBLX4d z<=qw^W#uhK%o8`gNJ(;*5$JwAm|dIF7-m;xKa}B*sAj#()P(u%#xNA|@G)#r=I8@; z%D4Yh@H2}Sp&YFk;TK(m!xY3UW&^pQA5VZXSOcApLZR@oTVj7l?vj&CntYzl%_d(3 zLTz|2bE4zec6~<<*bfGPyyl0wSVy%1j#nzSw4;wSQyYsN(Pi z1BJ5JNpKwq8E=e#eLE;eBZVuAeZj@=ann9g7W>_*-`Dt0%ROy64uxxI zLVw5got+!I^b){l6o=#fi>8xI?7ekew#-sYkI(=(lxMZ1BtXu+L ziXC$3DTTR$Rl-}x$gVMZAIvUB^f*RX4vSq9%?tQu-0>!ryaoCy+~I|GYqmkL9axVk zCe&{LMCvgJUTv1hha1qY%?FW3&j<+&?)cUdK5JiH=ol1A!xs$%s5nKO4#{(C+v(?8{53>N^<;sq5JTx?LCD}6FHpPJt^hK|Q8Fc3c z#oO{vvn*Aay(fZ=26#pwz&#m;vt=ZyzbhwHBrH5i?FNxWA4Vs?KL$~jA^T`5f%q#w zd{qHWQWizJWhbU%$m=V?ejfIDBRq6Ho|C-CmPw%kgs{ABYBUP7O^0mXQdiATQ;Rw8 zGI%L+5pTgVOYO7>GyW=0;+ma1>>KQoHsis49AVm{{D%XPY^B z{T?)j=42aLmzPT2&(zS~^?-YN6~Q5409mYKs`VdQdqTz1A>Lla9%idry5T`Y)ilhW z`DIK+V_^8Z#&DgMj;>@aS#|T5R zh@td%@JG0+b83BE4!HspD^*pTF?-Xw%&Ib};nBVuqKtrttf&+y(lSrd_{XfVrmE`E(td?_bS|@~ED6atSLL^}Ee#No9Ug0?Ql* z8X8(dNPwOr+ZmnRY)wd8g=n$FgxV?vMgfgu_nxKmej4*q8Sp#k3uG1V{8O?UC*?x3 znyZL(l-EqGLavjU2FiMM$9_BBZ&M)r%Ho1`2G7o@EY954l!dj6lsd}`ma&W498-ayYzT!f*#e`L*_ z_h->y*kx-WgNdQ_F8!*X;hcbnEBBcpKLYAcfa_62>8;&V|YRO=2g9SeC}5WiL= zqRzwGQSbfOXh$*xs67g>(ea*DSt`5N{Ot6Z;cB zqe{Fs0RiL2HGgI0W;Cu2jfVrqg{w8CU`|o=C#}##4<}K3RR`Cx!WWBM?-&w;5&@G$8@5HDG)k@gg^QmfTV2eE=W`AY@y0Q`W&`5Zr=K7c>F z*XIwOf-M@Jh*hHHcHAGOGcR`!p}g>isuM)fpo+=!NbpkEQ=5iA#NiIRtB~Dz^>ZB^ zgY;VddWf9_p6^bNF9itnC|9LfCg>}tdji^>j$nkFMgnet=)KhhF&xS+r=9~W_f#qZ z0pZ>w+K?b6jCTLLpK10>7NMWiR<~GP5J%@MQ^nX4%B3wbj!T4^2_elmI!&hEOxQA3 zB7)UaacRm|40#vIEm1{PQ8}6p7_7{d8cXZSWKE4a%h;9G8lQFQ)iP+x($||qchl5V zwJFP2`n8S#s*US5K$WNLg!_|fI!(5PTiop7@a!;4lm-QUQ6T>oZg|;8a&N}ZXt$DE zR@YfBkkMfstRju*DW5?C-Z;GAiP>zz&4@gg58?VQe*V0366sv0+MMaTVnsqP`VKuC z&k0RKSN`)ii||=1l-NrjC$lQ8hp^OORyqV?xhC0&{eB|GJaYeU3;#+rjtS zz;hJ(A%|k-riz0v%fRzE`eCy|W)4>DoM}m0s76_PSYca?Mj6k~!nPDNpxN(y5hG)F z4`63E&&CxU>qYVy+!@*Pe3Z+Xl#0CDZa~q8E->ltbQBp}7jZdT*7#8a-j160aFBrK z=+qmzAQKj!y9+ZWMN?%X(u1KuLmto@W%o-(6bQ~p7r)y+9L2rLu=ShHK$^L;`Q3oaDRX){x{zP^IOBzd10}7(1;q_}3YJJGhL&$% z*ahCGGjK`8hz(DiG#o0mw%&*Vl6^zzGZ*M6Li!~uN zm^t5>jbjn%3ef1up-1#34QjuZ^B*$WeS%VgyweWRRHdV;66`g#d+UtG5aEC*_b7P0 zHeT)La{1mQV)gZx1uPmXHi7{hR)aOw0eW6~ z^>U#QlGn3A1u;kwQBVm(gRN16r|1|V#U)~!Pn#s7l;r1OjMhO|ibltZ*&8^KB5!ld z_dAU;X;pc{F^1()HIkxQwpBQ5dx_mw4Ic_Q8dvcC;1EDbF5hEfEhmzpMN zgqc=ReBrGIdn?yB3jQLM4Y&gL;gX%ii5jM$8Ap030h(eSd9^x_g;(ED4y9-w z0?$;Xi3WRZ$=?ub{YkIhW}2sPsOe6QYxy-RbJL8d zA@?EY&!JM}Nd=aKFIdFrWW(22&9E4{J^-8&-&Gt^`h)R|&haKJ=t7;!0s-I%ho#8+ zk;>BPSB!Z-xKnMmyaR5bCnz{`w!FLG4q5JLgXI&nJmtBP+=w?MPN$Fu_xq}m7fubA!y z1S{w+EW~+mq~%2um{3#z9Z_;f8Am9&N6oY)M<3$!iBgn4?q-N`|5~}qpf~5<9Yd)B z#qOmlV2WKY8ie004-n3{6dxpf*V|sP5T6yF_g?x;(#wq`HU}o4MTB3Ttv6*}nLDPSA{}txh zWP$z;6*TRc_{RVc=5i6!pBwst<8>k2H5>pzGvF+6XN)OLEftU>+T41-2@r1Bzd{>1 zj2?Fux}m2$N)ayjEwy>2PSZ#@;$9`Z;s9xn=%yRIq~=CGwd`FHk7(t6F6zr7*DQV`!fuaGiD;J> znF72V0;#j>+4Yk_)~%YgvSULvH-x4?mGFf**L)A?sn+LlZ)h7{i9x^!=vR3#S8p|9 zXk2RtCR)Fr1CC$@A>@Yb`51kY4;oA=D8m=B1Lg@$Qmx^cqFDP`7XU~Jg~%A%-)PNR zD^q50`D&mD#bHTvc-;b<{%oR*^b>HLb1x4vIEIMonX z(B5%}#qVa%nv{a4Tz^j4eLBw}FYG1hI9D*49a1C~J@0!n#GNCUwfGPwf62z%N!G9t ztpF~&;QK8i9X~Ii&>8@}$2ScUQl>ieuu6q?_OYhe8H2RYSnX-6U-(G6&e$Kxh7z%` z{6ckO-ex0j8$FV|y?@Z$b)9|HGA0NS;m|=oMvTl}$LD1gv)>MgT-{3+PUd;lXaaFit)Q@`>=xoj9M$uj`q*$19;P^X^X_W*3WLq4;IqO>*mZG0m{ z`_K$cJ1Xn=tRYY_(_c6EOkrFV&xgFg)st7x-J(X%v9f6r>l=c@)Rz-@KHUq^Y1L?c zr*)E&H>+npM`{%Q9z+k{Ah_2I-@r0^jjFVCXks=Z(DdNnjI6avW1^;=R5$sfgWYa% zfx3N*+sW)JatBZgR=rL?!v%+wM$1*`kr%KrVRKj5O{=Kj9rouVvu5DxfuuYuw$#~3 zW~gA?+$~C5D<7Z(Ayey+Le4Bfq_#9ReaIDtHVPUYc-o!W3@?-TPOPUEbh}vy99{_g zvYsPVcZJN5iv&UJrsqj{$j3E$;~^5N*KCpo!%E)OP!2F`l~rIzq`^-3BY=W&tABYl zaj{2UwDdZO+PZYDL+c7lfK#d|oYSOG?aq3Go?2>96p;3EGd8D4$D#@@iD*ilS~?jd zG;mF&8)?EA+-_YzRw4AE>rc~cjz~=@Ta_s2GAojZ_)=&4{46&-2$SJvc2M+O%+=;E z9>*(EDGHd^ufe>Mvhn`01PdyFzFM2swHv>$N#k&$nc2V?FOmMVMdBA>-xOi*u)I%w zY9u0DrO|3hqR&f9qe8{EmW(L%@-rpxj?t8&1Ki!9+N8U$B^k2F(2T;}z-|gud0KA^ zvP`6Q4kUO9r&sp-k5a}R5)b6?4h69XGSJ$)C>Ov*)6n5utK#6RufVBguj%G5Qv~_C zU)e2W`&3h^^mk zv(1Ai6J%r^0pLjIaN?rctLtz+FSO0=vTsnl^LXu?&fAhoIFnz6%;yK&#eGsw((+^T zW}|a`6roej&kGw8h)-=qNT$c`YPg199Z`+HA3Wdf-}^H?rpR6df6;i(NAl zAh8IJ0WlRwiu$Jl8$yI(6Pfy@dhGYh{P0bZsk?Ei*7EZfg@a#wml>got9d+e{oroE zkkDta}_ zNEwgnFY05TJQG*l@!x=C#1l4GO$N_b_G z6;#Tw4!y(8(5Sf^fKsyLj`=Zzj==p>u#!llvs5&N%^lOjFi9Z(MFIdef{0vRj>Mj9 zL@+Uh_h$i}fa7G@8uR=A2zoeX$=C3ug8m$*0UQbH{JT>DAK5N z_Ealp=r#Fk-m&`N3iWt*u!uh%%TJ>Y|D0xQ)Ck}q4C2qEu>K{Wfv{R^%bczOFBi{$asH&Mra&5fOX zcV$8>9?{f@1%|%S$48+@{j+7ED>Wb60zg+d@uKVg;Y zz?(DMfJ*%E-)Hb>$|qXvM?@Zwi43)_!HvWE4AW`)!@1$pL3ZK#h?#Sd0Cd|!o`YDz z^!a@83H*0aXf2f^77Ygo2<#B}e-?$d3vmI`cDTf7!H@6g-_pK>t;R`vu@P- z{qaWmTwgy5B(L*?$G-H8sBvKFBws@c{v;5!D~cRu(G^Klsh6;4AHzX5V%c0#ZlMLx z4Dl{=Ovy+Pl82*~ga|3qyh@i{jy-?Ui~nk1CPf2>K^_`rA?jdS9Q286X;}Tu_nS#X>-BxRV&C$teTdga%(Iq|FTA$++-BIOEUo&1y8??fE0lCOzOxsXHl{lIk5& z;1t#y2-2t-@n-l8)H{pCNUzpXxwf5R_hoj}qMp90jA3vK;4kGKxgOUZx}l+4S3kVz zG5R*T8E)|fl2jb1D0a2WQ|WRm{SHT$4`NmShePxJb(roH?1$zqV*5bzN9`K{=-*Q$ z;gB}$2Mh!R4*I|AcFDzr0A1yBUZgGnAh_?GM@wqn>BxL?YQ^%M8{@?XlPEbdmQgZx zK_4JwfiGH<6i~qOP!w#OqsyK_(XK7`F~;Y1iL4X?#h+9LS=RtrD{38NRy>otx%^4@ zZt>TtI={QEOqJUr2P9k(?GLF5$kKM%5$?b!@6OcK zr;fNB(+wh&BMWuv)+_y@!$w*_fXY%WD=Sk=E;ud4=y^16sw{JVkX8+fH4}{!v>sDR zBN+`%deU0a#(wT)3!l??{qg=3m5>zhWJTz3B-*vCy&f&YlN2=a@-1Mye(@J;>msvo zf9$bPl+YAKR|CNw09GH4WUMg6Tge0VhIq@$eRA|W0dWS8#Z-6C$1d9dQQ(EX=nT`%@8j~`iqr~ zfKn^?aw*S63arY{HlYq`%=>Dd1Ul-)x1V6}0=w0Ib00@#eUyNxSL!c>8

Ze)( z>hwUUMUVswlJ3C%@D^NBaF9auOGRLJ^sl(~m_N*Ec@tX3{Dz&Hk;ot*d~=Tc98z_T zNLcXIiys#)hicP@aDInhu z-`WOiEz@oWu>@5QET`e-MV-6tyX{xVzFY?7hqV(U#s}9zWAXA^lfVUS;x@Tn14LUZ zEH?LM%MrW+DF~Oh|CD}{ZJ+K~zI9PQm4V!O?K!hZ-!7F9xEw+i&S>3IqVDv785g}!4GzLRsPCyUy1Qi`MGXScqU(|#0&a%o_jhOy+FnG5F1gJ2 zL9vO6_hPiS^wW6z;rWz)Yhdf|k39Fk`19;k45iIN*$qsiv^{Q3Rme`WYXAHL3W1=? zXifzV1mr^gKeyC+#7a(J$bUmx5o;76|5dO#VZNH>KXO(z8|YuiYgRRJ!czkr?0*$@ zPN1yj{zso&6>$P;HOhZv!UmZCGw!c#VHMiH`WgQHoAstOhQR+&j@GjLPl@ORxH?Xa zzg46E8_5Cl7m@p)R{d2=+R53&+Qf<8-Nsr$8W;o(2o308$0PaQ!5;QMHh%?QjfBiP zY_-46|2y1#6|4TQHtGljWMks|cy(5)K;vx#``23qtChkk&x=kKU_#T#z5$3Bc8O33d%I z|52rQC%`w5|HppZJ7J*ypZ0$Zz Date: Wed, 12 Oct 2016 14:34:51 -0400 Subject: [PATCH 29/30] Fix graphics naming schema to match actual files --- src/ss_personalization_manager.py | 4 ++-- src/ss_process_story_ods.py | 19 ++++++++++--------- src/test_db_manager.py | 8 ++++---- src/test_personalization_manager.py | 4 ++-- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/ss_personalization_manager.py b/src/ss_personalization_manager.py index e39f78b..4a8a828 100644 --- a/src/ss_personalization_manager.py +++ b/src/ss_personalization_manager.py @@ -221,8 +221,8 @@ def get_next_story_details(self): # If this is a demo session, load a demo scene. if (self._session == -1): # Demo set: - graphic_names = ["scenes/CR1-B-a.png", "scenes/CR1-B-b.png", - "scenes/CR1-B-c.png", "scenes/CR1-B-d.png"] + graphic_names = ["scenes/CR1-a-b.png", "scenes/CR1-b-b.png", + "scenes/CR1-c-b.png", "scenes/CR1-d-b.png"] # Demo story has scenes in order. in_order = True diff --git a/src/ss_process_story_ods.py b/src/ss_process_story_ods.py index cc009ad..a92ecfe 100755 --- a/src/ss_process_story_ods.py +++ b/src/ss_process_story_ods.py @@ -203,7 +203,7 @@ def ss_process_story_ods(): continue # Graphics scene numbers are 1-indexed. insert_to_graphics_table(cursor, - sheet.name.lower(), level + 1, scene_num, + sheet.name.upper(), level + 1, scene_num, sheet[level,key].lower()) # For each level, generate story. @@ -237,19 +237,20 @@ def insert_to_stories_table(cursor, story_names): def insert_to_graphics_table(cursor, story_name, level, scene, graphic_tag): """ Add a list of graphics names to the graphics table.""" - # story_id = The id from the stories table for this story. + # story_name = The name of the story. # level = The level number from the levels table for this level. # scene = Scene number (1,2,3,4) to load this graphic into. # graphic_tag = Tag of graphic to load (lowercase letter). print("ADD GRAPHIC: " + story_name + "-" + str(level) + " scene" + str(scene) + " " + graphic_tag) # Graphics file names: - # [env][story_num]-[background_type]-[tag].png - # e.g., LR1-B-a.png or CF1-P-f.png - # Levels 1-5: tag P for plain background. - # Levels 6-10: tag B for complex background. - graphic_name = story_name.replace("Story-","") + "-" + \ - ("P" if level < 6 else "B") + "-" + graphic_tag + ".png" + # [env][story_num]-[tag]-[background_type].png + # where background_type is b=background or p=plain + # e.g., LR1-a-b.png or CF1-f-p.png + # Levels 1-5: p for plain background. + # Levels 6-10: b for complex background. + graphic_name = story_name.replace("STORY-","") + "-" + graphic_tag + "-" \ + + ("p" if level < 6 else "b") + ".png" cursor.execute(""" INSERT INTO graphics (story_id, level, scene_num, graphic) VALUES ( @@ -257,7 +258,7 @@ def insert_to_graphics_table(cursor, story_name, level, scene, graphic_tag): (SELECT level FROM levels WHERE level=(?)), (?), (?)) - """, (story_name, level, scene, graphic_name)) + """, (story_name.lower(), level, scene, graphic_name)) def insert_to_questions_table(cursor, story, level, question_num, diff --git a/src/test_db_manager.py b/src/test_db_manager.py index a60926b..101c77a 100644 --- a/src/test_db_manager.py +++ b/src/test_db_manager.py @@ -266,12 +266,12 @@ def test_get_graphics(self): self.assertIsInstance(self.dbm.get_graphics("story-ki2", 4), list) self.assertListEqual(self.dbm.get_graphics("story-fo1", 1), - ["story-fo1-P-a.png"]) + ["FO1-a-p.png"]) self.assertListEqual(self.dbm.get_graphics("story-fo1", 2), - ["story-fo1-P-a.png", "story-fo1-P-b.png"]) + ["FO1-a-p.png", "FO1-b-p.png"]) self.assertListEqual(self.dbm.get_graphics("story-fo1", 8), - ["story-fo1-B-a.png", "story-fo1-B-b.png", - "story-fo1-B-c.png", "story-fo1-B-d.png"]) + ["FO1-a-b.png", "FO1-b-b.png", + "FO1-c-b.png", "FO1-d-b.png"]) def test_record_story_played(self): diff --git a/src/test_personalization_manager.py b/src/test_personalization_manager.py index 5a6d257..2d57dd6 100644 --- a/src/test_personalization_manager.py +++ b/src/test_personalization_manager.py @@ -50,7 +50,7 @@ def setup_demo(self): m.get_next_new_story.return_value = "story-fo1" m.get_next_review_story.return_value = None m.get_level_info.return_value = (3, 1) - m.get_graphics.return_value = ["story-fo1-P-a.png"] + m.get_graphics.return_value = ["FO1-a-p.png"] def setup_no_participant_data(self, participant, session): @@ -71,7 +71,7 @@ def setup_no_participant_data(self, participant, session): m.get_next_new_story.return_value = "story-fo1" m.get_next_review_story.return_value = None m.get_level_info.return_value = (3, 1) - m.get_graphics.return_value = ["story-fo1-P-a.png"] + m.get_graphics.return_value = ["FO1-a-p.png"] return m From f420f356011c9aaf6c546016a467dd2e4b5f6904 Mon Sep 17 00:00:00 2001 From: jakory Date: Wed, 12 Oct 2016 14:54:43 -0400 Subject: [PATCH 30/30] Add more personalization manager tests --- src/test_personalization_manager.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/test_personalization_manager.py b/src/test_personalization_manager.py index 2d57dd6..08c9756 100644 --- a/src/test_personalization_manager.py +++ b/src/test_personalization_manager.py @@ -244,9 +244,33 @@ def test_pick_next_story(self): self.assertFalse(self.pm._tell_new_story) + @patch("ss_personalization_manager.ss_personalization_manager." + + "pick_next_story") + def test_get_next_story_details(self, mock): + # Test demo session. + self.setup_demo() + self.assertEqual(self.pm.get_next_story_details(), + (["scenes/CR1-a-b.png", "scenes/CR1-b-b.png", "scenes/CR1-c-b.png", + "scenes/CR1-d-b.png"], True, 4)) - def test_get_next_story_details(self): - pass + # Test a participant with no data on their first session. + dbm = self.setup_no_participant_data("P001", 1) + + # Mock relevant story data and personalization. + # The current story starts out set to None, so get_next_story_details + # will call pick_next_story. + mock.return_value = "story-cr1" + dbm.get_graphics.return_value = ["scenes/CR1-a-p.png"] + dbm.get_level_info.return_value = (1, True) + self.assertEqual(self.pm.get_next_story_details(), + (["scenes/CR1-a-p.png"], True, 1)) + self.assertTrue(mock.called) + + # If we start with a current story set, the mock will not be called. + mock.reset_mock() + self.assertEqual(self.pm.get_next_story_details(), + (["scenes/CR1-a-p.png"], True, 1)) + self.assertFalse(mock.called) def test_record_story_loaded(self):